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"