From a95c2a9c2faf31fa63266d0d8d35da28aea77b2a Mon Sep 17 00:00:00 2001 From: Alec Ananian <1013230+alecananian@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:34:31 -0700 Subject: [PATCH] add endpoint and example for sending native tokens (#48) * add endpoint and example for sending native tokens * approve native send recipient address --- apps/api/src/routes/transactions.ts | 67 ++++++++++++++++++++++++ apps/api/src/schema.ts | 25 +++++++++ examples/connect/package.json | 3 +- examples/connect/src/App.tsx | 58 +++++++++++++------- examples/connect/src/main.tsx | 8 ++- package-lock.json | 21 ++++---- packages/core/src/api.ts | 39 +++++++++++--- packages/core/src/utils/currency.ts | 1 + packages/core/src/utils/session.ts | 21 ++++---- packages/react/src/contexts/treasure.tsx | 2 +- 10 files changed, 196 insertions(+), 49 deletions(-) diff --git a/apps/api/src/routes/transactions.ts b/apps/api/src/routes/transactions.ts index 69fa8402..a0af3410 100644 --- a/apps/api/src/routes/transactions.ts +++ b/apps/api/src/routes/transactions.ts @@ -4,11 +4,15 @@ import "../middleware/auth"; import "../middleware/chain"; import "../middleware/swagger"; import { + type CreateSendNativeTransactionBody, + type CreateSendNativeTransactionReply, type CreateTransactionBody, type CreateTransactionReply, type ErrorReply, type ReadTransactionParams, type ReadTransactionReply, + createSendNativeTransactionBodySchema, + createSendNativeTransactionReplySchema, createTransactionBodySchema, createTransactionReplySchema, readTransactionReplySchema, @@ -91,6 +95,69 @@ export const transactionsRoutes = }, ); + app.post<{ + Body: CreateSendNativeTransactionBody; + Reply: CreateSendNativeTransactionReply | ErrorReply; + }>( + "/transactions/send-native", + { + schema: { + summary: "Send native tokens", + description: + "Send the chain's native (gas) token to the provided recipient", + security: [{ authToken: [] }], + body: createSendNativeTransactionBodySchema, + response: { + 200: createSendNativeTransactionReplySchema, + }, + }, + }, + async (req, reply) => { + const { + chainId, + userAddress, + authError, + body: { to, amount, backendWallet: overrideBackendWallet }, + } = req; + if (!userAddress) { + throw new TdkError({ + code: "TDK_UNAUTHORIZED", + message: "Unauthorized", + data: { authError }, + }); + } + + const backendWallet = + overrideBackendWallet ?? env.DEFAULT_BACKEND_WALLET; + try { + const { result } = await engine.backendWallet.sendTransaction( + chainId.toString(), + backendWallet, + { + toAddress: to, + value: amount, + data: "0x", + }, + false, + userAddress, + ); + reply.send(result); + } catch (err) { + throw new TdkError({ + code: "TDK_CREATE_TRANSACTION", + message: `Error creating native send transaction: ${parseEngineErrorMessage(err) ?? "Unknown error"}`, + data: { + chainId, + backendWallet, + userAddress, + to, + amount, + }, + }); + } + }, + ); + app.get<{ Params: ReadTransactionParams; Reply: ReadTransactionReply | ErrorReply; diff --git a/apps/api/src/schema.ts b/apps/api/src/schema.ts index f7e50e24..829acf63 100644 --- a/apps/api/src/schema.ts +++ b/apps/api/src/schema.ts @@ -423,6 +423,25 @@ export const createTransactionReplySchema = Type.Object({ }), }); +export const createSendNativeTransactionBodySchema = Type.Object({ + to: Type.String({ + description: "The recipient address", + examples: [EXAMPLE_WALLET_ADDRESS], + }), + amount: Type.String({ + description: "The amount to send, in wei", + examples: ["1000000000000000000"], + }), + backendWallet: Type.Optional(Type.String()), +}); + +export const createSendNativeTransactionReplySchema = Type.Object({ + queueId: Type.String({ + description: "The transaction queue ID", + examples: [EXAMPLE_QUEUE_ID], + }), +}); + const readTransactionParamsSchema = Type.Object({ queueId: Type.String(), }); @@ -437,6 +456,12 @@ export type CreateTransactionBody = Static; export type CreateTransactionReply = Static< typeof createTransactionReplySchema >; +export type CreateSendNativeTransactionBody = Static< + typeof createSendNativeTransactionBodySchema +>; +export type CreateSendNativeTransactionReply = Static< + typeof createSendNativeTransactionReplySchema +>; export type ReadTransactionParams = Static; export type ReadTransactionReply = Static; diff --git a/examples/connect/package.json b/examples/connect/package.json index 20e8ea76..bcfbb3d4 100644 --- a/examples/connect/package.json +++ b/examples/connect/package.json @@ -9,7 +9,8 @@ "dependencies": { "@treasure-dev/tdk-react": "*", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "viem": "^2.15.1" }, "devDependencies": { "@treasure-project/tailwind-config": "^2.1.0", diff --git a/examples/connect/src/App.tsx b/examples/connect/src/App.tsx index 15eb8a5f..23abf4e9 100644 --- a/examples/connect/src/App.tsx +++ b/examples/connect/src/App.tsx @@ -2,15 +2,17 @@ import { type AddressString, Button, ConnectButton, + formatAmount, getContractAddress, useTreasure, } from "@treasure-dev/tdk-react"; +import { formatEther, parseEther } from "viem"; export const App = () => { const { tdk, chainId, user } = useTreasure(); const magicAddress = getContractAddress(chainId, "MAGIC"); - const handleMintMagic = async () => { + const handleMintMagic = async (amount: number) => { if (!user?.smartAccountAddress) { return; } @@ -42,7 +44,7 @@ export const App = () => { functionName: "mint", args: [ user.smartAccountAddress as AddressString, - 1000000000000000000000n, + parseEther(amount.toString()), ], }, { includeAbi: true }, @@ -52,20 +54,20 @@ export const App = () => { } }; - // const handleSendEth = async () => { - // if (!user?.smartAccountAddress) { - // return; - // } + const handleSendEth = async (amount: number) => { + if (!user?.smartAccountAddress) { + return; + } - // try { - // await tdk.transaction.sendNative({ - // to: "0x55d0cf68a1afe0932aff6f36c87efa703508191c", - // amount: 100000000000000n, - // }); - // } catch (err) { - // console.error("Error sending ETH:", err); - // } - // }; + try { + await tdk.transaction.sendNative({ + to: "0xE647b2c46365741e85268ceD243113d08F7E00B8", + amount: parseEther(amount.toString()), + }); + } catch (err) { + console.error("Error sending ETH:", err); + } + }; return (
@@ -87,7 +89,13 @@ export const App = () => { (a, b) => Number(b.endTimestamp) - Number(a.endTimestamp), ) .map( - ({ signer, isAdmin, endTimestamp, approvedTargets }) => ( + ({ + signer, + isAdmin, + endTimestamp, + approvedTargets, + nativeTokenLimitPerTransaction, + }) => (
  • {signer}{" "} @@ -117,6 +125,16 @@ export const App = () => { ))}

  • +

    + + Native token limit per transaction: + {" "} + {formatAmount( + formatEther( + BigInt(nativeTokenLimitPerTransaction), + ), + )} +

    ) : null} @@ -130,8 +148,12 @@ export const App = () => {

    Test Transactions

    - - {/* */} + +
    diff --git a/examples/connect/src/main.tsx b/examples/connect/src/main.tsx index 61be4001..51dde96f 100644 --- a/examples/connect/src/main.tsx +++ b/examples/connect/src/main.tsx @@ -2,6 +2,7 @@ import { TreasureProvider } from "@treasure-dev/tdk-react"; import "@treasure-dev/tdk-react/dist/index.css"; import "@treasure-project/tailwind-config/fonts.css"; import ReactDOM from "react-dom/client"; +import { parseEther } from "viem"; import { App } from "./App.tsx"; import "./index.css"; @@ -15,8 +16,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render( onAuthenticated={async (_, startUserSession) => { await startUserSession({ chainId: 421614, - approvedTargets: ["0x55d0cf68a1afe0932aff6f36c87efa703508191c"], - nativeTokenLimitPerTransaction: 1, + approvedTargets: [ + "0x55d0cf68a1afe0932aff6f36c87efa703508191c", + "0xE647b2c46365741e85268ceD243113d08F7E00B8", + ], + nativeTokenLimitPerTransaction: parseEther("1"), }); }} > diff --git a/package-lock.json b/package-lock.json index 4fa2e776..1b5b0deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,8 @@ "dependencies": { "@treasure-dev/tdk-react": "*", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "viem": "^2.15.1" }, "devDependencies": { "@treasure-project/tailwind-config": "^2.1.0", @@ -31606,9 +31607,9 @@ } }, "node_modules/viem": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.14.0.tgz", - "integrity": "sha512-+XnuRNONDRyMNEM+1n6Ak41Py0KBFOmirQ67wPv//ytCmNIJhmy8vqhdFREr3m51GAGgDkllh4JoAdCUUaZJLw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.15.1.tgz", + "integrity": "sha512-Vrveen3vDOJyPf8Q8TDyWePG2pTdK6IpSi4P6qlvAP+rXkAeqRvwYBy9AmGm+BeYpCETAyTT0SrCP6458XSt+w==", "funding": [ { "type": "github", @@ -31623,7 +31624,7 @@ "@scure/bip39": "1.2.1", "abitype": "1.0.0", "isows": "1.0.4", - "ws": "8.13.0" + "ws": "8.17.1" }, "peerDependencies": { "typescript": ">=5.0.4" @@ -31682,9 +31683,9 @@ } }, "node_modules/viem/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -32819,7 +32820,7 @@ }, "packages/core": { "name": "@treasure-dev/tdk-core", - "version": "0.2.7", + "version": "0.2.8", "dependencies": { "@wagmi/core": "^2.9.1", "abitype": "^1.0.2", @@ -32854,7 +32855,7 @@ }, "packages/react": { "name": "@treasure-dev/tdk-react", - "version": "0.2.7", + "version": "0.2.8", "dependencies": { "@radix-ui/react-dialog": "^1.0.5", "@tanstack/react-query": "^5.18.1", diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index d9ea239c..73dd0a2e 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -6,6 +6,8 @@ import type { } from "abitype"; import type { + CreateSendNativeTransactionBody, + CreateSendNativeTransactionReply, CreateTransactionBody, LoginBody, LoginReply, @@ -176,21 +178,44 @@ export class TDKAPI { args: params.args as any, backendWallet: params.backendWallet ?? this.backendWallet, }); - if (!waitForCompletion) { - return result; - } + return waitForCompletion ? this.transaction.wait(result.queueId) : result; + }, + sendNative: async ( + params: { + to: string; + amount: bigint; + backendWallet?: string; + }, + { + waitForCompletion = true, + }: { includeAbi?: boolean; waitForCompletion?: boolean } = {}, + ) => { + const result = await this.post< + CreateSendNativeTransactionBody, + CreateSendNativeTransactionReply + >("/transactions/send-native", { + ...params, + amount: params.amount.toString(), + backendWallet: params.backendWallet ?? this.backendWallet, + }); + + return waitForCompletion ? this.transaction.wait(result.queueId) : result; + }, + get: (queueId: string) => + this.get(`/transactions/${queueId}`), + wait: async (queueId: string, maxRetries = 15, retryMs = 2_500) => { let retries = 0; let transaction: ReadTransactionReply; do { if (retries > 0) { - await new Promise((r) => setTimeout(r, 2_500)); + await new Promise((r) => setTimeout(r, retryMs)); } - transaction = await this.transaction.get(result.queueId); + transaction = await this.transaction.get(queueId); retries += 1; } while ( - retries < 15 && + retries < maxRetries && transaction.status !== "errored" && transaction.status !== "cancelled" && transaction.status !== "mined" @@ -210,8 +235,6 @@ export class TDKAPI { return transaction; }, - get: (queueId: string) => - this.get(`/transactions/${queueId}`), }; harvester = { diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index df595cf7..8d301bd1 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -33,6 +33,7 @@ export const formatAmount = (value: string | number, toLocale = true) => { return rounded.toString(); }; + export const getTokenAddress = (chainId: number, token: Token) => { const contractAddresses = getContractAddresses(chainId); switch (token) { diff --git a/packages/core/src/utils/session.ts b/packages/core/src/utils/session.ts index 75719e82..7ee7ec96 100644 --- a/packages/core/src/utils/session.ts +++ b/packages/core/src/utils/session.ts @@ -13,25 +13,26 @@ import { getDateYearsFromNow, } from "./date"; +import { formatEther } from "viem"; import type { Session } from "../types"; export const isSessionRequired = ({ approvedTargets = [], - nativeTokenLimitPerTransaction = 0, + nativeTokenLimitPerTransaction = 0n, }: { approvedTargets?: string[]; - nativeTokenLimitPerTransaction?: number; + nativeTokenLimitPerTransaction?: bigint; }) => approvedTargets.length > 0 || nativeTokenLimitPerTransaction > 0; export const validateSession = async ({ backendWallet, approvedTargets: rawApprovedTargets, - nativeTokenLimitPerTransaction = 0, + nativeTokenLimitPerTransaction = 0n, sessions, }: { backendWallet: string; approvedTargets: string[]; - nativeTokenLimitPerTransaction?: number; + nativeTokenLimitPerTransaction?: bigint; sessions: Session[]; }) => { const approvedTargets = rawApprovedTargets.map((target) => @@ -68,7 +69,7 @@ export const validateSession = async ({ ) && // Native token limit per transaction is approved (!nativeTokenLimitPerTransaction || - Number(session.nativeTokenLimitPerTransaction) >= + BigInt(session.nativeTokenLimitPerTransaction) >= nativeTokenLimitPerTransaction) ); }); @@ -81,7 +82,7 @@ export const createSession = async ({ account, backendWallet, approvedTargets, - nativeTokenLimitPerTransaction, + nativeTokenLimitPerTransaction: nativeTokenLimitPerTransactionBI = 0n, }: { client: ThirdwebClient; chainId: number; @@ -89,13 +90,16 @@ export const createSession = async ({ account: Account; backendWallet: string; approvedTargets: string[]; - nativeTokenLimitPerTransaction?: number; + nativeTokenLimitPerTransaction?: bigint; }): Promise => { const contract = getContract({ client, chain: defineChain(chainId), address, }); + const nativeTokenLimitPerTransaction = formatEther( + nativeTokenLimitPerTransactionBI, + ); const startDate = getDateHoursFromNow(-1); const endDate = getDateDaysFromNow(1); const transaction = addSessionKey({ @@ -119,7 +123,6 @@ export const createSession = async ({ startTimestamp: Math.floor(startDate.getTime() / 1000).toString(), endTimestamp: Math.floor(endDate.getTime() / 1000).toString(), approvedTargets, - nativeTokenLimitPerTransaction: - nativeTokenLimitPerTransaction?.toString() ?? "0", + nativeTokenLimitPerTransaction, }; }; diff --git a/packages/react/src/contexts/treasure.tsx b/packages/react/src/contexts/treasure.tsx index 25380376..9a49c886 100644 --- a/packages/react/src/contexts/treasure.tsx +++ b/packages/react/src/contexts/treasure.tsx @@ -20,7 +20,7 @@ import { type SessionConfig = { chainId: number; approvedTargets: string[]; - nativeTokenLimitPerTransaction?: number; + nativeTokenLimitPerTransaction?: bigint; }; type StartUserSessionFn = (sessionConfig: SessionConfig) => void;