Bitcoin Frontend

Initialize a new project:

npm init -y

Install a simple HTTP server:

yarn add --dev http-server

Create a page with a simple form:

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <input type="number" id="amount" placeholder="Amount in tBTC" />
    <input type="text" id="contract" placeholder="Omnichain contract address" />
    <input type="text" id="message" placeholder="Contract call parameters" />
    <select id="walletSelect">
      <option value="xdefi">XDEFI</option>
      <option value="unisat">Unisat</option>
    <button onclick="sendTransaction()">Send transaction</button>
    <script src="./script.js"></script>

This HTML page contains a simple form with three inputs:

  • Amount - the amount of tBTC to send
  • Contract - the address of the omnichain contract to call
  • Params - the parameters to pass to the contract call
  • Wallet Select - a dropdown with a list of available wallets
  • Send transaction - a button that will call the sendTransaction function

To call an omnichain contract from Bitcoin you need to send a token transfer transaction to the ZetaChain's TSS address on Bitcoin with a memo that conforms to the required format.

The memo is composed of two parts:

  • The first part is the contract address encoded as hex string (without the 0x prefix). This is the address of the omnichain contract you want to call.
  • The second part is the message encoded as hex string (without the 0x prefix). This is the message you want to pass to the contract call.

For example, if the omnichain contract address is 0xc79E6DC99C5928C5b08ae7a0f79412521996938e and it expects to receive one argument that happens to be an address 0x3724C896Cdc958611873D81547A98565c8cb849d, the memo passed to the wallet would look like:


The list of TSS address is available in the docs, for the purposes of this tutorial we will use a hard-coded value.

Get the contract address and parameters from the form and ensure that the 0x prefix is removed.

The amount is passed in tBTC, so we need to multiply it by 1e8 to convert it to satoshis.

async function sendTransaction() {
  const tss = "tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur";
  const contract = document.getElementById("contract").value.replace(/^0x/, "");
  if (contract.length !== 40) return alert("Not a valid contract address");
  const message = document.getElementById("message").value.replace(/^0x/, "");
  const amount = parseFloat(document.getElementById("amount").value) * 1e8;
  if (isNaN(amount)) return alert("Amount must be a number");
  const params = { contract, message, amount, tss };
  const wallet = document.getElementById("walletSelect")?.value;
  switch (wallet) {
    case "unisat":
      await useUnisat(params);
    case "xdefi":
      await useXDEFI(params);

XDEFI injects itself into the window object, so we can access it from the window.xfi property. Use the getAccounts method to get the list of accounts and use the first one.

For XDEFI to properly encode the memo, you need to prefix it with hex::.

async function useXDEFI(p) {
  if (!window.xfi) return alert("XDEFI wallet not installed");
  const wallet = window.xfi;
  const account = (await wallet?.bitcoin?.getAccounts())?.[0];
  if (!account) return alert("No account found");
  const tx = {
    method: "transfer",
    params: [
        feeRate: 10,
        from: account,
        recipient: p.tss,
        amount: {
          amount: p.amount,
          decimals: 8,
        memo: `hex::${p.contract}${p.message}`,
  window.xfi.bitcoin.request(tx, (err, res) => {
    if (e) {
      return alert(`Couldn't send transaction, ${JSON.stringify(err)}`);
    } else if (res) {
      return alert(`Broadcasted a transaction, ${JSON.stringify(res)}`);

Call the request method on the window.xfi.bitcoin object to send the transaction.

Now that we have the from address (your Bitcoin address), recipient address (ZetaChain's TSS address), amount and a properly formatted memo, call window.xfi.bitcoin.request to create and broadcast a transaction.

Learn more about how to use XDEFI programmatically in the XDEFI docs (opens in a new tab).

Unisat injects itself into the window object, so we can access it from the window.unisat property. Use the requestAccounts method to get the list of accounts.

Prepare the memo by concatenating the contract address and the message and converting the resulting string to lower case.

Use unisat.sendBitcoin() to make a token transfer to the TSS address.

async function useUnisat(p) {
  if (!window.unisat) return alert("Unisat wallet not installed");
  try {
    await window.unisat.requestAccounts();
    const memos = [`${p.contract}${p.message}`.toLowerCase()];
    const tx = await unisat.sendBitcoin(p.tss, p.amount, { memos });
    return alert(`Broadcasted a transaction: ${JSON.stringify(tx)}`);
  } catch (e) {
    return alert(`Couldn't send transaction, ${JSON.stringify(e)}`);

Start the HTTP server:

npx http-server

Open the page in your browser and fill in the form. You can test functionality with your own contract address or follow one of the provided tutorials, for example, the Staking tutorial to deploy a contract that you can call from Bitcoin.

Fill out the form and click the "Send transaction" button. You will be prompted to confirm the transaction.

Once the transaction is confirmed, you will see the transaction hash in an alert window. You can use a Bitcoin block explorer like Blockstream (opens in a new tab) to check the transaction status.

Once the transaction is confirmed on Bitcoin, you can check the transaction status on ZetaChain using the cctx command from the Hardhat template (opens in a new tab).