From 8d668d9b49bcacc242d6572e2a46a2135a3ee293 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 2 Dec 2024 10:46:43 +0100 Subject: [PATCH 1/3] ERC20 Utils --- package.json | 2 +- packages/agent-sdk/package.json | 7 +- packages/agent-sdk/src/evm/erc20.ts | 102 ++++++++++++++++++++++++++++ packages/agent-sdk/src/evm/index.ts | 13 ++-- packages/agent-sdk/src/evm/types.ts | 4 ++ packages/agent-sdk/src/index.ts | 1 + packages/agent-sdk/src/types.ts | 0 7 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 packages/agent-sdk/src/evm/erc20.ts create mode 100644 packages/agent-sdk/src/evm/types.ts create mode 100644 packages/agent-sdk/src/types.ts diff --git a/package.json b/package.json index a8ce659..67e60ae 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "build": "bun run --cwd packages/agent-sdk build", "lint": "prettier --check packages/**/* && eslint packages/", - "format": "prettier --write packages/**/* && eslint packages/ --fix" + "fmt": "prettier --write packages/**/* && eslint packages/ --fix" }, "devDependencies": { "@types/bun": "latest", diff --git a/packages/agent-sdk/package.json b/packages/agent-sdk/package.json index 2e803c0..87dace5 100644 --- a/packages/agent-sdk/package.json +++ b/packages/agent-sdk/package.json @@ -3,7 +3,12 @@ "version": "0.0.1", "author": "bh2smith", "description": "Agent SDK for Bitte Protocol", - "keywords": ["bitte", "protocol", "agent", "sdk"], + "keywords": [ + "bitte", + "protocol", + "agent", + "sdk" + ], "repository": { "type": "git", "url": "git+https://github.com/bitteprotocol/core.git" diff --git a/packages/agent-sdk/src/evm/erc20.ts b/packages/agent-sdk/src/evm/erc20.ts new file mode 100644 index 0000000..6527d17 --- /dev/null +++ b/packages/agent-sdk/src/evm/erc20.ts @@ -0,0 +1,102 @@ +import { erc20Abi } from "viem"; +import { encodeFunctionData, type Address } from "viem"; +import { signRequestFor } from ".."; +import { getClient, type SignRequestData } from "near-safe"; +import type { TokenInfo } from "./types"; + +const MAX_APPROVAL = BigInt( + "115792089237316195423570985008687907853269984665640564039457584007913129639935", +); + +export async function erc20Transfer(params: { + chainId: number; + token: Address; + to: Address; + amount: bigint; +}): Promise { + const { chainId, token, to, amount } = params; + return signRequestFor({ + chainId, + metaTransactions: [ + { + to: token, + value: "0x", + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [to, amount], + }), + }, + ], + }); +} + +export async function erc20Approve(params: { + chainId: number; + token: Address; + spender: Address; + // If not provided, the maximum amount will be approved. + amount?: bigint; +}): Promise { + const { chainId, token, spender, amount } = params; + return signRequestFor({ + chainId, + metaTransactions: [ + { + to: token, + value: "0x", + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, amount ?? MAX_APPROVAL], + }), + }, + ], + }); +} + +export async function getTokenInfo( + chainId: number, + address: Address, +): Promise { + const [decimals, symbol] = await Promise.all([ + getTokenDecimals(chainId, address), + getTokenSymbol(chainId, address), + ]); + return { + decimals, + symbol, + }; +} + +export async function getTokenDecimals( + chainId: number, + address: Address, +): Promise { + const client = getClient(chainId); + try { + return await client.readContract({ + address, + abi: erc20Abi, + functionName: "decimals", + }); + } catch (error: unknown) { + throw new Error(`Error fetching token decimals: ${error}`); + } +} + +export async function getTokenSymbol( + chainId: number, + address: Address, +): Promise { + const client = getClient(chainId); + try { + return await client.readContract({ + address, + abi: erc20Abi, + functionName: "symbol", + }); + } catch (error: unknown) { + throw new Error(`Error fetching token decimals: ${error}`); + } +} diff --git a/packages/agent-sdk/src/evm/index.ts b/packages/agent-sdk/src/evm/index.ts index c48f410..7a9fc35 100644 --- a/packages/agent-sdk/src/evm/index.ts +++ b/packages/agent-sdk/src/evm/index.ts @@ -2,6 +2,9 @@ import type { MetaTransaction, SignRequestData } from "near-safe"; import { NearSafe } from "near-safe"; import { getAddress, type Hex, zeroAddress, type Address } from "viem"; +export * from "./types"; +export * from "./erc20"; + export function signRequestFor({ from, chainId, @@ -49,11 +52,11 @@ export function createResponse( export async function validateRequest< TRequest extends BaseRequest, - TResponse extends BaseResponse + TResponse extends BaseResponse, >( req: TRequest, // TODO(bh2smith): Use Bitte Wallet's safeSaltNonce as Default. - safeSaltNonce: string + safeSaltNonce: string, ): Promise { const metadataHeader = req.headers.get("mb-metadata"); console.log("Request Metadata:", JSON.stringify(metadataHeader, null, 2)); @@ -69,8 +72,10 @@ export async function validateRequest< const derivedSafeAddress = await getAdapterAddress(accountId, safeSaltNonce); if (derivedSafeAddress !== getAddress(evmAddress)) { return createResponse( - { error: `Invalid safeAddress in metadata: ${derivedSafeAddress} !== ${evmAddress}` }, - { status: 401 } + { + error: `Invalid safeAddress in metadata: ${derivedSafeAddress} !== ${evmAddress}`, + }, + { status: 401 }, ) as TResponse; } console.log(`Valid request for ${accountId} <-> ${evmAddress}`); diff --git a/packages/agent-sdk/src/evm/types.ts b/packages/agent-sdk/src/evm/types.ts new file mode 100644 index 0000000..b470e4f --- /dev/null +++ b/packages/agent-sdk/src/evm/types.ts @@ -0,0 +1,4 @@ +export interface TokenInfo { + decimals: number; + symbol: string; +} diff --git a/packages/agent-sdk/src/index.ts b/packages/agent-sdk/src/index.ts index 9431d1c..73a1f49 100644 --- a/packages/agent-sdk/src/index.ts +++ b/packages/agent-sdk/src/index.ts @@ -1 +1,2 @@ export * from "./validate"; +export * from "./evm"; diff --git a/packages/agent-sdk/src/types.ts b/packages/agent-sdk/src/types.ts new file mode 100644 index 0000000..e69de29 From 3384d1379e43ee6f864b2c23024a612e0f8870e0 Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 2 Dec 2024 10:48:14 +0100 Subject: [PATCH 2/3] Delete unused file --- packages/agent-sdk/src/types.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/agent-sdk/src/types.ts diff --git a/packages/agent-sdk/src/types.ts b/packages/agent-sdk/src/types.ts deleted file mode 100644 index e69de29..0000000 From a1d23d0f968d68b7195e07bd9803a97263610eef Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Mon, 2 Dec 2024 11:05:03 +0100 Subject: [PATCH 3/3] Add documentation --- packages/agent-sdk/README.md | 32 +++++++++++- packages/agent-sdk/examples/erc20transfer.ts | 55 ++++++++++++++++++++ packages/agent-sdk/src/index.ts | 55 ++++++++++++++++++++ packages/agent-sdk/tsconfig.json | 2 +- 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 packages/agent-sdk/examples/erc20transfer.ts diff --git a/packages/agent-sdk/README.md b/packages/agent-sdk/README.md index ccbe2dc..e3be38b 100644 --- a/packages/agent-sdk/README.md +++ b/packages/agent-sdk/README.md @@ -1,4 +1,34 @@ -# agent-sdk +# Bitte Protocol agent-sdk + +This is a TypeScript SDK for building agents on the Bitte Protocol. + +## Usage + +Define and validate Agent input parameters by example: + +Suppose we want to build an agent that transfers an amount of a token to a recipient (i.e. an ERC20 transfer). + +```ts +// Declare Route Input +interface Input { + chainId: number; + amount: number; + token: string; + recipient: Address; +} + +const parsers: FieldParser = { + chainId: numberField, + // Note that this is a float (i.e. token units) + amount: floatField, + token: addressOrSymbolField, + recipient: addressField, +}; +``` + +Then the route could be implemented as in [examples/erc20transfer.ts](./examples/erc20transfer.ts) - which utilizes other utils from this package. + +## Development To install dependencies: diff --git a/packages/agent-sdk/examples/erc20transfer.ts b/packages/agent-sdk/examples/erc20transfer.ts new file mode 100644 index 0000000..92998dc --- /dev/null +++ b/packages/agent-sdk/examples/erc20transfer.ts @@ -0,0 +1,55 @@ +import { parseUnits, type Address } from "viem"; +import { + erc20Transfer, + getTokenDecimals, + addressField, + floatField, + numberField, + validateInput, + type FieldParser, +} from "../src"; + +interface Input { + chainId: number; + amount: number; + token: Address; + recipient: Address; +} + +const parsers: FieldParser = { + chainId: numberField, + // Note that this is a float (i.e. token units) + amount: floatField, + token: addressField, + recipient: addressField, +}; + +export async function GET(req: Request): Promise { + const url = new URL(req.url); + const search = url.searchParams; + console.log("erc20/", search); + try { + const { chainId, amount, token, recipient } = validateInput( + search, + parsers, + ); + const decimals = await getTokenDecimals(chainId, token); + return Response.json( + { + transaction: erc20Transfer({ + chainId, + token, + to: recipient, + amount: parseUnits(amount.toString(), decimals), + }), + }, + { status: 200 }, + ); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : `Unknown error occurred ${String(error)}`; + return Response.json({ ok: false, message }, { status: 400 }); + } +} diff --git a/packages/agent-sdk/src/index.ts b/packages/agent-sdk/src/index.ts index 73a1f49..24f18cc 100644 --- a/packages/agent-sdk/src/index.ts +++ b/packages/agent-sdk/src/index.ts @@ -1,2 +1,57 @@ +import { parseUnits, type Address } from "viem"; +import { erc20Transfer, getTokenDecimals } from "./evm"; +import { + addressField, + floatField, + numberField, + validateInput, + type FieldParser, +} from "./validate"; + export * from "./validate"; export * from "./evm"; + +interface Input { + chainId: number; + amount: number; + token: Address; + recipient: Address; +} + +const parsers: FieldParser = { + chainId: numberField, + // Note that this is a float (i.e. token units) + amount: floatField, + token: addressField, + recipient: addressField, +}; + +export async function GET(req: Request): Promise { + const url = new URL(req.url); + const search = url.searchParams; + console.log("erc20/", search); + try { + const { chainId, amount, token, recipient } = validateInput( + search, + parsers, + ); + const decimals = await getTokenDecimals(chainId, token); + return Response.json( + { + transaction: erc20Transfer({ + chainId, + token, + to: recipient, + amount: parseUnits(amount.toString(), decimals), + }), + }, + { status: 200 }, + ); + } catch (error: unknown) { + const message = + error instanceof Error + ? error.message + : `Unknown error occurred ${String(error)}`; + return Response.json({ ok: false, message }, { status: 400 }); + } +} diff --git a/packages/agent-sdk/tsconfig.json b/packages/agent-sdk/tsconfig.json index caa3b40..c08dfee 100644 --- a/packages/agent-sdk/tsconfig.json +++ b/packages/agent-sdk/tsconfig.json @@ -30,5 +30,5 @@ "noPropertyAccessFromIndexSignature": false }, "include": ["src/index.ts", "src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "examples"] }