Skip to main content
This guide provides a complete implementation of a Bitcoin wallet for testing and development purposes.

What You’ll Accomplish

By the end of this step, you’ll have:
  • ✅ A complete Bitcoin wallet implementation for testing
  • ✅ Support for P2TR (Taproot) address generation
  • ✅ BIP322 message signing capabilities
  • ✅ Mnemonic seed phrase management
  • ✅ Integration with the Odin authentication flow

Why Use a Sample Wallet?

For development and testing purposes, having a programmatic Bitcoin wallet allows you to:
  • Test Authentication Flows: Quickly test your Odin integration without manual wallet interactions
  • Automated Testing: Create automated test suites for your authentication logic
  • Development Environment: Work in environments where external wallets may not be available
  • Educational Purposes: Understand how Bitcoin wallets work under the hood
This sample wallet is designed for development and testing only. Never use this code in production environments DYOR

Install Required Dependencies

First, install the additional dependencies needed for the Bitcoin wallet implementation:
npm install bip39 varuint-bitcoin @noble/secp256k1 @scure/bip32 @scure/bip39 @scure/btc-signer bip322-js
Add these to your package.json dependencies:
package.json
"dependencies": {
    "@dfinity/agent": "^2.4.1",
    "@dfinity/candid": "^2.4.1",
    "@dfinity/identity": "^2.4.1",
    "@dfinity/principal": "^2.4.1",
    "axios": "^1.6.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.3.0",
    "bip322-js": "^3.0.0",
    "bip39": "^3.1.0",
    "varuint-bitcoin": "^1.1.2",
    "@noble/secp256k1": "^1.7.1",
    "@scure/bip32": "^1.7.0",
    "@scure/bip39": "^1.6.0",
    "@scure/btc-signer": "^1.8.0"
  }

Sample Wallet Implementation

Here’s a complete Bitcoin wallet implementation that supports P2TR (Taproot) addresses and BIP322 message signing:
sample-wallet.ts
import * as bip32 from '@scure/bip32';
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import * as btc from '@scure/btc-signer';
import { hex } from '@scure/base';
import * as sha2 from '@noble/hashes/sha2';
import { encode } from 'varuint-bitcoin';

const NETWORK = btc.NETWORK;
const DEFAULT_DERIVATION_PATH = "m/86'/0'/0'/0/0";

export type SampleWallet = {
  address: string;
  publicKey: string;
  privateKey: string;
  mnemonic: string;
  derivationPath: string;
  signMessage: (message: string) => Promise<string>;
};

/**
 * BIP0322 message hashing
 */
function bip0322Hash(message: string) {
  const tag = 'BIP0322-signed-message';
  const tagHash = sha2.sha256(Buffer.from(tag));
  const result = sha2.sha256(Buffer.concat([tagHash, tagHash, Buffer.from(message)]));
  return hex.encode(result);
}

/**
 * Encode variable-length string for BIP322
 */
function encodeVarString(b: Uint8Array) {
  return Buffer.concat([encode(b.byteLength), b]);
}

/**
 * Sign a message using BIP322 format (adapted from odin/utils/bip322.ts)
 */
async function signBip322MessageWithKey({
  message,
  privateKeyHex,
}: {
  message: string;
  privateKeyHex: string;
}): Promise<string> {
  const secp256k1 = await import('@noble/secp256k1');

  const privateKeyBytes = hex.decode(privateKeyHex);
  const publicKeyBytes = secp256k1.schnorr.getPublicKey(privateKeyBytes);

  const txScript = btc.p2tr(publicKeyBytes, undefined, NETWORK);

  const inputHash = hex.decode('0000000000000000000000000000000000000000000000000000000000000000');
  const txVersion = 0;
  const inputIndex = 4294967295;
  const sequence = 0;
  const scriptSig = btc.Script.encode(['OP_0', hex.decode(bip0322Hash(message))]);

  const txToSpend = new btc.Transaction({
    allowUnknownOutputs: true,
    version: txVersion,
  });
  txToSpend.addOutput({
    amount: BigInt(0),
    script: txScript.script,
  });
  txToSpend.addInput({
    txid: inputHash,
    index: inputIndex,
    sequence,
    finalScriptSig: scriptSig,
  });

  const txToSign = new btc.Transaction({
    allowUnknownOutputs: true,
    version: txVersion,
  });
  txToSign.addInput({
    txid: txToSpend.id,
    index: 0,
    sequence,
    tapInternalKey: publicKeyBytes,
    witnessUtxo: {
      script: txScript.script,
      amount: BigInt(0),
    },
    redeemScript: Buffer.alloc(0),
  });
  txToSign.addOutput({
    script: btc.Script.encode(['RETURN']),
    amount: BigInt(0),
  });

  txToSign.sign(privateKeyBytes);
  txToSign.finalize();

  const firstInput = txToSign.getInput(0);
  if (firstInput.finalScriptWitness?.length) {
    const len = encode(firstInput.finalScriptWitness?.length);
    const result = Buffer.concat([
      len,
      ...firstInput.finalScriptWitness.map(w => encodeVarString(w)),
    ]);
    return result.toString('base64');
  }
  return '';
}

/**
 * Generates P2TR (Taproot) address and keypair from a derived HDKey node
 */
async function getP2TRFromNode(node: bip32.HDKey) {
  const secp256k1 = await import('@noble/secp256k1');

  if (!node.privateKey) {
    throw new Error('HDKey node does not contain a private key.');
  }

  const privateKeyHex = hex.encode(node.privateKey);
  const publicKey = secp256k1.schnorr.getPublicKey(privateKeyHex);

  const p2tr = btc.p2tr(publicKey, undefined, NETWORK);

  return {
    p2tr,
    keyPair: {
      publicKey: hex.encode(publicKey),
      privateKey: privateKeyHex,
    },
  };
}

/**
 * Creates a master HDKey and derives a child HDKey using the provided derivation path
 */
function createHDNodeFromMnemonic(mnemonic: string, derivationPath: string) {
  if (!validateMnemonic(mnemonic, wordlist)) {
    throw new Error('Invalid mnemonic');
  }

  const seed = mnemonicToSeedSync(mnemonic);
  const masterNode = bip32.HDKey.fromMasterSeed(seed);
  const derivedNode = masterNode.derive(derivationPath);

  return { derivedNode, seedHex: Buffer.from(seed).toString('hex') };
}

/**
 * Creates a sample wallet for testing and examples
 * If mnemonic is not provided, a new one is generated
 */
export async function createSampleWallet(
  mnemonic?: string,
  derivationPath?: string
): Promise<SampleWallet> {
  const phrase = mnemonic || generateMnemonic(wordlist);
  const path = derivationPath || DEFAULT_DERIVATION_PATH;

  const { derivedNode } = createHDNodeFromMnemonic(phrase, path);
  const { p2tr, keyPair } = await getP2TRFromNode(derivedNode);

  const wallet: SampleWallet = {
    address: p2tr.address!,
    publicKey: keyPair.publicKey,
    privateKey: keyPair.privateKey,
    mnemonic: phrase,
    derivationPath: path,
    signMessage: async (message: string) => {
      return signBip322MessageWithKey({
        message,
        privateKeyHex: keyPair.privateKey,
      });
    },
  };

  return wallet;
}

/**
 * Create a sample wallet from an existing mnemonic
 */
export async function createSampleWalletFromMnemonic(
  mnemonic: string,
  derivationPath?: string
): Promise<SampleWallet> {
  return createSampleWallet(mnemonic, derivationPath);
}

/**
 * Generate a new mnemonic phrase
 */
export function generateSampleMnemonic(): string {
  return generateMnemonic(wordlist);
}

/**
 * Validate a mnemonic phrase
 */
export function validateSampleMnemonic(mnemonic: string): boolean {
  return validateMnemonic(mnemonic, wordlist);
}

Understanding the Wallet Implementation

The sample wallet implementation includes several key components:
Implements the BIP322 standard for Bitcoin message signing, which is required for Taproot (P2TR) addresses. This is more secure than legacy ECDSA signing.
Uses BIP32 hierarchical deterministic (HD) wallet functionality to derive keys from a mnemonic seed phrase, following standard Bitcoin wallet patterns.
Creates Pay-to-Taproot (P2TR) addresses, which are the latest Bitcoin address format and provide enhanced privacy and functionality.
Supports generating, validating, and importing mnemonic seed phrases using the BIP39 standard.

Using the Sample Wallet

Here are some examples of how to use the sample wallet:
  • Generate New Wallet
  • Import from Mnemonic
  • Sign Messages
import { createSampleWallet } from './sample-wallet';

// Generate a new wallet with random mnemonic
const wallet = await createSampleWallet();

console.log({
  address: wallet.address,
  publicKey: wallet.publicKey,
  mnemonic: wallet.mnemonic
});

Security Best Practices

Development vs Production
  • Never use this sample wallet in production: The private keys are generated programmatically and may not have sufficient entropy
  • Secure mnemonic storage: In production, use hardware wallets or secure key management systems
  • Key rotation: Regularly rotate keys and addresses in production environments
  • Loss funds: Do not send real funds to the generated address, you might lose them.

Wallet Integration Summary

You now have a complete Bitcoin wallet implementation that:
  • ✅ Generates secure P2TR (Taproot) addresses
  • ✅ Supports BIP322 message signing
  • ✅ Uses industry-standard HD wallet derivation
  • ✅ Integrates seamlessly with Odin authentication
  • ✅ Provides comprehensive testing capabilities

Back to Welcome

Return to the main documentation to explore advanced features and API references
I