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 .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ jobs:
- name: Lint & Build
run: |
bun lint
# bun run test
bun run test
bun run build
Binary file modified bun.lockb
Binary file not shown.
9 changes: 9 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
extensionsToTreatAsEsm: ['.ts'],
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
"workspaces": [
"packages/*"
],
"type": "module",
"scripts": {
"build": "bun run --cwd packages/agent-sdk build",
"lint": "prettier --check packages/**/* && eslint packages/",
"fmt": "prettier --write packages/**/* && eslint packages/ --fix"
"fmt": "prettier --write packages/**/* && eslint packages/ --fix",
"test": "jest"
},
"devDependencies": {
"@types/bun": "latest",
"@types/jest": "^29.5.14",
"@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/parser": "^8.16.0",
"eslint": "^9.15.0",
"jest": "^29.7.0",
"prettier": "^3.4.1",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2"
}
}
14 changes: 14 additions & 0 deletions packages/agent-sdk/src/evm/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ export async function erc20Approve(params: {
});
}

export async function checkAllowance(
owner: Address,
token: Address,
spender: Address,
chainId: number,
): Promise<bigint> {
return getClient(chainId).readContract({
address: token,
abi: erc20Abi,
functionName: "allowance",
args: [owner, spender],
});
}

export async function getTokenInfo(
chainId: number,
address: Address,
Expand Down
4 changes: 1 addition & 3 deletions packages/agent-sdk/src/evm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getAddress, type Hex, zeroAddress, type Address } from "viem";

export * from "./types";
export * from "./erc20";
export * from "./weth";

export function signRequestFor({
from,
Expand All @@ -14,7 +15,6 @@ export function signRequestFor({
chainId: number;
metaTransactions: MetaTransaction[];
}): SignRequestData {
console.log("metaTransactions", metaTransactions);
return {
method: "eth_sendTransaction",
chainId,
Expand Down Expand Up @@ -59,7 +59,6 @@ export async function validateRequest<
safeSaltNonce: string,
): Promise<TResponse | null> {
const metadataHeader = req.headers.get("mb-metadata");
console.log("Request Metadata:", JSON.stringify(metadataHeader, null, 2));
const metadata = JSON.parse(metadataHeader ?? "{}");
const { accountId, evmAddress } = metadata;
if (!accountId || !evmAddress) {
Expand All @@ -78,7 +77,6 @@ export async function validateRequest<
{ status: 401 },
) as TResponse;
}
console.log(`Valid request for ${accountId} <-> ${evmAddress}`);
return null;
}

Expand Down
115 changes: 115 additions & 0 deletions packages/agent-sdk/src/evm/weth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Network, type MetaTransaction, type SignRequestData } from "near-safe";
import {
type Address,
encodeFunctionData,
getAddress,
parseAbi,
parseEther,
toHex,
} from "viem";
import { signRequestFor } from ".";

type NativeAsset = {
address: Address;
symbol: string;
scanUrl: string;
decimals: number;
};

export function validateWethInput(params: URLSearchParams): {
chainId: number;
amount: bigint;
nativeAsset: NativeAsset;
} {
const chainIdStr = params.get("chainId");
const amountStr = params.get("amount");

// Ensure required fields
if (!chainIdStr) {
throw new Error("Missing required parameter: chainId");
}
if (!amountStr) {
throw new Error("Missing required parameter: amount");
}

// Validate chainId
const chainId = parseInt(chainIdStr);
if (isNaN(chainId)) {
throw new Error("Invalid chainId, must be a number");
}

// Validate amount
const amount = parseFloat(amountStr);
if (isNaN(amount) || amount <= 0) {
throw new Error("Invalid amount, must be a positive float");
}

return {
chainId,
amount: parseEther(amount.toString()),
nativeAsset: getNativeAsset(chainId),
};
}

export function unwrapSignRequest(
chainId: number,
amount: bigint,
): SignRequestData {
return signRequestFor({
chainId,
metaTransactions: [unwrapMetaTransaction(chainId, amount)],
});
}

export const unwrapMetaTransaction = (
chainId: number,
amount: bigint,
): MetaTransaction => {
return {
to: getNativeAsset(chainId).address,
value: "0x0",
data: encodeFunctionData({
abi: parseAbi(["function withdraw(uint wad)"]),
functionName: "withdraw",
args: [amount],
}),
};
};

export function wrapSignRequest(
chainId: number,
amount: bigint,
): SignRequestData {
return signRequestFor({
chainId,
metaTransactions: [wrapMetaTransaction(chainId, amount)],
});
}

export const wrapMetaTransaction = (
chainId: number,
amount: bigint,
): MetaTransaction => {
return {
to: getNativeAsset(chainId).address,
value: toHex(amount),
// methodId for weth.deposit
data: "0xd0e30db0",
};
};

export function getNativeAsset(chainId: number): NativeAsset {
const network = Network.fromChainId(chainId);
const wethAddress = network.nativeCurrency.wrappedAddress;
if (!wethAddress) {
throw new Error(
`Couldn't find wrapped address for Network ${network.name} (chainId=${chainId})`,
);
}
return {
address: getAddress(wethAddress),
symbol: network.nativeCurrency.symbol,
scanUrl: `${network.scanUrl}/address/${wethAddress}`,
decimals: network.nativeCurrency.decimals,
};
}
55 changes: 0 additions & 55 deletions packages/agent-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,2 @@
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 });
}
}
Loading