TradingDEFiWallet API

This solutions guide explains how to create a Telegram trading bot using the Coinbase Developer Platform (CDP) SDK and Wallet API. The bot will allow users to check balances, deposit and withdraw ETH, and perform buy and sell operations directly from Telegram.

This is a great template for building your own Telegram bot with additional features from the CDP SDK, such as deploying tokens or interacting with smart contracts!

Replit for easy deployment

Replit is an AI-powered software development & deployment platform for building, sharing, and shipping software fast. Coinbase has partnered with Replit to create a template that enables developers to register their AI agent onchain in just minutes.

Use the Replit template for an easier cloning and deployment experience.

This sample app is for demonstration purposes only. Make sure to persist your private keys, and deposit only small amounts of ETH to reduce the risk of losing your funds. Secure your wallet using best practices. In production, you should use the 2-of-2 CDP Server-Signer with IP whitelisting for your Secret API key for increased security.

Prerequisites

  • Install the CDP SDK.
  • Provision a CDP Secret API Key.
  • Provision a Telegram Bot Token and register your Bot.
  • Generate a 32-byte encryption key using OpenSSL:
    openssl rand -hex 32 # Save the output to use as the encryption key in Step 5.
    

Step-by-Step Guide

Step 1. Import Required Modules

First, we need to import the necessary modules for our bot. This includes the grammy library for interacting with Telegram, the coinbase-sdk for interacting with the CDK SDK, and other utility libraries.

index.js
const { Bot, InlineKeyboard } = require("grammy");
const { Coinbase, Wallet } = require("@coinbase/coinbase-sdk");
const Database = require("@replit/database");
const Decimal = require("decimal.js");
const Web3 = require("web3");
const crypto = require("crypto");

Step 2. Ensure Environment Variables are Set

Next, we ensure that all required environment variables are set. These variables include the Telegram bot token, Coinbase secret API key name and secret, and the encryption key.

index.js
// Ensure environment variables are set.
const requiredEnvVars = [
  "TELEGRAM_BOT_TOKEN",
  "COINBASE_API_KEY_NAME",
  "COINBASE_API_KEY_SECRET",
  "ENCRYPTION_KEY",
];

requiredEnvVars.forEach((env) => {
  if (!process.env[env]) {
    throw new Error(`missing ${env} environment variable`);
  }
});

Step 3. Create a Bot Object

We create a bot object using the grammy library and set up in-memory storage for user states and a database for storing wallets.

index.js
// Create a bot object
const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN);

// In-memory storage for user states
const userStates = {};

// Database for storing wallets
const db = new Database();

Step 4. Initialize Coinbase SDK

We initialize the Coinbase SDK with the provided secret API key name and secret.

index.js
// Initialize Coinbase SDK
const privateKey = process.env.COINBASE_API_KEY_SECRET.replace(/\\n/g, "\n");
Coinbase.configure({
  apiKeyName: process.env.COINBASE_API_KEY_NAME,
  privateKey: privateKey,
});

Step 5. Define Helper Functions

We define several helper functions to manage user states, send replies, and handle user interactions.

index.js
// Helper functions
const updateUserState = (user, state) => {
  userStates[user.id] = { ...userStates[user.id], ...state };
};

const clearUserState = (user) => {
  delete userStates[user.id];
};

const sendReply = async (ctx, text, options = {}) => {
  const message = await ctx.reply(text, options);
  updateUserState(ctx.from, { messageId: message.message_id });
};

const handleUserState = async (ctx, handler) => {
  const userState = userStates[ctx.from.id] || {};
  if (
    ctx.message.reply_to_message &&
    ctx.message.reply_to_message.message_id === userState.messageId
  ) {
    await handler(ctx);
  } else {
    await ctx.reply("Please select an option from the menu.");
  }
};

Step 6. Bot Command Handlers

We define the command handlers for the bot. The /start command initializes the bot and presents the user with a menu of options.

index.js
// Bot command handlers
bot.command("start", async (ctx) => {
  const { from: user } = ctx;
  updateUserState(user, {});
  userAddress = await getOrCreateAddress(user);

  const keyboard = new InlineKeyboard()
    .text("Check Balance", "check_balance")
    .row()
    .text("Deposit ETH", "deposit_eth")
    .row()
    .text("Withdraw ETH", "withdraw_eth")
    .row()
    .text("Buy", "buy")
    .text("Sell", "sell")
    .row()
    .text("Export key", "export_key")
    .text("Pin message", "pin_message");

  const welcomeMessage = `
  *Welcome to your Onchain Trading Bot!*
  Your Base address is ${userAddress.getId()}.
  Select an option below:`;

  await sendReply(ctx, welcomeMessage, {
    reply_markup: keyboard,
    parse_mode: "Markdown",
  });
});

Step 7. Callback Query Handlers

We define the callback query handlers for the bot. These handlers respond to user interactions with the menu options.

index.js
// Callback query handlers
const callbackHandlers = {
  check_balance: handleCheckBalance,
  deposit_eth: handleDeposit,
  withdraw_eth: handleInitialWithdrawal,
  buy: handleInitialBuy,
  sell: handleInitialSell,
  pin_message: handlePinMessage,
  export_key: handleExportKey,
};

bot.on("callback_query:data", async (ctx) => {
  const handler = callbackHandlers[ctx.callbackQuery.data];
  if (handler) {
    await ctx.answerCallbackQuery();
    await handler(ctx);
  } else {
    await ctx.reply("Unknown button clicked!");
  }
  console.log(
    `User ID: ${ctx.from.id}, Username: ${ctx.from.username}, First Name: ${ctx.from.first_name}`,
  );
});

Step 8. Handle User Messages

We handle user messages by checking the current state of the user and calling the appropriate handler function.

index.js
// Handle user messages
bot.on("message:text", async (ctx) =>
  handleUserState(ctx, async () => {
    const userState = userStates[ctx.from.id] || {};
    if (userState.withdrawalRequested) await handleWithdrawal(ctx);
    else if (userState.buyRequested) await handleBuy(ctx);
    else if (userState.sellRequested) await handleSell(ctx);
  }),
);

Step 9. Get or Create the User’s Address

We define a function to get or create the user’s address. If the user does not have an address, a new wallet is created and stored in the database.

index.js
// Get or create the user's address
async function getOrCreateAddress(user) {
  if (userStates.address) {
    return userStates.address;
  }

  const result = await db.get(user.id.toString());

  let wallet;
  if (result?.value) {
    const { ivString, encryptedWalletData } = result.value;
    const iv = Buffer.from(ivString, "hex");
    const walletData = JSON.parse(decrypt(encryptedWalletData, iv));
    wallet = await Wallet.import(walletData);
  } else {
    wallet = await Wallet.create({ networkId: "base-mainnet" });
    const iv = crypto.randomBytes(16);
    const encryptedWalletData = encrypt(JSON.stringify(wallet.export()), iv);
    await db.set(user.id.toString(), {
      ivString: iv.toString("hex"),
      encryptedWalletData,
    });
  }

  updateUserState(user, { address: await wallet.getDefaultAddress() });

  return await wallet.getDefaultAddress();
}

Step 10. Handle Checking Balance

We define a function to handle checking the user’s balance. This function retrieves the user’s address and lists their balances.

index.js
// Handle checking balance
async function handleCheckBalance(ctx) {
  const userAddress = await getOrCreateAddress(ctx.from);
  const balanceMap = await userAddress.listBalances();
  const balancesString =
    balanceMap.size > 0
      ? balanceMap.toString().slice(11, -1)
      : "You have no balances.";
  await sendReply(
    ctx,
    `Your current balances are as follows:\n${balancesString}`,
  );
}

Step 11. Handle Deposits

We define a function to handle deposits. This function provides the user with their address and instructions for depositing ETH.

index.js
// Handle deposits
async function handleDeposit(ctx) {
  const userAddress = await getOrCreateAddress(ctx.from);
  await sendReply(
    ctx,
    "_Note: As this is a test app, make sure to deposit only small amounts of ETH!_",
    { parse_mode: "Markdown" },
  );
  await sendReply(
    ctx,
    "Please send your ETH to the following address on Base:",
  );
  await sendReply(ctx, `${userAddress.getId()}`, { parse_mode: "Markdown" });
}

Step 12. Handle Initial Withdrawal Request

We define a function to handle the initial withdrawal request. This function prompts the user to enter the amount of ETH they want to withdraw.

index.js
// Handle initial withdrawal request
async function handleInitialWithdrawal(ctx) {
  updateUserState(ctx.from, { withdrawalRequested: true });
  await sendReply(
    ctx,
    "Please respond with the amount of ETH you want to withdraw.",
    { reply_markup: { force_reply: true } },
  );
}

Step 13. Handle Withdrawals

We define a function to handle withdrawals. This function processes the user’s withdrawal request and initiates the transfer.

index.js
// Handle withdrawals
async function handleWithdrawal(ctx) {
  const userState = userStates[ctx.from.id] || {};

  if (!userState.withdrawalAmount) {
    const withdrawalAmount = parseFloat(ctx.message.text);
    if (isNaN(withdrawalAmount)) {
      await ctx.reply("Invalid withdrawal amount. Please try again.");
      clearUserState(ctx.from);
    } else {
      const userAddress = await getOrCreateAddress(ctx.from);
      const currentBalance = await userAddress.getBalance(Coinbase.assets.Eth);
      if (new Decimal(withdrawalAmount).greaterThan(currentBalance)) {
        await ctx.reply("You do not have enough ETH to withdraw that amount.");
        clearUserState(ctx.from);
      } else {
        await sendReply(
          ctx,
          "Please respond with the address, ENS name, or Base name at which you would like to receive the ETH.",
          { reply_markup: { force_reply: true } },
        );
        updateUserState(ctx.from, {
          withdrawalAmount,
        });
      }
    }
  } else {
    const destination = ctx.message.text;
    if (!Web3.utils.isAddress(destination) && !destination.endsWith(".eth")) {
      await ctx.reply("Invalid destination address. Please try again.");
      clearUserState(ctx.from);
      return;
    }

    const userAddress = await getOrCreateAddress(ctx.from);

    try {
      await sendReply(ctx, "Initiating withdrawal...");
      const transfer = await userAddress.createTransfer({
        amount: userState.withdrawalAmount,
        assetId: Coinbase.assets.Eth,
        destination: destination,
      });
      await transfer.wait();
      await sendReply(
        ctx,
        `Successfully completed withdrawal: [Basescan Link](${transfer.getTransactionLink()})`,
        { parse_mode: "Markdown" },
      );
      clearUserState(ctx.from);
    } catch (error) {
      await ctx.reply("An error occurred while initiating the transfer.");
      console.error(error);
      clearUserState(ctx.from);
    }
  }
}

Step 14. Handle Buy Request

We define functions to handle buy requests. These functions prompt the user to enter the asset they want to buy and the amount of ETH they want to spend.

index.js
// Handle buy request
async function handleInitialBuy(ctx) {
  await handleTradeInit(ctx, "buy");
}

// Handle buys
async function handleBuy(ctx) {
  await executeTrade(ctx, "buy");
}

Step 15. Handle Sell Request

We define functions to handle sell requests. These functions prompt the user to enter the asset they want to sell and the amount they want to sell.

index.js
// Handle sell request
async function handleInitialSell(ctx) {
  await handleTradeInit(ctx, "sell");
}

// Handle sells
async function handleSell(ctx) {
  await executeTrade(ctx, "sell");
}

Step 16. Initialize Trade (Buy/Sell)

We define functions to initialize and execute trades. These functions handle the user input and perform the trade operations.

index.js
// Initialize trade (Buy/Sell)
async function handleTradeInit(ctx, type) {
  const prompt =
    type === "buy"
      ? "Please respond with the asset you would like to buy (ticker or contract address)."
      : "Please respond with the asset you would like to sell (ticker or contract address).";
  updateUserState(ctx.from, { [`${type}Requested`]: true });
  await sendReply(ctx, prompt, { reply_markup: { force_reply: true } });
}

// Generalized function to execute trades
async function executeTrade(ctx, type) {
  const userState = userStates[ctx.from.id] || {};
  if (!userState.asset) {
    // Prevent sale of ETH and log asset to user state
    if (ctx.message.text.toLowerCase() === "eth" && type === "sell") {
      await ctx.reply(
        "You cannot sell ETH, as it is the quote currency. Please try again.",
      );
      clearUserState(ctx.from);
      return;
    }

    updateUserState(ctx.from, { asset: ctx.message.text.toLowerCase() });

    const prompt =
      type === "buy"
        ? "Please respond with the amount of ETH you would like to spend."
        : "Please respond with the amount of the asset you would like to sell.";
    await sendReply(ctx, prompt, { reply_markup: { force_reply: true } });
  } else {
    const amount = new Decimal(parseFloat(ctx.message.text));
    const userAddress = await getOrCreateAddress(ctx.from);
    const currentBalance = await userAddress.getBalance(
      type === "buy" ? Coinbase.assets.Eth : userState.asset,
    );
    if (amount.isNaN() || amount.greaterThan(currentBalance)) {
      await ctx.reply(
        "Invalid amount or insufficient balance. Please try again.",
      );
      clearUserState(ctx.from);
    } else {
      const tradeType =
        type === "buy"
          ? { fromAssetId: Coinbase.assets.Eth, toAssetId: userState.asset }
          : { fromAssetId: userState.asset, toAssetId: Coinbase.assets.Eth };
      await sendReply(ctx, `Initiating ${type}...`);
      try {
        const userAddress = await getOrCreateAddress(ctx.from);
        const trade = await userAddress.createTrade({ amount, ...tradeType });
        await trade.wait();
        await sendReply(
          ctx,
          `Successfully completed ${type}: [Basescan Link](${trade.getTransaction().getTransactionLink()})`,
          { parse_mode: "Markdown" },
        );
        clearUserState(ctx.from);
      } catch (error) {
        await ctx.reply(`An error occurred while initiating the ${type}.`);
        console.error(error);
        clearUserState(ctx.from);
      }
    }
  }
}

Step 17. Handle Pinning the Start Message

We define a function to handle pinning the start message. This function pins the welcome message to the chat.

index.js
// Handle pinning the start message
async function handlePinMessage(ctx) {
  try {
    await ctx.api.pinChatMessage(
      ctx.chat.id,
      userStates[ctx.from.id].messageId,
    );
    await ctx.reply("Message pinned successfully!");
  } catch (error) {
    console.error("Failed to pin the message:", error);
    await ctx.reply(
      "Failed to pin the message. Ensure the bot has the proper permissions.",
    );
  }
  clearUserState(ctx.from);
}

Step 18. Handle Exporting the Key

We define a function to handle exporting the key. This function provides the user with their private key and instructions for storing it safely.

index.js
// Handle exporting the key
async function handleExportKey(ctx) {
  const userAddress = await getOrCreateAddress(ctx.from);
  const privateKey = userAddress.export();
  await sendReply(
    ctx,
    "Your private key will be in the next message. Do NOT share it with anyone, and make sure you store it in a safe place.",
  );
  await sendReply(ctx, privateKey);
}

Step 19. Encrypt and Decrypt Functions

We define functions to encrypt and decrypt data. These functions use the AES-256-CBC encryption algorithm.

index.js
// Encrypt and Decrypt functions
function encrypt(text, iv) {
  const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
  const cipher = crypto.createCipheriv("aes-256-cbc", encryptionKey, iv);
  return cipher.update(text, "utf8", "hex") + cipher.final("hex");
}

function decrypt(encrypted, iv) {
  const encryptionKey = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
  const decipher = crypto.createDecipheriv("aes-256-cbc", encryptionKey, iv);
  return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
}

Step 20. Start the Bot

Finally, we start the bot using long polling.

index.js
// Start the bot (using long polling)
bot.start();

console.log("Trading bot is running...");

Conclusion

Now that you have set up your Telegram trading bot, you can interact with it by sending commands and messages. The bot will handle checking balances, deposits, withdrawals, and trading operations. If you have any questions or need further assistance, feel free to reach out to us in the CDP Discord.