Skip to content

Sign a BitBadges transaction payload by hand with Keplr or another Cosmos wallet using signDirect, then build the broadcast body.

Manual Cosmos signing takes the signDirect payload from createTransactionPayload, asks the wallet for a signature over it, and wraps the result with createTxBroadcastBody. Use Signing Client unless you need this level of control.

Example

ts
import { createTransactionPayload, createTxBroadcastBody, type TxContext, type TransactionPayload } from 'bitbadges';

const chainId = 'bitbadges-1';

const signTxn = async (context: TxContext, payload: TransactionPayload, msgs: any[], simulate: boolean) => {
  const sender = context.sender!;
  await window.keplr?.enable(chainId);

  // Simulations do not check signatures, so an empty signature is fine.
  let hexSig = '';

  if (!simulate) {
    const signResponse = await window.keplr?.signDirect(
      chainId,
      sender.address,
      {
        bodyBytes: payload.signDirect.body.toBinary(),
        authInfoBytes: payload.signDirect.authInfo.toBinary(),
        chainId,
        // v34+ account numbers exceed Number.MAX_SAFE_INTEGER. Build the value from the
        // string the API returned. Never `Number()` it, and never `new Long(n)` from a JS number.
        accountNumber: BigInt(String(sender.accountNumber)) as any
      },
      { preferNoSetFee: true, preferNoSetMemo: true }
    );
    if (!signResponse) throw new Error('No signature returned from Keplr');

    hexSig = Array.from(atob(signResponse.signature.signature), (char) => char.charCodeAt(0).toString(16).padStart(2, '0')).join('');
  }

  return createTxBroadcastBody(context, msgs, hexSig);
};
ts
// Usage
import { BitBadgesAPI, BigIntify, MsgTransferTokens } from 'bitbadges';

const api = new BitBadgesAPI({ convertFunction: BigIntify, apiKey: process.env.BITBADGES_API_KEY }); // key from https://bitbadges.io/developer
const ALICE = 'bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d';

const msgs = [
  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 { account } = await api.getAccount({ address: ALICE });
const txContext: TxContext = {
  sender: { address: account.address, sequence: account.sequence ?? 0n, accountNumber: account.accountNumber, publicKey: account.publicKey },
  fee: { amount: '0', denom: 'ubadge', gas: '400000' },
  memo: ''
};

const payload = createTransactionPayload(txContext, msgs.map((msg) => msg.toProto()));
const simBody = await signTxn(txContext, payload, msgs, true);
const sim = await api.simulateTx(simBody);
txContext.fee.gas = String(Math.ceil(Number(sim.gas_info.gas_used) * 1.3));
const txBody = await signTxn(txContext, createTransactionPayload(txContext, msgs.map((msg) => msg.toProto())), msgs, false);
const res = await api.broadcastTx(txBody);
console.log(res.tx_response.txhash);

Behavior

  • payload.signDirect.body and .authInfo are protobuf classes. toBinary() gives the bytes Keplr expects.
  • accountNumber in the sign doc must be the exact chain value. Keplr's type for this field is a Long; if your Keplr typings reject a bigint, use Long.fromString(String(sender.accountNumber), true) from the long package. Never construct it from a JS number.
  • preferNoSetFee: true and preferNoSetMemo: true ask Keplr to preserve the fee and memo used to build the payload. If your wallet changes the returned signed bytes, broadcast those bytes instead of rebuilding the original payload.
  • createTxBroadcastBody(txContext, messages, hexSignature) returns a JSON string encoding { tx_bytes, mode }. Both simulateTx and broadcastTx accept that string directly; use JSON.parse if you need to inspect its fields. It throws when sender or sender.publicKey is missing, since a Cosmos signature needs the public key in authInfo.
  • Leap and Cosmostation expose the same signDirect shape. GenericCosmosAdapter.fromBrowserWallet(wallet, chainId) wraps any such provider if you want the signing client instead.
  • Server-side keys: GenericCosmosAdapter.fromMnemonic or fromPrivateKey sign the sha256 of the SignDoc bytes with secp256k1. payload.signDirect.signBytes is keccak256-hashed for ethermint compatibility, so do not sign that field with a plain Cosmos key.

Edit this page on GitHub