Skip to content

Circle Wallet Service

The WalletService class in MessageKit provides a way to create agent wallets that can perform gasless payments and transfers on Base.

  • Gasless
  • Onramp
  • Offramp
  • Swaps
  • Transfers

Wallet Management

The getWallet method will retrieve an existing wallet or create a new one if it doesn't exist:

// Get the wallet service
const { walletService } = context;
 
// Get or create a wallet
const wallet = await walletService.getWallet(identifier or address);
 
// Explicitly create a new wallet
const isCreated = await walletService.createWallet(identifier or address);
 
// Delete a wallet
await walletService.deleteWallet(identifier or address);

Actions

Perform actions on the Agent Wallet like transfers and swaps or checking the balance.

// Get the wallet service
const { walletService } = context;
 
// Check balance
const { address, balance } = await walletService.checkBalance(identifier or address);
 
// Transfer between wallets
await walletService.transfer(identifier or address, identifier or address, amount);
 
// Swap assets (USDC and ETH)
await walletService.swap(identifier or address, fromAssetId, toAssetId, amount);

Requirements

Visit the Circle Developer Portal to create an API key and entity secret.

  • @circle-fin/developer-controlled-wallets: Already included in MessageKit
  • Environment variables:
    • CIRCLE_API_KEY= # Circle API Key
    • CIRCLE_ENTITY_SECRET= # Circle Entity Secret
    • CIRCLE_WALLET_SET_ID= # Circle Wallet Set ID

Code

plugins/circle.ts
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
import { LocalStorage } from "./storage";
 
const apiKey = process.env.CIRCLE_API_KEY;
const entitySecret = process.env.CIRCLE_ENTITY_SECRET;
const walletSetId = process.env.CIRCLE_WALLET_SET_ID;
 
export type AgentWalletData = {
  id: string;
  wallet: any;
  address: string;
  agent_address: string;
  blockchain?: string;
  state?: string;
  key: string;
};
 
export interface AgentWallet {
  getWallet: (
    key: string,
    createIfNotFound?: boolean,
  ) => Promise<AgentWalletData | undefined>;
  transfer: (
    fromAddress: string,
    toAddress: string,
    amount: number,
  ) => Promise<any>;
  checkBalance: (
    key: string,
  ) => Promise<{ address: string | undefined; balance: number }>;
  createWallet: (key: string) => Promise<AgentWalletData>;
  onRampURL: (amount: number, address: string) => Promise<string | undefined>;
}
 
const client =
  apiKey && entitySecret && walletSetId
    ? initiateDeveloperControlledWalletsClient({
        apiKey,
        entitySecret,
      })
    : undefined;
 
interface TransactionStatus {
  id: string;
  state: string;
  txHash?: string;
  createDate?: string;
  updateDate?: string;
}
 
interface TokenBalance {
  token: {
    id: string;
    blockchain: string;
    name: string;
    symbol: string;
    decimals: number;
    isNative: boolean;
    tokenAddress?: string;
  };
  amount: string;
  updateDate: string;
}
 
interface TokenBalanceResponse {
  data: {
    tokenBalances: TokenBalance[];
  };
}
 
export class WalletService implements AgentWallet {
  private walletStorage!: LocalStorage;
  private senderAddress: string;
 
  constructor(sender: string) {
    this.senderAddress = sender;
    this.walletStorage = new LocalStorage(".data/wallets");
  }
 
  async getWallet(userAddress: string): Promise<AgentWalletData> {
    const normalizedAddress = userAddress.toLowerCase();
    const walletData = await this.walletStorage.get(
      `wallet:${normalizedAddress}`,
    );
 
    if (walletData) {
      const wallet = JSON.parse(walletData);
      return wallet as AgentWalletData;
    }
 
    return this.createWallet(normalizedAddress);
  }
 
  async createWallet(identifier: string): Promise<AgentWalletData> {
    const normalizedIdentifier = identifier.toLowerCase();
    console.log(`Creating new wallet with id ${normalizedIdentifier}...`);
    const response = await client?.createWallets({
      accountType: "SCA",
      blockchains: ["ETH-SEPOLIA"],
      count: 1,
      walletSetId: process.env.CIRCLE_WALLET_SET_ID as string,
      metadata: [
        {
          name: "user",
          refId: normalizedIdentifier,
        },
      ],
    });
    //test
    if (!response?.data?.wallets?.[0]) {
      throw new Error("Failed to create wallet");
    }
 
    const wallet = response?.data?.wallets?.[0];
 
    // Store in LocalStorage
    await this.walletStorage.set(
      `wallet:${normalizedIdentifier}`,
      JSON.stringify({
        id: wallet.id,
        address: wallet.address.toLowerCase(),
      }),
    );
 
    console.log(`Created wallet with id ${normalizedIdentifier}`);
 
    return {
      id: wallet.id,
      blockchain: wallet.blockchain,
      state: wallet.state,
      wallet: client?.getWallet,
      address: this.senderAddress,
      agent_address: wallet.address,
      key: wallet.id,
    };
  }
 
  async transfer(
    fromAddress: string,
    toAddress: string,
    amount: number,
  ): Promise<any> {
    try {
      const normalizedFromAddress = fromAddress.toLowerCase();
      const normalizedToAddress = toAddress.toLowerCase();
 
      // Get token balances to find the requested token
      const fromWallet = await this.getWallet(normalizedFromAddress);
      const { balance, id } = await this.checkBalance(normalizedFromAddress);
 
      // Check if balance is sufficient
      if (balance < amount) {
        throw new Error(`Insufficient USDC balance. Available: ${balance}`);
      }
 
      console.log("Initiating transfer:", {
        from: fromWallet.id,
        to: normalizedToAddress,
        amount,
      });
 
      const response = await client?.createTransaction({
        walletId: fromWallet.id,
        tokenId: id as string,
        destinationAddress: normalizedToAddress,
        amount: [amount.toString()],
        fee: {
          type: "level",
          config: {
            feeLevel: "MEDIUM",
          },
        },
      });
 
      if (!response?.data?.id) {
        throw new Error("Failed to create transaction");
      }
 
      // Start monitoring transaction status
      const status = await this.checkTransactionStatus(response?.data?.id);
      return status;
    } catch (error) {
      console.error("Error in transfer:", error);
      throw error;
    }
  }
 
  // Helper method to get available tokens
  private async getAvailableTokens(
    walletData: AgentWalletData,
  ): Promise<string[]> {
    const balances = await this.getBalance(walletData);
    return balances.map((b) => b.token.symbol);
  }
 
  private async getBalance(
    walletData: AgentWalletData,
  ): Promise<TokenBalance[]> {
    try {
      console.log(`Checking balance for wallet ${walletData.id}...`);
      const response = (await client?.getWalletTokenBalance({
        id: walletData.id,
      })) as TokenBalanceResponse;
 
      //console.log("Wallet balance retrieved:", response.data.tokenBalances);
 
      if (!response.data?.tokenBalances) {
        throw new Error("Failed to get wallet balance");
      }
 
      return response.data.tokenBalances;
    } catch (error) {
      console.error("Error checking balance:", error);
      throw error;
    }
  }
 
  // Helper method to get specific token balance
  async checkBalance(
    humanAddress: string,
    tokenSymbol: string = "USDC",
  ): Promise<{
    address: string | undefined;
    balance: number;
    id: string | undefined;
  }> {
    const userWallet = await this.getWallet(humanAddress);
    if (!userWallet) {
      return { address: undefined, balance: 0, id: undefined };
    }
 
    const balances = await this.getBalance(userWallet);
    const token = balances.find(
      (b) => b.token.symbol.toUpperCase() === tokenSymbol.toUpperCase(),
    );
    return {
      address: token?.token.tokenAddress,
      id: token?.token.id,
      balance: token ? parseFloat(token.amount) : 0,
    };
  }
 
  async checkTransactionStatus(
    transactionId: string,
  ): Promise<TransactionStatus> {
    try {
      const response = await client?.getTransaction({
        id: transactionId,
      });
 
      if (!response?.data?.transaction) {
        throw new Error("Failed to get transaction status");
      }
 
      const tx = response.data.transaction;
      return {
        id: tx.id,
        state: tx.state,
        txHash: tx.txHash,
        createDate: tx.createDate,
        updateDate: tx.updateDate,
      };
    } catch (error) {
      console.error("Error checking transaction status:", error);
      throw error;
    }
  }
  async onRampURL(amount: number, address: string) {
    return undefined;
  }
}