UNPKG

@btc-vision/transaction

Version:

OPNet transaction library allows you to create and sign transactions for the OPNet network.

583 lines (479 loc) 19.1 kB
# Quick Start Guide Complete, runnable examples for the most common OPNet Transaction Library operations. ## Table of Contents - [Prerequisites](#prerequisites) - [1. Creating a Wallet from Mnemonic](#1-creating-a-wallet-from-mnemonic) - [2. Sending BTC (Funding Transaction)](#2-sending-btc-funding-transaction) - [3. Deploying a Contract](#3-deploying-a-contract) - [4. Calling a Contract Function](#4-calling-a-contract-function) - [5. Message Signing](#5-message-signing) ## Prerequisites Before running these examples, make sure you have the library installed: ```bash npm install @btc-vision/transaction @btc-vision/bitcoin ``` All examples target **regtest** for local development. Replace `networks.regtest` with `networks.bitcoin` or `networks.testnet` for other environments. ## 1. Creating a Wallet from Mnemonic Generate a new mnemonic phrase and derive a wallet with both classical (secp256k1) and quantum (ML-DSA) key pairs. ```typescript import { Mnemonic, MnemonicStrength, MLDSASecurityLevel, Wallet, } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; // --- Generate a new mnemonic --- const mnemonic = Mnemonic.generate( MnemonicStrength.MAXIMUM, // 24 words (256-bit entropy) '', // No BIP39 passphrase networks.regtest, // Network MLDSASecurityLevel.LEVEL2, // ML-DSA-44 (BIP360 recommended default) ); console.log('Mnemonic phrase:', mnemonic.phrase); // => "abandon abandon abandon ... about" // --- Derive the first wallet (account 0, index 0) --- const wallet: Wallet = mnemonic.derive(0); console.log('P2TR address:', wallet.p2tr); // bcrt1p... console.log('P2WPKH address:', wallet.p2wpkh); // bcrt1q... console.log('Legacy address:', wallet.legacy); // m/n... console.log('OPNet address:', wallet.address.toHex()); // 0x-prefixed hex (SHA-256 of ML-DSA pubkey) // --- Derive multiple wallets --- const wallets: Wallet[] = mnemonic.deriveMultiple( 5, // count 0, // startIndex 0, // account false, // isChange ); for (let i = 0; i < wallets.length; i++) { const w = wallets[i]!; console.log(`Wallet ${i}: ${w.p2tr}`); } // --- Load an existing mnemonic --- const existingPhrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const restored = new Mnemonic( existingPhrase, '', networks.regtest, MLDSASecurityLevel.LEVEL2, ); const restoredWallet = restored.derive(0); console.log('Restored P2TR:', restoredWallet.p2tr); // --- Clean up sensitive material --- mnemonic.zeroize(); wallet.zeroize(); ``` ### Wallet Properties Reference | Property | Type | Description | |----------|------|-------------| | `wallet.p2tr` | `string` | Taproot address (`bc1p...`) -- primary for OPNet | | `wallet.p2wpkh` | `string` | Native SegWit address (`bc1q...`) | | `wallet.legacy` | `string` | Legacy P2PKH address (`1...`) | | `wallet.segwitLegacy` | `string` | Wrapped SegWit P2SH address (`3...`) | | `wallet.keypair` | `UniversalSigner` | secp256k1 key pair for signing | | `wallet.mldsaKeypair` | `QuantumBIP32Interface` | ML-DSA key pair for quantum signing | | `wallet.address` | `Address` | 32-byte OPNet address | | `wallet.publicKey` | `Uint8Array` | Compressed secp256k1 public key (33 bytes) | | `wallet.quantumPublicKey` | `Uint8Array` | ML-DSA public key (1312 bytes for LEVEL2) | ## 2. Sending BTC (Funding Transaction) Create a simple BTC transfer using `TransactionFactory.createBTCTransfer`. This produces a single Bitcoin transaction. ```typescript import { Mnemonic, TransactionFactory, OPNetLimitedProvider, type UTXO, } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; // --- Setup --- const network = networks.regtest; const mnemonic = new Mnemonic( 'your twelve word mnemonic phrase goes here abandon abandon abandon abandon about', '', network, ); const wallet = mnemonic.derive(0); // --- Fetch UTXOs from an OPNet node --- const provider = new OPNetLimitedProvider('https://regtest.opnet.org'); const utxos: UTXO[] = await provider.fetchUTXO({ address: wallet.p2tr, minAmount: 10_000n, requestedAmount: 100_000n, }); // --- Build and sign the transfer --- const factory = new TransactionFactory(); const result = await factory.createBTCTransfer({ // Signer configuration signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, network: network, // Transaction details from: wallet.p2tr, to: 'bcrt1p...recipient_taproot_address...', amount: 50_000n, // 50,000 satoshis utxos: utxos, // Fee configuration feeRate: 2, // sat/vB priorityFee: 0n, // Additional priority fee (satoshis) gasSatFee: 0n, // OPNet gas fee (0 for simple transfers) }); console.log('Transaction hex:', result.tx); console.log('Estimated fees:', result.estimatedFees, 'satoshis'); console.log('Remaining UTXOs:', result.nextUTXOs.length); // --- Broadcast the transaction --- // Use provider.broadcastTransaction(result.tx) or your own RPC node // --- Clean up --- mnemonic.zeroize(); wallet.zeroize(); ``` ### Transfer Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `signer` | `Signer \| UniversalSigner` | Yes | secp256k1 key pair | | `mldsaSigner` | `QuantumBIP32Interface \| null` | Yes | ML-DSA key pair (or `null`) | | `network` | `Network` | Yes | Bitcoin network | | `from` | `string` | Yes | Sender P2TR address | | `to` | `string` | Yes | Recipient address | | `amount` | `bigint` | Yes | Amount in satoshis | | `utxos` | `UTXO[]` | Yes | Available UTXOs | | `feeRate` | `number` | Yes | Fee rate in sat/vB | | `priorityFee` | `bigint` | Yes | Priority fee in satoshis | | `gasSatFee` | `bigint` | Yes | OPNet gas fee in satoshis | ## 3. Deploying a Contract Deploy an OPNet smart contract using `TransactionFactory.signDeployment`. This produces two transactions: a funding TX and a deployment TX. ```typescript import { Mnemonic, TransactionFactory, ChallengeSolution, OPNetLimitedProvider, type UTXO, type RawChallenge, } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; import * as fs from 'fs'; // --- Setup --- const network = networks.regtest; const mnemonic = new Mnemonic( 'your twelve word mnemonic phrase goes here abandon abandon abandon abandon about', '', network, ); const wallet = mnemonic.derive(0); // --- Fetch UTXOs --- const provider = new OPNetLimitedProvider('https://regtest.opnet.org'); const utxos: UTXO[] = await provider.fetchUTXO({ address: wallet.p2tr, minAmount: 100_000n, requestedAmount: 500_000n, }); // --- Load contract bytecode --- const bytecode: Uint8Array = fs.readFileSync('./my-contract.wasm'); // --- Obtain a challenge solution from the OPNet network --- // The challenge is obtained from the OPNet API and proves the transaction // is valid for the current epoch. This is a simplified representation; // in practice, you fetch this from the OPNet node API. const rawChallenge: RawChallenge = { epochNumber: '1', mldsaPublicKey: toHex(wallet.quantumPublicKey), legacyPublicKey: toHex(wallet.address.originalPublicKey!), solution: '0x...challenge_solution_hex...', salt: '0x...salt_hex...', graffiti: '0x00', difficulty: 1, verification: { epochHash: '0x...epoch_hash...', epochRoot: '0x...epoch_root...', targetHash: '0x...target_hash...', targetChecksum: '0x...target_checksum...', startBlock: '0', endBlock: '100', proofs: [], }, }; const challenge = new ChallengeSolution(rawChallenge); // --- Build and sign the deployment --- const factory = new TransactionFactory(); const result = await factory.signDeployment({ // Signer configuration signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, network: network, // Deployment details from: wallet.p2tr, bytecode: bytecode, utxos: utxos, challenge: challenge, // Fee configuration feeRate: 2, priorityFee: 330n, gasSatFee: 330n, }); console.log('Funding TX:', result.transaction[0]); console.log('Deployment TX:', result.transaction[1]); console.log('Contract address:', result.contractAddress); console.log('Contract public key:', result.contractPubKey); console.log('Remaining UTXOs:', result.utxos.length); // Broadcast both transactions in order: // 1. Broadcast result.transaction[0] (funding) // 2. Broadcast result.transaction[1] (deployment) // --- Clean up --- mnemonic.zeroize(); wallet.zeroize(); ``` ### Deployment Result | Field | Type | Description | |-------|------|-------------| | `transaction` | `[string, string]` | `[fundingTxHex, deploymentTxHex]` | | `contractAddress` | `string` | The deployed contract's address | | `contractPubKey` | `string` | The contract's public key | | `challenge` | `RawChallenge` | The challenge used | | `utxos` | `UTXO[]` | Remaining UTXOs (change) | | `inputUtxos` | `UTXO[]` | UTXOs consumed as inputs | ## 4. Calling a Contract Function Interact with a deployed contract using `TransactionFactory.signInteraction`. Encode the function calldata with `BinaryWriter`, then sign and broadcast. ```typescript import { Mnemonic, TransactionFactory, BinaryWriter, ChallengeSolution, OPNetLimitedProvider, ABICoder, Address, type UTXO, type RawChallenge, } from '@btc-vision/transaction'; import { networks, fromHex } from '@btc-vision/bitcoin'; // --- Setup --- const network = networks.regtest; const mnemonic = new Mnemonic( 'your twelve word mnemonic phrase goes here abandon abandon abandon abandon about', '', network, ); const wallet = mnemonic.derive(0); const contractAddress = 'bcrt1p...the_contract_taproot_address...'; // --- Fetch UTXOs --- const provider = new OPNetLimitedProvider('https://regtest.opnet.org'); const utxos: UTXO[] = await provider.fetchUTXO({ address: wallet.p2tr, minAmount: 100_000n, requestedAmount: 500_000n, }); // --- Encode calldata --- // Example: calling a "transfer" function with (Address to, uint256 amount) const abiCoder = new ABICoder(); const selectorHex = abiCoder.encodeSelector('transfer'); // First 4 bytes of SHA-256("transfer") const selector = parseInt(selectorHex, 16); const calldata = new BinaryWriter(); calldata.writeSelector(selector); // Write the recipient address (32 bytes -- OPNet address) const recipientAddress = Address.fromString( '0x...recipient_mldsa_pubkey_hash_hex...', // ML-DSA public key hash '0x...recipient_legacy_pubkey_hex...', // Legacy public key ); calldata.writeAddress(recipientAddress); // Write the amount (uint256) calldata.writeU256(1_000_000_000n); // 1 billion smallest units // --- Obtain challenge from OPNet API --- const rawChallenge: RawChallenge = { epochNumber: '1', mldsaPublicKey: toHex(wallet.quantumPublicKey), legacyPublicKey: toHex(wallet.address.originalPublicKey!), solution: '0x...challenge_solution_hex...', salt: '0x...salt_hex...', graffiti: '0x00', difficulty: 1, verification: { epochHash: '0x...epoch_hash...', epochRoot: '0x...epoch_root...', targetHash: '0x...target_hash...', targetChecksum: '0x...target_checksum...', startBlock: '0', endBlock: '100', proofs: [], }, }; const challenge = new ChallengeSolution(rawChallenge); // --- Build and sign the interaction --- const factory = new TransactionFactory(); const result = await factory.signInteraction({ // Signer configuration signer: wallet.keypair, mldsaSigner: wallet.mldsaKeypair, network: network, // Interaction details from: wallet.p2tr, to: contractAddress, calldata: calldata.getBuffer(), utxos: utxos, challenge: challenge, // Fee configuration feeRate: 2, priorityFee: 330n, gasSatFee: 330n, }); console.log('Funding TX:', result.fundingTransaction); console.log('Interaction TX:', result.interactionTransaction); console.log('Estimated fees:', result.estimatedFees, 'satoshis'); console.log('Next UTXOs:', result.nextUTXOs.length); // Broadcast both transactions in order: // 1. Broadcast result.fundingTransaction // 2. Broadcast result.interactionTransaction // --- Clean up --- mnemonic.zeroize(); wallet.zeroize(); ``` ### Interaction Result | Field | Type | Description | |-------|------|-------------| | `fundingTransaction` | `string \| null` | Funding TX hex (null for P2WDA) | | `interactionTransaction` | `string` | Interaction TX hex | | `estimatedFees` | `bigint` | Total fees in satoshis | | `nextUTXOs` | `UTXO[]` | Remaining UTXOs (change) | | `fundingUTXOs` | `UTXO[]` | UTXOs used in the funding TX | | `fundingInputUtxos` | `UTXO[]` | Original input UTXOs | | `challenge` | `RawChallenge` | The challenge used | | `interactionAddress` | `string \| null` | One-time Taproot script address | | `compiledTargetScript` | `string \| null` | Compiled Tapscript hex | ### Transaction Flow ```mermaid sequenceDiagram participant App as Your Application participant BW as BinaryWriter participant TF as TransactionFactory participant BTC as Bitcoin Network App->>BW: Encode calldata (selector + args) App->>TF: signInteraction(params) Note over TF: 1. Estimate funding amount<br/>2. Build funding TX<br/>3. Build interaction TX<br/>4. Sign both TF-->>App: { fundingTransaction, interactionTransaction } App->>BTC: Broadcast funding TX App->>BTC: Broadcast interaction TX ``` ## 5. Message Signing Sign messages using both classical (Schnorr) and quantum (ML-DSA) signatures. The `Auto` methods automatically detect browser wallet extensions. ### Schnorr Signing (Backend) ```typescript import { Mnemonic, MessageSigner } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; const network = networks.regtest; const mnemonic = new Mnemonic( 'your twelve word mnemonic phrase goes here abandon abandon abandon abandon about', '', network, ); const wallet = mnemonic.derive(0); // --- Sign a message with Schnorr --- const message = 'Hello, OPNet!'; const signed = MessageSigner.signMessage(wallet.keypair, message); console.log('Signature:', toHex(signed.signature)); console.log('Message hash:', toHex(signed.message)); // --- Verify the signature --- const isValid = MessageSigner.verifySignature( wallet.publicKey, message, signed.signature, ); console.log('Schnorr signature valid:', isValid); // true // --- Tweaked Schnorr signing (Taproot-compatible) --- const tweakedSigned = MessageSigner.tweakAndSignMessage( wallet.keypair, message, network, ); const isTweakedValid = MessageSigner.tweakAndVerifySignature( wallet.publicKey, message, tweakedSigned.signature, ); console.log('Tweaked signature valid:', isTweakedValid); // true mnemonic.zeroize(); wallet.zeroize(); ``` ### ML-DSA Signing (Backend) ```typescript import { Mnemonic, MessageSigner } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; const network = networks.regtest; const mnemonic = new Mnemonic( 'your twelve word mnemonic phrase goes here abandon abandon abandon abandon about', '', network, ); const wallet = mnemonic.derive(0); // --- Sign with ML-DSA (quantum-resistant) --- const message = 'Quantum-resistant message'; const mldsaSigned = MessageSigner.signMLDSAMessage( wallet.mldsaKeypair, message, ); console.log('ML-DSA signature length:', mldsaSigned.signature.length, 'bytes'); console.log('ML-DSA public key length:', mldsaSigned.publicKey.length, 'bytes'); console.log('Security level:', mldsaSigned.securityLevel); // --- Verify ML-DSA signature --- const isQuantumValid = MessageSigner.verifyMLDSASignature( wallet.mldsaKeypair, message, mldsaSigned.signature, ); console.log('ML-DSA signature valid:', isQuantumValid); // true mnemonic.zeroize(); wallet.zeroize(); ``` ### Auto Signing (Browser + Backend) The `Auto` methods try the browser wallet extension (OP_WALLET) first, then fall back to the provided key pair. This allows the same code to work in both environments. ```typescript import { Mnemonic, MessageSigner } from '@btc-vision/transaction'; import { networks } from '@btc-vision/bitcoin'; const network = networks.regtest; // In a browser, the wallet keypair may be undefined (the extension handles signing). // In Node.js, you must provide the keypair. const wallet = /* your Wallet instance, or undefined in browser */ undefined; // --- Auto Schnorr signing --- // Browser: delegates to window.opnet.web3.signSchnorr() // Backend: signs with the provided keypair const schnorrResult = await MessageSigner.signMessageAuto( 'Sign this message', wallet?.keypair, // undefined in browser => uses OP_WALLET ); console.log('Schnorr signature:', schnorrResult.signature); // --- Auto tweaked Schnorr signing --- const tweakedResult = await MessageSigner.tweakAndSignMessageAuto( 'Sign this tweaked message', wallet?.keypair, network, // Required when signing with a local keypair ); console.log('Tweaked signature:', tweakedResult.signature); // --- Auto ML-DSA signing --- // Browser: delegates to window.opnet.web3.signMLDSAMessage() // Backend: signs with the provided ML-DSA keypair const mldsaResult = await MessageSigner.signMLDSAMessageAuto( 'Quantum-resistant auto-sign', wallet?.mldsaKeypair, // undefined in browser => uses OP_WALLET ); console.log('ML-DSA signature:', mldsaResult.signature); // --- Check if OP_WALLET is available --- if (MessageSigner.isOPWalletAvailable()) { console.log('OP_WALLET extension detected'); // Get ML-DSA public key directly from the wallet extension const pubKey = await MessageSigner.getMLDSAPublicKeyFromOPWallet(); if (pubKey) { console.log('OP_WALLET ML-DSA public key length:', pubKey.length, 'bytes'); } } ``` ### Signing Method Summary | Method | Signature Type | Auto-detects OP_WALLET | Use Case | |--------|---------------|------------------------|----------| | `signMessage` | Schnorr | No | Backend-only signing | | `tweakAndSignMessage` | Schnorr (tweaked) | No | Taproot-compatible backend signing | | `signMLDSAMessage` | ML-DSA | No | Backend-only quantum signing | | `signMessageAuto` | Schnorr | Yes | Universal (browser + backend) | | `tweakAndSignMessageAuto` | Schnorr (tweaked) | Yes | Universal Taproot signing | | `signMLDSAMessageAuto` | ML-DSA | Yes | Universal quantum signing | | `verifySignature` | Schnorr | No | Verify classical signatures | | `tweakAndVerifySignature` | Schnorr (tweaked) | No | Verify tweaked signatures | | `verifyMLDSASignature` | ML-DSA | No | Verify quantum signatures | --- **Previous:** [Architecture Overview](./overview.md) | **Up:** [Documentation Index](../README.md)