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/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/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..24f18cc 100644
--- a/packages/agent-sdk/src/index.ts
+++ b/packages/agent-sdk/src/index.ts
@@ -1 +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"]
}