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
5 changes: 5 additions & 0 deletions typescript/.changeset/fuzzy-birds-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": minor
---

Added gasless transfers for USDC/CBBTC on Base/Base-Sepolia
14 changes: 14 additions & 0 deletions typescript/agentkit/src/action-providers/erc20/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Coinbase } from "@coinbase/coinbase-sdk";

export const abi = [
{
type: "event",
Expand Down Expand Up @@ -186,3 +188,15 @@ export const abi = [
],
},
] as const;

export const BaseTokenToAssetId = new Map([
["0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", Coinbase.assets.Cbbtc],
["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", Coinbase.assets.Usdc],
["0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42", Coinbase.assets.Eurc],
]);

export const BaseSepoliaTokenToAssetId = new Map([
["0xcbB7C0006F23900c38EB856149F799620fcb8A4a", Coinbase.assets.Cbbtc],
["0x036CbD53842c5426634e7929541eC2318f3dCF7e", Coinbase.assets.Usdc],
["0x808456652fdb597867f38412077A9182bf77359F", Coinbase.assets.Eurc],
]);
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ describe("Transfer Action", () => {
mockWallet = {
sendTransaction: jest.fn(),
waitForTransactionReceipt: jest.fn(),
getName: jest.fn().mockReturnValue("evm_wallet_provider"),
getNetwork: jest.fn().mockReturnValue({
networkId: "base-mainnet",
}),
} as unknown as jest.Mocked<EvmWalletProvider>;

mockWallet.sendTransaction.mockResolvedValue(TRANSACTION_HASH);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { ActionProvider } from "../actionProvider";
import { Network } from "../../network";
import { CreateAction } from "../actionDecorator";
import { GetBalanceSchema, TransferSchema } from "./schemas";
import { abi } from "./constants";
import { encodeFunctionData, formatUnits, Hex } from "viem";
import { EvmWalletProvider } from "../../wallet-providers";
import { abi, BaseTokenToAssetId, BaseSepoliaTokenToAssetId } from "./constants";
import { encodeFunctionData, formatUnits, Hex, getAddress } from "viem";
import { EvmWalletProvider, CdpWalletProvider } from "../../wallet-providers";

/**
* ERC20ActionProvider is an action provider for ERC20 tokens.
Expand Down Expand Up @@ -85,6 +85,37 @@ Important notes:
args: z.infer<typeof TransferSchema>,
): Promise<string> {
try {
// Check if we can do gasless transfer
const isCdpWallet = walletProvider.getName() === "cdp_wallet_provider";
const network = walletProvider.getNetwork();
const tokenAddress = getAddress(args.contractAddress);

const canDoGasless =
isCdpWallet &&
((network.networkId === "base-mainnet" && BaseTokenToAssetId.has(tokenAddress)) ||
(network.networkId === "base-sepolia" && BaseSepoliaTokenToAssetId.has(tokenAddress)));

if (canDoGasless) {
// Cast to CdpWalletProvider to access erc20Transfer
const cdpWallet = walletProvider as CdpWalletProvider;
const assetId =
network.networkId === "base-mainnet"
? BaseTokenToAssetId.get(tokenAddress)!
: BaseSepoliaTokenToAssetId.get(tokenAddress)!;
const hash = await cdpWallet.gaslessERC20Transfer(
assetId,
args.destination as Hex,
args.amount,
);

await walletProvider.waitForTransactionReceipt(hash);

return `Transferred ${args.amount} of ${args.contractAddress} to ${
args.destination
} using gasless transfer.\nTransaction hash: ${hash}`;
}

// Fallback to regular transfer
const hash = await walletProvider.sendTransaction({
to: args.contractAddress as Hex,
data: encodeFunctionData({
Expand Down
38 changes: 38 additions & 0 deletions typescript/agentkit/src/wallet-providers/cdpWalletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
Abi,
ContractFunctionName,
ContractFunctionArgs,
Address,
Hex,
} from "viem";
import { EvmWalletProvider } from "./evmWalletProvider";
import { Network } from "../network";
Expand Down Expand Up @@ -567,4 +569,40 @@ export class CdpWalletProvider extends EvmWalletProvider {
}
return this.#cdpWallet;
}

/**
* ERC20 transfer method
*
* @param assetId - The asset ID to transfer. Either USDC, CBBTC or EURC
* @param destination - The destination address
* @param amount - The amount to transfer
* @returns The transaction hash
*/
async gaslessERC20Transfer(
assetId:
| typeof Coinbase.assets.Usdc
| typeof Coinbase.assets.Cbbtc
| typeof Coinbase.assets.Eurc,
destination: Address,
amount: bigint,
): Promise<Hex> {
if (!this.#cdpWallet) {
throw new Error("Wallet not initialized");
}

const transferResult = await this.#cdpWallet.createTransfer({
amount,
assetId,
destination,
gasless: true,
});

const result = await transferResult.wait();

if (!result.getTransactionHash()) {
throw new Error("Transaction hash not found");
}

return result.getTransactionHash() as Hex;
}
}