Skip to content
create-bsv-app

Wallet recipes

Encrypt user data, message another identity, sign things anyone can verify, and derive per-app keys. Four BRC-100 calls you can use the moment a wallet connects.

Login and signed requests are just the start. The connected wallet from useWallet() is a full BRC-100 wallet, so your app can ask it for cryptography without ever touching a key. Depending on the protocol's security level, the wallet may ask the user to approve the first call.

Every recipe on this page was run against real wallet instances (ProtoWallet from @bsv/sdk, which implements the same calls) with the SDK version the scaffold installs.

tsx
const { wallet } = useWallet() // WalletInterface from @bsv/sdk, null until connected

Protocol IDs in 30 seconds#

Every call names a protocol and a key ID. The wallet derives a fresh key from them (BRC-43), so your app's keys never collide with another app's.

ts
const protocolID: [1, string] = [1, 'my app notes'] // [security level, name]
const keyID = 'note-1'                             // any string, per item or per purpose
Level Wallet asks the user Use for
0 never low-stakes, high-frequency operations
1 once per app most app features
2 once per app and counterparty messages and signatures involving other identities

Names use lowercase letters, numbers and spaces. Pick one per feature and never reuse it for something else.

Encrypt data only the user can read#

Store private notes, drafts or settings on your server without being able to read them. Use counterparty 'self':

client/src/notes.ts
import { import UtilsUtils, type WalletInterface } from '@bsv/sdk'

const 
const notes: {
    protocolID: [1, string];
    keyID: string;
}
notes
= { protocolID: [1, string]protocolID: [1, 'my app notes'] as [1, string], keyID: stringkeyID: 'note-1' }
export async function function seal(wallet: WalletInterface, text: string): Promise<number[]>seal (wallet: WalletInterfacewallet: WalletInterface, text: stringtext: string): interface Promise<T>Promise<number[]> { const { const ciphertext: number[]ciphertext } = await wallet: WalletInterfacewallet.WalletInterface.encrypt: (args: WalletEncryptArgs, originator?: OriginatorDomainNameStringUnder250Bytes) => Promise<WalletEncryptResult>encrypt({ ...
const notes: {
    protocolID: [1, string];
    keyID: string;
}
notes
, WalletEncryptArgs.plaintext: number[]plaintext: import UtilsUtils.const toArray: (msg: any, enc?: "hex" | "utf8" | "base64") => any[]toArray(text: stringtext, 'utf8'), WalletEncryptionArgs.counterparty?: string | undefinedcounterparty: 'self' })
return const ciphertext: number[]ciphertext // store this; it's useless without the user's wallet } export async function function unseal(wallet: WalletInterface, ciphertext: number[]): Promise<string>unseal (wallet: WalletInterfacewallet: WalletInterface, ciphertext: number[]ciphertext: number[]): interface Promise<T>Promise<string> { const { const plaintext: number[]plaintext } = await wallet: WalletInterfacewallet.WalletInterface.decrypt: (args: WalletDecryptArgs, originator?: OriginatorDomainNameStringUnder250Bytes) => Promise<WalletDecryptResult>decrypt({ ...
const notes: {
    protocolID: [1, string];
    keyID: string;
}
notes
, WalletDecryptArgs.ciphertext: number[]ciphertext, WalletEncryptionArgs.counterparty?: string | undefinedcounterparty: 'self' })
return import UtilsUtils.const toUTF8: (arr: number[] | Uint8Array) => stringtoUTF8(const plaintext: number[]plaintext) }

Send a message only one identity can read#

Encrypt to another user's identity key. Only their wallet can open it, and it proves it came from you:

ts
const 
const dm: {
    protocolID: [2, string];
    keyID: string;
}
dm
= { protocolID: [2, string]protocolID: [2, 'my app messages'] as [2, string], keyID: stringkeyID: 'msg-1' }
// Alice, to Bob: const { const ciphertext: number[]ciphertext } = await const aliceWallet: WalletInterfacealiceWallet.WalletInterface.encrypt: (args: WalletEncryptArgs, originator?: OriginatorDomainNameStringUnder250Bytes) => Promise<WalletEncryptResult>encrypt({ ...
const dm: {
    protocolID: [2, string];
    keyID: string;
}
dm
, WalletEncryptArgs.plaintext: number[]plaintext: import UtilsUtils.const toArray: (msg: any, enc?: "hex" | "utf8" | "base64") => any[]toArray('hi bob', 'utf8'), WalletEncryptionArgs.counterparty?: string | undefinedcounterparty: const bobIdentityKey: stringbobIdentityKey })
// Bob, from Alice: const { const plaintext: number[]plaintext } = await const bobWallet: WalletInterfacebobWallet.WalletInterface.decrypt: (args: WalletDecryptArgs, originator?: OriginatorDomainNameStringUnder250Bytes) => Promise<WalletDecryptResult>decrypt({ ...
const dm: {
    protocolID: [2, string];
    keyID: string;
}
dm
, WalletDecryptArgs.ciphertext: number[]ciphertext, WalletEncryptionArgs.counterparty?: string | undefinedcounterparty: const aliceIdentityKey: stringaliceIdentityKey })

The key is derived from both identities, so the pair is what matters. Use the same protocolID and keyID on both sides, and swap the counterparty.

Sign something anyone can verify#

Public votes, attestations, "I wrote this" stamps. Sign with counterparty 'anyone', and verify with an 'anyone' wallet, which needs no secret at all:

ts
import { class ProtoWalletProtoWallet, import UtilsUtils } from '@bsv/sdk'

const 
const votes: {
    protocolID: [2, string];
    keyID: string;
}
votes
= { protocolID: [2, string]protocolID: [2, 'my app votes'] as [2, string], keyID: stringkeyID: 'poll-42' }
const const data: any[]data = import UtilsUtils.const toArray: (msg: any, enc?: "hex" | "utf8" | "base64") => any[]toArray(var JSON: JSONJSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)stringify({ vote: stringvote: 'yes', poll: numberpoll: 42 }), 'utf8') // In the browser, with the user's wallet: const { const signature: number[]signature } = await const wallet: WalletInterfacewallet.WalletInterface.createSignature: (args: CreateSignatureArgs, originator?: OriginatorDomainNameStringUnder250Bytes) => Promise<CreateSignatureResult>createSignature({ ...
const votes: {
    protocolID: [2, string];
    keyID: string;
}
votes
, CreateSignatureArgs.data?: number[] | undefineddata, WalletEncryptionArgs.counterparty?: string | undefinedcounterparty: 'anyone' })
// Anywhere, later (your server, another app, a script): const const verifier: ProtoWalletverifier = new new ProtoWallet(rootKeyOrKeyDeriver?: PrivateKey | "anyone" | KeyDeriverApi): ProtoWalletProtoWallet('anyone') const const ok: booleanok = await const verifier: ProtoWalletverifier .ProtoWallet.verifySignature(args: VerifySignatureArgs): Promise<VerifySignatureResult>verifySignature({ ...
const votes: {
    protocolID: [2, string];
    keyID: string;
}
votes
, VerifySignatureArgs.data?: number[] | undefineddata, VerifySignatureArgs.signature: number[]signature, WalletEncryptionArgs.counterparty?: string | undefinedcounterparty: const signerIdentityKey: stringsignerIdentityKey })
.Promise<VerifySignatureResult>.then<boolean, boolean>(onfulfilled?: ((value: VerifySignatureResult) => boolean | PromiseLike<boolean>) | null | undefined, onrejected?: ((reason: any) => boolean | PromiseLike<boolean>) | null | undefined): Promise<boolean>then(() => true, () => false)

A public key per app or feature#

Show users a key for your app without exposing (or correlating) their identity key:

ts
const { publicKey } = await wallet.getPublicKey({ protocolID: [1, 'my app'], keyID: '1', counterparty: 'self' })

Different protocol or key ID, different key. Your app sees a stable key per user, and two apps can't link their users by it.

Pay someone#

Creating transactions needs a funded wallet, so start on testnet. The shape of the call is on the BSV primer.

Test these without a wallet#

Every call above works on a ProtoWallet with a random key, which is how this page was verified. Copy the pattern from Testing without a wallet:

ts
import { PrivateKey, ProtoWallet } from '@bsv/sdk'
const alice = new ProtoWallet(PrivateKey.fromRandom())