Skip to content

Sign a BitBadges transaction by hand with MetaMask, Privy, or ethers.js by sending the payload's EVM precompile call.

With evmAddress in the TxContext, createTransactionPayload converts the messages into a call to a BitBadges EVM precompile. An Ethereum wallet signs and sends that call like any contract transaction. Use Signing Client unless you need this level of control.

Example

ts
import { createTransactionPayload, MsgTransferTokens, type TxContext } from 'bitbadges';
import { ethers } from 'ethers';

const ALICE = 'bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d';

const msg = new MsgTransferTokens({
  creator: ALICE,
  collectionId: '1',
  transfers: [
    {
      from: ALICE,
      toAddresses: ['bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue'],
      balances: [{ amount: '1', tokenIds: [{ start: '1', end: '1' }], ownershipTimes: [{ start: '1', end: '18446744073709551615' }] }]
    }
  ]
});

const txContext: TxContext = {
  sender: {
    address: ALICE, // bb1 form of the same account
    sequence: '0',
    accountNumber: '17246720312988307372', // decimal string, as the API returned it
    publicKey: '' // not needed for EVM
  },
  fee: { amount: '0', denom: 'ubadge', gas: '200000' },
  memo: '',
  evmAddress: '0x0bc63cfe31d5218eb414b142c799e20964a54a1a' // enables precompile conversion
};

const payload = createTransactionPayload(txContext, msg.toProto());
if (!payload.evmTx) throw new Error('Messages are not supported for EVM transactions');

const { to, data } = payload.evmTx;

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

// Simulate: estimate gas
const gasEstimate = await signer.estimateGas({ to, data, value: 0n });

// Send
const tx = await signer.sendTransaction({ to, data, value: 0n, gasLimit: gasEstimate });
const receipt = await tx.wait();
console.log('tx hash:', receipt?.hash);

With Privy:

ts
import { useSendTransaction } from '@privy-io/react-auth';
import { BitBadgesAPI, BigIntify, createTransactionPayload, type TxContext } from 'bitbadges';

function useEthereumSigning() {
  const { sendTransaction } = useSendTransaction();
  const api = new BitBadgesAPI({ convertFunction: BigIntify });

  const signAndBroadcast = async (evmAddress: string, bitbadgesAddress: string, messages: any[]) => {
    const { account } = await api.getAccount({ address: bitbadgesAddress });

    const txContext: TxContext = {
      sender: { address: bitbadgesAddress, sequence: account.sequence ?? 0n, accountNumber: account.accountNumber, publicKey: '' },
      fee: { amount: '0', denom: 'ubadge', gas: '200000' },
      memo: '',
      evmAddress
    };

    const payload = createTransactionPayload(txContext, messages);
    if (!payload.evmTx) throw new Error('Messages not supported for EVM transactions');

    const result = await sendTransaction({ to: payload.evmTx.to, data: payload.evmTx.data, value: 0n }, { address: evmAddress });

    return { txHash: result.hash, precompileAddress: payload.evmTx.to, functionName: payload.evmTx.functionName };
  };

  return { signAndBroadcast };
}

Fields

payload.evmTx:

FieldTypeDescription
tostringPrecompile address, chosen from the message type
datastringABI-encoded call data
valuestringAlways "0"
functionNamestringThe precompile function, for logging
PrecompileAddressMessages
Tokenization0x0000000000000000000000000000000000001001Every x/tokenization message: MsgTransferTokens, MsgCreateCollection, MsgUniversalUpdateCollection, MsgSetIncomingApproval, MsgSetOutgoingApproval, MsgSetCollectionApprovals, MsgSetTokenMetadata, MsgSetCollectionMetadata, dynamic store messages, and the rest
Gamm0x0000000000000000000000000000000000001002MsgCreateBalancerPool, MsgJoinPool, MsgExitPool, MsgSwapExactAmountIn, MsgSwapExactAmountInWithIBCTransfer
Send manager0x0000000000000000000000000000000000001003MsgSend (native coin transfers)

Behavior

  • sender.address must be the bb1 form and evmAddress the 0x form of the same account. The SDK converts addresses inside the message fields.
  • publicKey is not used on the EVM path. The signature is a standard EIP-155 transaction signature.
  • If you set only evmAddress and no sender, the payload contains only evmTx. With both, you also get the Cosmos payloads and can fall back.
  • payload.evmTx is undefined when any message has no precompile mapping. Check for it and fall back to Cosmos signing.
  • Several tokenization messages in one payload become one executeMultiple call to the tokenization precompile (functionName is executeMultiple). Messages from different modules cannot share one EVM transaction.
  • The wallet must be on the BitBadges EVM network: chain ID 50024 on mainnet, RPC https://evm-rpc.bitbadges.io. See RPC endpoints.
CosmosEthereum
WalletsKeplr, Leap, CosmostationMetaMask, Privy, any EIP-1193 provider
Address in walletbb1 form0x form
Public key in contextRequiredNot required
SignatureCosmos signDirectEIP-155
What is signedProtobuf SignDocA contract call to a precompile

Edit this page on GitHub