Signing
Sign and verify exact message bytes, finalize custom inputs at their real transaction index, and handle interrupted BRC-100 signing safely.
Use wallet signatures to prove approval of exact bytes or to unlock a transaction input. Your app requests a permissioned operation; private-key custody belongs to the wallet implementation. These examples were checked against @bsv/sdk 4.0.0 on October 26, 2026. Use a maintained Node.js 22 or 24 release.
Sign and verify the same derivation context
Both sides must agree on the exact message bytes, protocol ID, key ID and counterparty relationship. Alice signs using Bob's complete public identity key; Bob verifies using Alice's. A signature is not encryption. Counterparty derivation is not a promise that no third party can verify a signature if its derived public key is later disclosed.
1import type { WalletInterface } from '@bsv/sdk'
2import { PublicKey, Utils } from '@bsv/sdk'
3
4async function signForPeer(
5 wallet: Pick<WalletInterface, 'createSignature'>,
6 peerIdentityKey: string,
7 message: string
8) {
9 PublicKey.fromString(peerIdentityKey)
10 const { signature } = await wallet.createSignature({
11 data: Utils.toArray(message, 'utf8'),
12 protocolID: [1, 'example app signatures'],
13 keyID: 'message-1',
14 counterparty: peerIdentityKey
15 })
16 return signature
17}
18
19async function verifyFromPeer(
20 wallet: Pick<WalletInterface, 'verifySignature'>,
21 senderIdentityKey: string,
22 message: string,
23 signature: number[]
24) {
25 PublicKey.fromString(senderIdentityKey)
26 return wallet.verifySignature({
27 data: Utils.toArray(message, 'utf8'),
28 signature,
29 protocolID: [1, 'example app signatures'],
30 keyID: 'message-1',
31 counterparty: senderIdentityKey
32 })
33}These functions do not create a transaction. Authenticate the connected wallet and obtain consent before calling them. Handle both denied requests and invalid-signature errors; do not turn every exception into a successful check. In a real protocol, sign an unambiguous payload that includes the operation, audience and freshness/replay context, rather than an arbitrary reusable message.
For publicly verifiable signatures, use the documented counterparty: 'anyone' pattern and its corresponding verifier context. It changes key derivation; it is not an instruction to expose a private key. See the wallet API reference for that variant and hash-versus-data rules.
When a transaction needs signAction
Wallet-funded inputs can be signed by the wallet during createAction. An input that needs an app-specific unlocking script can produce signableTransaction; not every transaction or explicit input necessarily takes this path. Inspect the result instead of assuming one solely from the number of inputs.
The signable transaction is AtomicBEEF. Parse it with Transaction.fromAtomicBEEF, identify the custom input by its exact outpoint, and sign the actual returned template. Wallet-added funding inputs can change the index. This helper finalizes one custom input using an unlocking-script builder that your contract implementation supplies:
1import { Transaction } from '@bsv/sdk'
2import type { CreateActionResult, UnlockingScript, WalletInterface } from '@bsv/sdk'
3
4async function finishCustomInput(
5 wallet: Pick<WalletInterface, 'signAction'>,
6 prepared: CreateActionResult,
7 sourceTXID: string,
8 sourceOutputIndex: number,
9 buildUnlockingScript: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
10) {
11 const signable = prepared.signableTransaction
12 if (!signable) throw new Error('The wallet returned no custom-signing preparation')
13 const tx = Transaction.fromAtomicBEEF(signable.tx)
14 const inputIndex = tx.inputs.findIndex(input =>
15 (input.sourceTXID || input.sourceTransaction?.id('hex')) === sourceTXID &&
16 input.sourceOutputIndex === sourceOutputIndex
17 )
18 if (inputIndex < 0) throw new Error('Expected custom input is absent from the wallet template')
19 const unlockingScript = await buildUnlockingScript(tx, inputIndex)
20 return wallet.signAction({
21 reference: signable.reference,
22 spends: { [inputIndex]: { unlockingScript: unlockingScript.toHex() } }
23 })
24}The callback must implement the original locking conditions using the correct source value/script and derivation metadata. This is not a complete contract signer by itself. Review the template's intended outputs/amounts and ensure the source output is still unspent before signing. For multiple custom inputs, construct the appropriate entries together; do not finalize after supplying only one if others still need scripts. The wallet remains responsible for its own added inputs.
Interrupted preparations and submissions
An unsubmitted preparation, a signed transaction, broadcast acknowledgement and confirmation are different states. Preserve the wallet's reference and transaction identifiers securely, then inspect wallet action history after an interruption. A preparation reference is not a promise of signAction recovery on every wallet or across every new session. Use abortAction only for a preparation the wallet can acknowledge canceling. Never invent a fresh payment merely because the prior call timed out or signing state is uncertain.
Use the transactions guide, Metanet payment recovery, and maintained SDK source when implementing the complete lifecycle. None of the examples here automatically sends funds.
