Skip to content

Commit

Permalink
feat(solana): support actions and blinks
Browse files Browse the repository at this point in the history
  • Loading branch information
chainsona committed Jul 5, 2024
1 parent 1bcfc59 commit c2b8699
Show file tree
Hide file tree
Showing 8 changed files with 815 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@solana/actions": "^1.1.1",
"@solana/web3.js": "^1.94.0",
"@types/mdx": "^2.0.13",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down
601 changes: 601 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Binary file added public/chainsona-madlads.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/chainsona-smb-gen3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/chainsona.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions src/app/actions.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ACTIONS_CORS_HEADERS, ActionsJson } from "@solana/actions";

export const GET = async () => {
const payload: ActionsJson = {
rules: [
{
pathPattern: "/donate",
apiPath: "/api/actions/donate",
},
],
};

return Response.json(payload, {
headers: ACTIONS_CORS_HEADERS,
});
};

// DO NOT FORGET TO INCLUDE THE `OPTIONS` HTTP METHOD
// THIS WILL ENSURE CORS WORKS FOR BLINKS
export const OPTIONS = GET;
7 changes: 7 additions & 0 deletions src/app/api/actions/donate/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PublicKey } from "@solana/web3.js";

export const DEFAULT_SOL_ADDRESS: PublicKey = new PublicKey(
"2sbEWeiYGyyLdgmBB8MUwZWcyWBSzoCQd5T4fG3MPkR2"
);

export const DEFAULT_SOL_AMOUNT: number = 1.0;
185 changes: 185 additions & 0 deletions src/app/api/actions/donate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Solana Actions Example
*/

import {
ActionPostResponse,
ACTIONS_CORS_HEADERS,
createPostResponse,
ActionGetResponse,
ActionPostRequest,
} from "@solana/actions";
import {
clusterApiUrl,
ComputeBudgetProgram,
Connection,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { DEFAULT_SOL_ADDRESS, DEFAULT_SOL_AMOUNT } from "./const";

export const GET = async (req: Request) => {
try {
const requestUrl = new URL(req.url);

const baseHref = new URL(
`/api/actions/donate`,
requestUrl.origin
).toString();

const payload: ActionGetResponse = {
title: "Support Soona",
icon: new URL("/chainsona-smb-gen3.png", requestUrl.origin).toString(),
description: "Donate SOL to support Soona’s web3 journey on Solana!",
label: "Donate",
links: {
actions: [
{
label: "0.1 SOL",
href: `${baseHref}?amount=${"0.1"}`,
},
{
label: "0.25 SOL",
href: `${baseHref}?amount=${"0.25"}`,
},
{
label: "0.5 SOL",
href: `${baseHref}?amount=${"0.5"}`,
},
{
label: "Donate",
href: `${baseHref}?amount={amount}`,
parameters: [
{
name: "amount", // parameter name in the `href` above
label: "Amount of SOL to donate",
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 } = validatedQueryParams(requestUrl);

const body: ActionPostRequest = await req.json();

// validate the client provided input
let fromPubkey: PublicKey;
try {
fromPubkey = new PublicKey(body.account);
} catch (err) {
return new Response('Invalid "from" provided', {
status: 400,
headers: ACTIONS_CORS_HEADERS,
});
}

const toPubkey = new PublicKey(DEFAULT_SOL_ADDRESS);
const connection = new Connection(
process.env.SOLANA_RPC! || clusterApiUrl("mainnet-beta")
);

// ensure the receiving account will be rent exempt
const minimumBalance = await connection.getMinimumBalanceForRentExemption(
0 // note: simple accounts that just store native SOL have `0` bytes of data
);
if (amount * LAMPORTS_PER_SOL < minimumBalance) {
throw `account may not be rent exempt: ${toPubkey.toBase58()}`;
}

const transaction = new Transaction();

transaction.add(
// Set transaction compute units
ComputeBudgetProgram.setComputeUnitLimit({
units: 800,
}),
SystemProgram.transfer({
fromPubkey: fromPubkey,
toPubkey: toPubkey,
lamports: amount * LAMPORTS_PER_SOL,
})
);

// set the end user as the fee payer
transaction.feePayer = fromPubkey;

transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;

const payload: ActionPostResponse = await createPostResponse({
fields: {
transaction,
message: `Send ${amount} SOL 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_SOL_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,
};
}

0 comments on commit c2b8699

Please sign in to comment.