Sign with a Cosmos Wallet
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
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);
};// 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.bodyand.authInfoare protobuf classes.toBinary()gives the bytes Keplr expects.accountNumberin the sign doc must be the exact chain value. Keplr's type for this field is aLong; if your Keplr typings reject a bigint, useLong.fromString(String(sender.accountNumber), true)from thelongpackage. Never construct it from a JSnumber.preferNoSetFee: trueandpreferNoSetMemo: trueask 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 }. BothsimulateTxandbroadcastTxaccept that string directly; useJSON.parseif you need to inspect its fields. It throws whensenderorsender.publicKeyis missing, since a Cosmos signature needs the public key inauthInfo.- Leap and Cosmostation expose the same
signDirectshape.GenericCosmosAdapter.fromBrowserWallet(wallet, chainId)wraps any such provider if you want the signing client instead. - Server-side keys:
GenericCosmosAdapter.fromMnemonicorfromPrivateKeysign the sha256 of theSignDocbytes with secp256k1.payload.signDirect.signBytesis keccak256-hashed for ethermint compatibility, so do not sign that field with a plain Cosmos key.