diff --git a/examples/next-js/src/app/api/actions/transfer-spl/const.ts b/examples/next-js/src/app/api/actions/transfer-spl/const.ts
new file mode 100644
index 0000000..c5ddd40
--- /dev/null
+++ b/examples/next-js/src/app/api/actions/transfer-spl/const.ts
@@ -0,0 +1,9 @@
+import { PublicKey } from "@solana/web3.js";
+
+export const DEFAULT_SOL_ADDRESS: PublicKey = new PublicKey(
+ "nick6zJc6HpW3kfBm4xS2dmbuVRyb5F3AnUvj5ymzR5", // devnet wallet
+);
+
+export const DEFAULT_SPL_AMOUNT: number = 1.0;
+export const SOLANA_MAINNET_USDC_PUBKEY =
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
diff --git a/examples/next-js/src/app/api/actions/transfer-spl/route.ts b/examples/next-js/src/app/api/actions/transfer-spl/route.ts
new file mode 100644
index 0000000..f3d4b53
--- /dev/null
+++ b/examples/next-js/src/app/api/actions/transfer-spl/route.ts
@@ -0,0 +1,214 @@
+import {
+ ActionPostResponse,
+ ACTIONS_CORS_HEADERS,
+ createPostResponse,
+ ActionGetResponse,
+ ActionPostRequest,
+} from "@solana/actions";
+import {
+ clusterApiUrl,
+ Connection,
+ PublicKey,
+ Transaction,
+} from "@solana/web3.js";
+import * as splToken from "@solana/spl-token";
+import {
+ DEFAULT_SOL_ADDRESS,
+ DEFAULT_SPL_AMOUNT,
+ SOLANA_MAINNET_USDC_PUBKEY,
+} from "./const";
+
+export const GET = async (req: Request) => {
+ try {
+ const requestUrl = new URL(req.url);
+ const { toPubkey } = validatedQueryParams(requestUrl);
+
+ const baseHref = new URL(
+ `/api/actions/transfer-spl?to=${toPubkey.toBase58()}`,
+ requestUrl.origin,
+ ).toString();
+
+ const payload: ActionGetResponse = {
+ title: "Actions Example - Transfer USDC-SPL",
+ icon: new URL("/solana-devs.png", requestUrl.origin).toString(),
+ description: "Transfer USDC-SPL to another Solana wallet ",
+ label: "Transfer", // this value will be ignored since `links.actions` exists
+ links: {
+ actions: [
+ {
+ label: "Send 10 USDC", // button text
+ href: `${baseHref}&amount=${"1"}`,
+ },
+ {
+ label: "Send 50 USDC", // button text
+ href: `${baseHref}&amount=${"5"}`,
+ },
+ {
+ label: "Send 100 USDC", // button text
+ href: `${baseHref}&amount=${"10"}`,
+ },
+ {
+ label: "Send USDC", // button text
+ href: `${baseHref}&amount={amount}`, // this href will have a text input
+ parameters: [
+ {
+ name: "amount", // parameter name in the `href` above
+ label: "Enter the amount of USDC to send", // placeholder of the text input
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ };
+
+ return Response.json(payload, {
+ headers: ACTIONS_CORS_HEADERS,
+ });
+ } catch (err) {
+ console.log(err);
+ let message = "An unknown error occurred";
+ if (typeof err == "string") message = err;
+ return new Response(message, {
+ status: 400,
+ headers: ACTIONS_CORS_HEADERS,
+ });
+ }
+};
+
+// DO NOT FORGET TO INCLUDE THE `OPTIONS` HTTP METHOD
+// THIS WILL ENSURE CORS WORKS FOR BLINKS
+export const OPTIONS = GET;
+
+export const POST = async (req: Request) => {
+ try {
+ const requestUrl = new URL(req.url);
+ const { amount, toPubkey } = validatedQueryParams(requestUrl);
+
+ const body: ActionPostRequest = await req.json();
+
+ // validate the client provided input
+ let account: PublicKey;
+ try {
+ account = new PublicKey(body.account);
+ } catch (err) {
+ return new Response('Invalid "account" provided', {
+ status: 400,
+ headers: ACTIONS_CORS_HEADERS,
+ });
+ }
+
+ const connection = new Connection(
+ process.env.SOLANA_RPC! || clusterApiUrl("devnet"),
+ );
+ const decimals = 6; // In the example, we use 6 decimals for USDC, but you can use any SPL token and change this value
+ const mintAddress = new PublicKey(SOLANA_MAINNET_USDC_PUBKEY); // replace this with any SPL token mint address
+
+ // converting value to fractional units
+
+ let transferAmount: any = parseFloat(amount.toString());
+ transferAmount = transferAmount.toFixed(decimals);
+ transferAmount = transferAmount * Math.pow(10, decimals);
+
+ const fromTokenAccount = await splToken.getAssociatedTokenAddress(
+ mintAddress,
+ account,
+ false,
+ splToken.TOKEN_PROGRAM_ID,
+ splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
+ );
+
+ let toTokenAccount = await splToken.getAssociatedTokenAddress(
+ mintAddress,
+ toPubkey,
+ true,
+ splToken.TOKEN_PROGRAM_ID,
+ splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
+ );
+
+ const ifexists = await connection.getAccountInfo(toTokenAccount);
+
+ let instructions = [];
+
+ if (!ifexists || !ifexists.data) {
+ let createATAiX = splToken.createAssociatedTokenAccountInstruction(
+ account,
+ toTokenAccount,
+ toPubkey,
+ mintAddress,
+ splToken.TOKEN_PROGRAM_ID,
+ splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
+ );
+ instructions.push(createATAiX);
+ }
+
+ let transferInstruction = splToken.createTransferInstruction(
+ fromTokenAccount,
+ toTokenAccount,
+ account,
+ transferAmount,
+ );
+ instructions.push(transferInstruction);
+
+ const transaction = new Transaction();
+ transaction.feePayer = account;
+
+ transaction.add(...instructions);
+
+ // set the end user as the fee payer
+ transaction.feePayer = account;
+
+ transaction.recentBlockhash = (
+ await connection.getLatestBlockhash()
+ ).blockhash;
+
+ const payload: ActionPostResponse = await createPostResponse({
+ fields: {
+ transaction,
+ message: `Send ${amount} USDC-SPL to ${toPubkey.toBase58()}`,
+ },
+ // note: no additional signers are needed
+ // signers: [],
+ });
+
+ return Response.json(payload, {
+ headers: ACTIONS_CORS_HEADERS,
+ });
+ } catch (err) {
+ console.log(err);
+ let message = "An unknown error occurred";
+ if (typeof err == "string") message = err;
+ return new Response(message, {
+ status: 400,
+ headers: ACTIONS_CORS_HEADERS,
+ });
+ }
+};
+
+function validatedQueryParams(requestUrl: URL) {
+ let toPubkey: PublicKey = DEFAULT_SOL_ADDRESS;
+ let amount: number = DEFAULT_SPL_AMOUNT;
+
+ try {
+ if (requestUrl.searchParams.get("to")) {
+ toPubkey = new PublicKey(requestUrl.searchParams.get("to")!);
+ }
+ } catch (err) {
+ throw "Invalid input query parameter: to";
+ }
+
+ try {
+ if (requestUrl.searchParams.get("amount")) {
+ amount = parseFloat(requestUrl.searchParams.get("amount")!);
+ }
+
+ if (amount <= 0) throw "amount is too small";
+ } catch (err) {
+ throw "Invalid input query parameter: amount";
+ }
+
+ return {
+ amount,
+ toPubkey,
+ };
+}
diff --git a/examples/next-js/src/app/page.tsx b/examples/next-js/src/app/page.tsx
index 4eef4b2..830c784 100644
--- a/examples/next-js/src/app/page.tsx
+++ b/examples/next-js/src/app/page.tsx
@@ -37,12 +37,12 @@ const actionCards: Array<{
description: "Easily transfer native SOL to any other Solana wallet.",
icon: ,
},
- // {
- // title: "Transfer SPL Tokens",
- // href: "/transfer-spl",
- // description: "Easily transfer SPL tokens to any other Solana wallet.",
- // icon: ,
- // },
+ {
+ title: "Transfer SPL Tokens",
+ href: "/transfer-spl",
+ description: "Easily transfer SPL tokens to any other Solana wallet.",
+ icon: ,
+ },
// {
// title: "Mint an NFT",
// href: "/mint-nft",
diff --git a/package-lock.json b/package-lock.json
index befc209..41a03b7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@solana/actions",
- "version": "0.0.2",
+ "version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@solana/actions",
- "version": "0.0.2",
+ "version": "0.0.1",
"license": "Apache-2.0",
"engines": {
"node": ">=16"