Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added SPL Transfers #9

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/next-js/src/app/api/actions/transfer-spl/const.ts
Original file line number Diff line number Diff line change
@@ -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";
214 changes: 214 additions & 0 deletions examples/next-js/src/app/api/actions/transfer-spl/route.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
12 changes: 6 additions & 6 deletions examples/next-js/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ const actionCards: Array<{
description: "Easily transfer native SOL to any other Solana wallet.",
icon: <WalletIcon className="size-12" />,
},
// {
// title: "Transfer SPL Tokens",
// href: "/transfer-spl",
// description: "Easily transfer SPL tokens to any other Solana wallet.",
// icon: <CoinsIcon className="size-12" />,
// },
{
title: "Transfer SPL Tokens",
href: "/transfer-spl",
description: "Easily transfer SPL tokens to any other Solana wallet.",
icon: <CoinsIcon className="size-12" />,
},
// {
// title: "Mint an NFT",
// href: "/mint-nft",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.