From de9648e8e51ac53b05834cbd58a4bf97cb14a099 Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Mon, 4 Nov 2024 10:35:03 -0500 Subject: [PATCH] save untested payme paymaster --- .../api/actions/lighthouse/lamports/route.ts | 16 +-- .../src/app/api/actions/pay-me/route.ts | 101 ++++++++++++++++++ .../src/app/api/actions/pay-me/schema.ts | 18 ++++ .../src/app/api/actions/paymaster/route.ts | 22 ++++ .../src/app/api/actions/transfer-sol/route.ts | 55 +++++++++- .../app/api/actions/transfer-sol/schema.ts | 14 ++- .../src/app/api/utils/action-handler.ts | 53 +++++++-- .../next-js/src/app/api/utils/validation.ts | 10 ++ 8 files changed, 263 insertions(+), 26 deletions(-) create mode 100644 examples/next-js/src/app/api/actions/pay-me/route.ts create mode 100644 examples/next-js/src/app/api/actions/pay-me/schema.ts diff --git a/examples/next-js/src/app/api/actions/lighthouse/lamports/route.ts b/examples/next-js/src/app/api/actions/lighthouse/lamports/route.ts index c0b8c40..fd605c4 100644 --- a/examples/next-js/src/app/api/actions/lighthouse/lamports/route.ts +++ b/examples/next-js/src/app/api/actions/lighthouse/lamports/route.ts @@ -63,6 +63,7 @@ async function handleGet(req: Request): Promise { parameters: [ { type: "url", + // type: "blink", name: "blink", label: "Blink", required: true, @@ -144,13 +145,14 @@ async function handlePost(req: Request): Promise { }, }).getInstructions()[0], ), - toWeb3JsInstruction( - memoryClose(umi, { - memory: publicKey(memory), - memoryId: 0, - memoryBump, - }).getInstructions()[0], - ), + // Close instruction when I want the Sol back haha + // toWeb3JsInstruction( + // memoryClose(umi, { + // memory: publicKey(memory), + // memoryId: 0, + // memoryBump, + // }).getInstructions()[0], + // ), ]; txMessage.instructions = [ diff --git a/examples/next-js/src/app/api/actions/pay-me/route.ts b/examples/next-js/src/app/api/actions/pay-me/route.ts new file mode 100644 index 0000000..3a28482 --- /dev/null +++ b/examples/next-js/src/app/api/actions/pay-me/route.ts @@ -0,0 +1,101 @@ +import { + ActionGetResponse, + ActionPostRequest, + ActionPostResponse, + createActionHeaders, + createPostResponse, +} from "@solana/actions"; +import { createActionRoutes } from "../../utils/action-handler"; +import { PayMeQuerySchema } from "./schema"; +import { createQueryParser } from "../../utils/validation"; +import { VersionedTransaction } from "@solana/web3.js"; +import { fetchBlink } from "../../utils/fetch"; +import { + getConnection, + hydrateTransactionMessage, +} from "../../utils/connection"; +import { createTransferInstruction } from "@solana/spl-token"; + +async function handleGet(req: Request): Promise { + const requestUrl = new URL(req.url); + const baseHref = new URL("/api/actions/memo", requestUrl.origin).toString(); + + return { + type: "action", + title: "Actions Example - Pay Me", + icon: new URL("/solana_devs.jpg", requestUrl.origin).toString(), + description: "Append a payment instruction to a recipient", + label: "Write", + links: { + actions: [ + { + label: "Pay Me", + href: `${baseHref}?mint={mint}&recipient={recipient}&amountUsd={amountUsd}&blink={blink}`, + parameters: [ + { + type: "text", + name: "mint", + label: "Mint", + required: false, + }, + { + type: "text", + name: "recipient", + label: "Recipient", + required: true, + }, + { + type: "number", + name: "amountUsd", + label: "Amount USD", + required: true, + }, + ], + }, + ], + }, + }; +} + +const parseQueryParams = createQueryParser(PayMeQuerySchema); + +async function handlePost(req: Request): Promise { + const requestUrl = new URL(req.url); + const { blink, amountUsd, mint, payer, recipient } = + parseQueryParams(requestUrl); + + const body: ActionPostRequest = await req.json(); + + // get a quote for the amount in USD + const jupPrice: { data: { [key: string]: { price: number } } } = await ( + await fetch(`https://api.jup.ag/price/v2?ids=${mint.toBase58()}`) + ).json(); + + const amountInMint = Math.ceil( + jupPrice.data[mint.toBase58()].price * amountUsd, + ); + + const txResponseBody = await fetchBlink(blink, body.account); + const connection = getConnection(); + const tx = VersionedTransaction.deserialize( + Buffer.from(txResponseBody.transaction, "base64"), + ); + const txMessage = await hydrateTransactionMessage(tx, connection); + + const finalTx = new VersionedTransaction(txMessage.compileToV0Message()); + + return createPostResponse({ + fields: { + transaction: finalTx, + message: `Payed ${amountUsd} in ${mint.toBase58()} to ${recipient.toBase58()}"`, + }, + }); +} + +export const { GET, POST, OPTIONS } = createActionRoutes( + { + GET: handleGet, + POST: handlePost, + }, + createActionHeaders(), +); diff --git a/examples/next-js/src/app/api/actions/pay-me/schema.ts b/examples/next-js/src/app/api/actions/pay-me/schema.ts new file mode 100644 index 0000000..31695a2 --- /dev/null +++ b/examples/next-js/src/app/api/actions/pay-me/schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { + publicKeySchema, + numberFromStringSchema, +} from "../../utils/validation"; + +export const PayMeQuerySchema = z.object({ + blink: z.string(), + amountUsd: numberFromStringSchema(), + mint: publicKeySchema.default( + // USDC + "EPjFWdd5AufqSSqeM2gEi2U9jFw27yN25g8yLWkxfV8rauYU", + ), + payer: publicKeySchema, + recipient: publicKeySchema, +}); + +export type PayMeQuery = z.infer; diff --git a/examples/next-js/src/app/api/actions/paymaster/route.ts b/examples/next-js/src/app/api/actions/paymaster/route.ts index 916e7e9..cd61e91 100644 --- a/examples/next-js/src/app/api/actions/paymaster/route.ts +++ b/examples/next-js/src/app/api/actions/paymaster/route.ts @@ -62,6 +62,28 @@ const parseQueryParams = createQueryParser(GenericTransactionExtensionSchema); // http%3A%2F%2Flocalhost%3A3000%2Fapi%2Factions%2Ftransfer-sol%3Famount%3D0.01%26to%3D67ZiM1TRqPFR5s2Jz1z4d6noHHBRRzt1Te6xbWmPgYF7 // localhost:3000/api/actions/transfer-sol?amount=0.01&to=67ZiM1TRqPFR5s2Jz1z4d6noHHBRRzt1Te6xbWmPgYF7 +// blink +// wrap lighthouse +// i as user want to wrap myself in a blink +// wrap paymaster + +// send USDC to ilan.sol +// [getOrCreateAta, transfer] + +// lighthouse 2 ixs +// [pre, [...ix], postAssert] + +// user ? how do intelligent config my txs +// human readable description & parameters + +// Distribution +// Consumers | Developers +// Developers - we now "vibrant" API ecosystem +// anyone can write 3p API +// "AI Agents are Wallets" +// wallets agnostic of stack - go, ios / objective C, bahjj +// Consumers - + async function handlePost(req: Request): Promise { console.log("Request", req); const requestUrl = new URL(req.url); diff --git a/examples/next-js/src/app/api/actions/transfer-sol/route.ts b/examples/next-js/src/app/api/actions/transfer-sol/route.ts index f6a0ce8..255f500 100644 --- a/examples/next-js/src/app/api/actions/transfer-sol/route.ts +++ b/examples/next-js/src/app/api/actions/transfer-sol/route.ts @@ -14,11 +14,21 @@ import { PublicKey, SystemProgram, Transaction, + TransactionInstruction, + VersionedTransaction, } from "@solana/web3.js"; -import { createQueryParser } from "../../utils/validation"; +import { createQueryParser, InsertionType } from "../../utils/validation"; import { TransferSolQuerySchema } from "./schema"; -import { createActionRoutes } from "../../utils/action-handler"; -import { getConnection } from "../../utils/connection"; +import { + createActionRoutes, + insertInstructionsInBlink, +} from "../../utils/action-handler"; +import { + getConnection, + hydrateTransactionMessage, +} from "../../utils/connection"; +import { Url } from "next/dist/shared/lib/router/router"; +import { fetchBlink } from "../../utils/fetch"; // create the standard headers for this route (including CORS) const headers = createActionHeaders(); @@ -42,7 +52,7 @@ async function handleGet(req: Request): Promise { actions: [ { label: "Send SOL", - href: `${baseHref}&amount={amount}&to={to}`, + href: `${baseHref}&amount={amount}&to={to}&blink={blink}&insertion={insertion}`, parameters: [ { name: "amount", @@ -54,6 +64,22 @@ async function handleGet(req: Request): Promise { label: "Enter the address to send SOL", required: true, }, + { + type: "url", + name: "blink", + label: "", + required: false, + }, + { + type: "radio", + name: "insertion", + label: "", + required: false, + options: [ + { label: "Prepend", value: "prepend" }, + { label: "Append", value: "append" }, + ], + }, ], }, ], @@ -63,7 +89,12 @@ async function handleGet(req: Request): Promise { async function handlePost(req: Request): Promise { const requestUrl = new URL(req.url); - const { to: toPubkey, amount } = parseQueryParams(requestUrl); + const { + to: toPubkey, + amount, + blink, + insertion, + } = parseQueryParams(requestUrl); const body: ActionPostRequest = await req.json(); const account = new PublicKey(body.account); @@ -82,6 +113,20 @@ async function handlePost(req: Request): Promise { lamports: amount * LAMPORTS_PER_SOL, }); + if (blink) { + return createPostResponse({ + fields: { + transaction: await insertInstructionsInBlink( + blink, + insertion, + body.account, + [transferSolInstruction], + ), + message: `Send ${amount} SOL to ${toPubkey.toBase58()}`, + }, + }); + } + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); diff --git a/examples/next-js/src/app/api/actions/transfer-sol/schema.ts b/examples/next-js/src/app/api/actions/transfer-sol/schema.ts index bdf1e79..47ab982 100644 --- a/examples/next-js/src/app/api/actions/transfer-sol/schema.ts +++ b/examples/next-js/src/app/api/actions/transfer-sol/schema.ts @@ -1,13 +1,19 @@ import { z } from "zod"; -import { PublicKey } from "@solana/web3.js"; -import { DEFAULT_SOL_AMOUNT } from "./const"; -import { numberFromStringSchema, publicKeySchema } from "../../utils/validation"; +import { + blinkSchema, + insertionTypeSchema, + 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 }), + blink: blinkSchema, + // insertion one of "prepend", "append" + insertion: insertionTypeSchema, }); // Type representing the parsed and transformed data -export type TransferSolQuery = z.infer; \ No newline at end of file +export type TransferSolQuery = z.infer; diff --git a/examples/next-js/src/app/api/utils/action-handler.ts b/examples/next-js/src/app/api/utils/action-handler.ts index 9ddad72..4996d75 100644 --- a/examples/next-js/src/app/api/utils/action-handler.ts +++ b/examples/next-js/src/app/api/utils/action-handler.ts @@ -1,6 +1,10 @@ import { ActionError } from "@solana/actions"; import { fromZodError } from "zod-validation-error"; import { ZodError } from "zod"; +import { TransactionInstruction, VersionedTransaction } from "@solana/web3.js"; +import { getConnection, hydrateTransactionMessage } from "./connection"; +import { InsertionType } from "./validation"; +import { fetchBlink } from "./fetch"; type ActionHandler = (req: Request) => Promise; type ActionHandlerFn = (req: Request) => Promise; @@ -14,24 +18,29 @@ export type ActionRouteHandlers = { * Creates a handler for OPTIONS requests, which is required for CORS preflight requests. * Returns an empty response with the appropriate CORS headers. */ -export function createOptionsHandler(headers: HeadersInit): ActionHandler { +export function createOptionsHandler( + headers: HeadersInit, +): ActionHandler { return async () => Response.json(null, { headers }); } /** * Creates a set of route handlers for a Solana Action API endpoint. - * + * * Features: * - Automatically adds OPTIONS handler for CORS support * - Wraps handlers with error handling * - Ensures consistent response format * - Handles common error types (Zod validation, strings, Error objects) - * + * * @param handlers - Object containing GET and/or POST handler functions * @param headers - Headers to include in all responses (including CORS headers) * @returns Object containing wrapped route handlers */ -export function createActionRoutes(handlers: ActionRouteHandlers, headers: HeadersInit) { +export function createActionRoutes( + handlers: ActionRouteHandlers, + headers: HeadersInit, +) { const routes: Record> = { // Always include OPTIONS handler for CORS preflight requests OPTIONS: createOptionsHandler(headers), @@ -50,25 +59,28 @@ export function createActionRoutes(handlers: ActionRouteHandlers, headers: Heade /** * Wraps a handler function with error handling and response formatting. - * + * * Error handling: * - ZodError: Converts to user-friendly validation error message * - String: Uses directly as error message * - Error: Uses error.message * - Unknown: Falls back to generic error message - * + * * All responses include the provided headers and are formatted as JSON. */ -function createActionHandler(handler: ActionHandlerFn, headers: HeadersInit): ActionHandler { +function createActionHandler( + handler: ActionHandlerFn, + headers: HeadersInit, +): ActionHandler { return async (req: Request) => { try { const result = await handler(req); return Response.json(result, { headers }); } catch (err) { console.error(err); - + let actionError: ActionError = { message: "An unknown error occurred" }; - + if (err instanceof ZodError) { actionError.message = fromZodError(err).message; } else if (typeof err === "string") { @@ -83,4 +95,25 @@ function createActionHandler(handler: ActionHandlerFn, headers: HeadersInit }); } }; -} \ No newline at end of file +} + +export async function insertInstructionsInBlink( + blink: string, + insertion: InsertionType, + account: string, + instructions: TransactionInstruction[], +) { + const txResponseBody = await fetchBlink(blink, account); + + const connection = getConnection(); + const tx = VersionedTransaction.deserialize( + Buffer.from(txResponseBody.transaction, "base64"), + ); + const txMessage = await hydrateTransactionMessage(tx, connection); + if (insertion === "prepend") { + txMessage.instructions.push(...instructions); + } else { + txMessage.instructions.unshift(...instructions); + } + return new VersionedTransaction(txMessage.compileToV0Message()); +} diff --git a/examples/next-js/src/app/api/utils/validation.ts b/examples/next-js/src/app/api/utils/validation.ts index 82c3c2a..871ca5b 100644 --- a/examples/next-js/src/app/api/utils/validation.ts +++ b/examples/next-js/src/app/api/utils/validation.ts @@ -137,3 +137,13 @@ export function createQueryParser< return result.data; }; } + +// insertion one of "prepend", "append" +export const insertionTypeSchema = z + .enum(["prepend", "append"]) + .optional() + .default("append"); + +export type InsertionType = z.infer; + +export const blinkSchema = z.string().url().optional();