Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 31 additions & 1 deletion packages/agent-sdk/README.md
Original file line number Diff line number Diff line change
@@ -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<Input> = {
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:

Expand Down
55 changes: 55 additions & 0 deletions packages/agent-sdk/examples/erc20transfer.ts
Original file line number Diff line number Diff line change
@@ -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<Input> = {
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<Response> {
const url = new URL(req.url);
const search = url.searchParams;
console.log("erc20/", search);
try {
const { chainId, amount, token, recipient } = validateInput<Input>(
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 });
}
}
7 changes: 6 additions & 1 deletion packages/agent-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
102 changes: 102 additions & 0 deletions packages/agent-sdk/src/evm/erc20.ts
Original file line number Diff line number Diff line change
@@ -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<SignRequestData> {
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<SignRequestData> {
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<TokenInfo> {
const [decimals, symbol] = await Promise.all([
getTokenDecimals(chainId, address),
getTokenSymbol(chainId, address),
]);
return {
decimals,
symbol,
};
}

export async function getTokenDecimals(
chainId: number,
address: Address,
): Promise<number> {
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<string> {
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}`);
}
}
13 changes: 9 additions & 4 deletions packages/agent-sdk/src/evm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<TResponse | null> {
const metadataHeader = req.headers.get("mb-metadata");
console.log("Request Metadata:", JSON.stringify(metadataHeader, null, 2));
Expand All @@ -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}`);
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-sdk/src/evm/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface TokenInfo {
decimals: number;
symbol: string;
}
56 changes: 56 additions & 0 deletions packages/agent-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<Input> = {
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<Response> {
const url = new URL(req.url);
const search = url.searchParams;
console.log("erc20/", search);
try {
const { chainId, amount, token, recipient } = validateInput<Input>(
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 });
}
}
2 changes: 1 addition & 1 deletion packages/agent-sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@
"noPropertyAccessFromIndexSignature": false
},
"include": ["src/index.ts", "src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "examples"]
}