Skip to content

Commit

Permalink
add paymaster info
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra committed Oct 31, 2024
1 parent 10d9822 commit 6de8411
Show file tree
Hide file tree
Showing 13 changed files with 885 additions and 63 deletions.
503 changes: 502 additions & 1 deletion examples/next-js/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions examples/next-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@
},
"dependencies": {
"@hookform/resolvers": "^3.6.0",
"@metaplex-foundation/umi-bundle-defaults": "^0.9.2",
"@metaplex-foundation/umi-web3js-adapters": "^0.9.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-slot": "^1.0.2",
"@solana/actions": "^1.6.4",
"@solana/qr-code-styling": "^1.6.0",
"@solana/spl-token": "^0.4.9",
"@solana/web3.js": "^1.95.3",
"bs58": "^5.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cross-fetch": "^4.0.0",
"js-base64": "^3.7.7",
"lighthouse-sdk-legacy": "^2.0.0",
"lucide-react": "^0.390.0",
"next": "14.2.3",
"next-themes": "^0.3.0",
Expand Down
16 changes: 4 additions & 12 deletions examples/next-js/src/app/api/actions/memo/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import { MemoQuerySchema } from "./schema";
// Create memo program instruction
import { createMemoInstruction } from "./instruction";

import { createQueryParser } from "../../utils/validation";

const headers = createActionHeaders();

const parseQueryParams = createQueryParser(MemoQuerySchema);

async function handleGet(req: Request): Promise<ActionGetResponse> {
const requestUrl = new URL(req.url);
const baseHref = new URL("/api/actions/memo", requestUrl.origin).toString();
Expand Down Expand Up @@ -85,18 +89,6 @@ async function handlePost(req: Request): Promise<ActionPostResponse> {
});
}

function parseQueryParams(requestUrl: URL) {
const result = MemoQuerySchema.safeParse(
Object.fromEntries(requestUrl.searchParams)
);

if (!result.success) {
throw result.error;
}

return result.data;
}

export const { GET, POST, OPTIONS } = createActionRoutes(
{
GET: handleGet,
Expand Down
11 changes: 9 additions & 2 deletions examples/next-js/src/app/api/actions/memo/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { z } from "zod";
import { publicKeySchema } from "../../utils/validation";

export const MemoQuerySchema = z.object({
message: z.string().min(1, "Message is required").max(256, "Message must be less than 256 characters"),
message: z.string().min(1, "Message is required"),
});

export type MemoQuery = z.infer<typeof MemoQuerySchema>;
export type MemoQuery = z.infer<typeof MemoQuerySchema>;

export const MemoBodySchema = z.object({
account: publicKeySchema,
});

export type MemoBody = z.infer<typeof MemoBodySchema>;
125 changes: 125 additions & 0 deletions examples/next-js/src/app/api/actions/paymaster/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
ActionGetResponse,
ActionPostRequest,
ActionPostResponse,
createActionHeaders,
createPostResponse,
} from "@solana/actions";
import { createActionRoutes } from "../../utils/action-handler";
import { loadPaymasterKeypair } from "../../utils/keypair";
import { createQueryParser } from "../../utils/validation";
import { GenericTransactionExtensionSchema } from "./schema";
import {
AddressLookupTableAccount,
Message,
MessageV0,
Transaction,
TransactionMessage,
VersionedMessage,
VersionedTransaction,
} from "@solana/web3.js";
import { getConnection } from "../../utils/connection";

const headers = createActionHeaders();

async function handleGet(req: Request): Promise<ActionGetResponse> {
const requestUrl = new URL(req.url);
const paymaster = loadPaymasterKeypair();

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

return {
type: "action",
title: "Actions Example - Paymaster",
icon: new URL("/solana_devs.jpg", requestUrl.origin).toString(),
description: `Have ${paymaster.publicKey.toBase58()} cover the tx fee`,
label: "Paymaster",
links: {
actions: [
{
label: "Cover Fee",
href: `${baseHref}?blink={blink}`,
parameters: [
{
type: "url",
name: "blink",
label: "Blink URL",
required: true,
},
],
},
],
},
};
}

const parseQueryParams = createQueryParser(GenericTransactionExtensionSchema);

// http://localhost:3000/api/actions/transfer-sol?amount=0.01&to=67ZiM1TRqPFR5s2Jz1z4d6noHHBRRzt1Te6xbWmPgYF7
// http://localhost:3000/api/actions/transfer-sol?amount=0.001&to=6Le7uLy8Y2JvCq5x5huvF3pSQBvP1Y6W325wNpFz4s4u
// http://localhost:3000/api/actions/transfer-spl?&amount=1&to=67ZiM1TRqPFR5s2Jz1z4d6noHHBRRzt1Te6xbWmPgYF7&mint=CzLSujWBLFsSjncfkh59rUFqvafWcY5tzedWJSuypump

async function handlePost(req: Request): Promise<ActionPostResponse> {
const requestUrl = new URL(req.url);
const paymaster = loadPaymasterKeypair();
const { blink } = parseQueryParams(requestUrl);

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

const txResponse = await fetch(blink, {
method: "POST",
body: JSON.stringify({ account: body.account }),
});

const txResponseBody: ActionPostResponse = await txResponse.json();
const tx = VersionedTransaction.deserialize(
Buffer.from(txResponseBody.transaction, "base64"),
);

// hydrate the message's instructions using the static account keys and lookup tables
const connection = getConnection();
const LUTs = (
await Promise.all(
tx.message.addressTableLookups.map((acc) =>
connection.getAddressLookupTable(acc.accountKey),
),
)
)
.map((lut) => lut.value)
.filter((val) => val !== null) as AddressLookupTableAccount[];

// if we need to get all accounts
// const allAccs = tx.message.getAccountKeys({ addressLookupTableAccounts: LUTs })
// .keySegments().reduce((acc, cur) => acc.concat(cur), []);

const txMessage = TransactionMessage.decompile(tx.message, {
addressLookupTableAccounts: LUTs,
});

// Modify the message to use the paymaster as the payer
txMessage.payerKey = paymaster.publicKey;
txMessage.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;

const finalTx = new VersionedTransaction(txMessage.compileToV0Message());
finalTx.sign([paymaster]);

return createPostResponse({
fields: {
transaction: finalTx,
message: `Covered fee for "${Buffer.from(tx.serialize()).toString(
"base64",
)}"`,
},
});
}

export const { GET, POST, OPTIONS } = createActionRoutes(
{
GET: handleGet,
POST: handlePost,
},
headers,
);
8 changes: 8 additions & 0 deletions examples/next-js/src/app/api/actions/paymaster/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";
import { publicKeySchema, numberFromStringSchema } from "../../utils/validation";

export const GenericTransactionExtensionSchema = z.object({
blink: z.string(),
});

export type GenericTransactionExtension = z.infer<typeof GenericTransactionExtensionSchema>;
20 changes: 6 additions & 14 deletions examples/next-js/src/app/api/actions/transfer-sol/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import {
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { createQueryParser } from "../../utils/validation";
import { TransferSolQuerySchema } from "./schema";
import { createActionRoutes } from "../../utils/action-handler";
import { getConnection } from "../../utils/connection";

// create the standard headers for this route (including CORS)
const headers = createActionHeaders();

const parseQueryParams = createQueryParser(TransferSolQuerySchema);

async function handleGet(req: Request): Promise<ActionGetResponse> {
const requestUrl = new URL(req.url);
const baseHref = new URL(
Expand Down Expand Up @@ -79,7 +82,8 @@ async function handlePost(req: Request): Promise<ActionPostResponse> {
lamports: amount * LAMPORTS_PER_SOL,
});

const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();

const transaction = new Transaction({
feePayer: account,
Expand All @@ -95,22 +99,10 @@ async function handlePost(req: Request): Promise<ActionPostResponse> {
});
}

function parseQueryParams(requestUrl: URL) {
const result = TransferSolQuerySchema.safeParse(
Object.fromEntries(requestUrl.searchParams)
);

if (!result.success) {
throw result.error;
}

return result.data;
}

export const { GET, POST, OPTIONS } = createActionRoutes(
{
GET: handleGet,
POST: handlePost,
},
headers
headers,
);
12 changes: 5 additions & 7 deletions examples/next-js/src/app/api/actions/transfer-sol/schema.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { z } from "zod";
import { PublicKey } from "@solana/web3.js";
import { DEFAULT_SOL_AMOUNT } from "./const";
import { publicKeySchema, numberFromStringSchema } from "../../utils/validation";
import { numberFromStringSchema, publicKeySchema } from "../../utils/validation";

// Define the input type (what comes from URL params)
export const TransferSolQuerySchema = z.object({
to: publicKeySchema,
amount: numberFromStringSchema({
min: 0,
description: "SOL amount"
})
.optional()
.default(DEFAULT_SOL_AMOUNT.toString()),
amount: numberFromStringSchema({ min: 0 }),
});

// Type representing the parsed and transformed data
export type TransferSolQuery = z.infer<typeof TransferSolQuerySchema>;
99 changes: 99 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,99 @@
import { ActionGetResponse, ActionPostRequest, ActionPostResponse, createActionHeaders, createPostResponse } from "@solana/actions";
import { PublicKey, Transaction } from "@solana/web3.js";
import { getConnection } from "../../utils/connection";
import { createActionRoutes } from "../../utils/action-handler";
import { TransferSplQuerySchema } from "./schema";
import { createQueryParser } from "../../utils/validation";
import { createTransferInstruction, getAssociatedTokenAddressSync, getMint } from "@solana/spl-token";

// create the standard headers for this route (including CORS)
const headers = createActionHeaders();

const parseQueryParams = createQueryParser(TransferSplQuerySchema);

async function handleGet(req: Request): Promise<ActionGetResponse> {
const requestUrl = new URL(req.url);
const result = TransferSplQuerySchema.safeParse(
Object.fromEntries(requestUrl.searchParams)
);

const baseHref = new URL(
`/api/actions/transfer-spl?`,
requestUrl.origin,
).toString();

return {
type: "action",
title: "Actions Example - Transfer SPL Token",
icon: new URL("/solana_devs.jpg", requestUrl.origin).toString(),
description: "Transfer SOL to another Solana wallet",
label: "Transfer",
links: {
actions: [
{
label: "Send SPL Token",
href: `${baseHref}&amount={amount}&to={to}&mint={mint}`,
parameters: [
{
name: "amount",
label: "Enter the amount of SPL tokens to send",
required: true,
},
{
name: "to",
label: "Enter the address to send SPL tokens",
required: true,
},
{
name: "mint",
label: "Enter the SPL token mint address",
required: true,
},
],
},
],
},
};
}

async function handlePost(req: Request): Promise<ActionPostResponse> {
const requestUrl = new URL(req.url);
const { to: toPubkey, mint, amount } = parseQueryParams(requestUrl);

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

const connection = getConnection();
// get ATAs
const mintInfo = await getMint(connection, mint, "confirmed");
const sourceAta = getAssociatedTokenAddressSync(mint, account);
const destinationAta = getAssociatedTokenAddressSync(mint, toPubkey);
// get the true amount of tokens to transfer
const amountToTransfer = BigInt(amount) * BigInt(10 ** mintInfo.decimals);
const transferSplInstruction = createTransferInstruction(sourceAta, destinationAta, account, amountToTransfer);

const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();

const transaction = new Transaction({
feePayer: account,
blockhash,
lastValidBlockHeight,
}).add(transferSplInstruction);

return createPostResponse({
fields: {
transaction,
message: `Send ${amount} ${mint.toBase58()} to ${toPubkey.toBase58()}`,
},
});
}


export const { GET, POST, OPTIONS } = createActionRoutes(
{
GET: handleGet,
POST: handlePost,
},
headers,
);
13 changes: 13 additions & 0 deletions examples/next-js/src/app/api/actions/transfer-spl/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";
import { publicKeySchema, numberFromStringSchema } from "../../utils/validation";

export const TransferSplQuerySchema = z.object({
to: publicKeySchema,
mint: publicKeySchema,
amount: numberFromStringSchema({
min: 1,
description: "SPL token amount"
})
});

export type TransferSolQuery = z.infer<typeof TransferSplQuerySchema>;
Loading

0 comments on commit 6de8411

Please sign in to comment.