From 7567b009cf052dac5c1efaea1f26a021ee2bb9af Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 14 Jan 2026 14:46:55 +0100 Subject: [PATCH] feat(wasm-utxo): fix ordinal inscriptions protocol compatibility Updates the inscription envelope format to match the ordinals protocol specification. The content type tag is now correctly encoded as a byte value (0x01) instead of using OP_1. Also switches to using a NUMS point for the taproot internal key to ensure compatibility with other implementations. The signRevealTransaction function now returns raw transaction bytes instead of a PSBT for simpler broadcast. Adds comprehensive tests to verify compatibility with utxo-lib's ordinals implementation. Issue: BTC-2936 Co-authored-by: llm-git --- packages/wasm-utxo/js/inscriptions.ts | 4 +- .../wasm-utxo/src/inscriptions/envelope.rs | 13 +- packages/wasm-utxo/src/inscriptions/reveal.rs | 178 ++++++++-- packages/wasm-utxo/src/wasm/inscriptions.rs | 9 +- packages/wasm-utxo/test/inscriptions.ts | 308 ++++++++++++++++++ 5 files changed, 477 insertions(+), 35 deletions(-) create mode 100644 packages/wasm-utxo/test/inscriptions.ts diff --git a/packages/wasm-utxo/js/inscriptions.ts b/packages/wasm-utxo/js/inscriptions.ts index 0f23c1f..f54bae0 100644 --- a/packages/wasm-utxo/js/inscriptions.ts +++ b/packages/wasm-utxo/js/inscriptions.ts @@ -89,11 +89,11 @@ export function createInscriptionRevealData( * @param commitOutputScript - The commit output script (P2TR) * @param recipientOutputScript - Where to send the inscription (output script) * @param outputValueSats - Value in satoshis for the inscription output - * @returns The signed PSBT as bytes + * @returns The signed transaction as bytes (ready to broadcast) * * @example * ```typescript - * const psbtBytes = signRevealTransaction( + * const txBytes = signRevealTransaction( * privateKey, * revealData.tapLeafScript, * commitTx, diff --git a/packages/wasm-utxo/src/inscriptions/envelope.rs b/packages/wasm-utxo/src/inscriptions/envelope.rs index c2bf3db..7664dfb 100644 --- a/packages/wasm-utxo/src/inscriptions/envelope.rs +++ b/packages/wasm-utxo/src/inscriptions/envelope.rs @@ -3,9 +3,7 @@ //! Creates the taproot script containing the inscription data following //! the Ordinals protocol format. -use miniscript::bitcoin::opcodes::all::{ - OP_CHECKSIG, OP_ENDIF, OP_IF, OP_PUSHBYTES_0, OP_PUSHNUM_1, -}; +use miniscript::bitcoin::opcodes::all::{OP_CHECKSIG, OP_ENDIF, OP_IF, OP_PUSHBYTES_0}; use miniscript::bitcoin::opcodes::OP_FALSE; use miniscript::bitcoin::script::{Builder, PushBytesBuf}; use miniscript::bitcoin::secp256k1::XOnlyPublicKey; @@ -56,11 +54,10 @@ pub fn build_inscription_script( let ord_bytes = PushBytesBuf::try_from(b"ord".to_vec()).expect("ord is 3 bytes"); builder = builder.push_slice(ord_bytes); - // OP_1 OP_1 - content type tag - // Note: The ordinals decoder has a quirk where it expects two separate OP_1s - // instead of a single OP_PUSHNUM_1 - builder = builder.push_opcode(OP_PUSHNUM_1); - builder = builder.push_opcode(OP_PUSHNUM_1); + // Content type tag: push byte 0x01 (tag number for content-type) + // Encoded as PUSHBYTES_1 0x01 (two bytes: 01 01) + let tag_content_type = PushBytesBuf::try_from(vec![0x01]).expect("single byte"); + builder = builder.push_slice(tag_content_type); // let content_type_bytes = diff --git a/packages/wasm-utxo/src/inscriptions/reveal.rs b/packages/wasm-utxo/src/inscriptions/reveal.rs index 4c4d969..4628a40 100644 --- a/packages/wasm-utxo/src/inscriptions/reveal.rs +++ b/packages/wasm-utxo/src/inscriptions/reveal.rs @@ -5,14 +5,35 @@ use super::envelope::build_inscription_script; use crate::error::WasmUtxoError; +use miniscript::bitcoin::consensus::Encodable; +use miniscript::bitcoin::hashes::sha256; use miniscript::bitcoin::hashes::Hash; use miniscript::bitcoin::key::UntweakedKeypair; -use miniscript::bitcoin::psbt::Psbt; -use miniscript::bitcoin::secp256k1::{Secp256k1, SecretKey, XOnlyPublicKey}; +use miniscript::bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey, XOnlyPublicKey}; use miniscript::bitcoin::sighash::{Prevouts, SighashCache}; use miniscript::bitcoin::taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder}; use miniscript::bitcoin::{ScriptBuf, Transaction, TxOut, Witness}; +/// NUMS point (Nothing Up My Sleeve) - a secp256k1 x coordinate with unknown discrete logarithm. +/// Equal to SHA256(uncompressedDER(SECP256K1_GENERATOR_POINT)). +/// Used as internal key when key-path spending is disabled. +/// This matches utxo-lib's implementation for compatibility. +fn nums_point() -> XOnlyPublicKey { + let secp = Secp256k1::new(); + // Generator point G is the public key for secret key = 1 + let one = SecretKey::from_slice(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, + ]) + .expect("valid secret key"); + let generator = PublicKey::from_secret_key(&secp, &one); + // Uncompressed format: 04 || x || y (65 bytes) + let uncompressed = generator.serialize_uncompressed(); + // SHA256 hash of uncompressed generator point + let hash = sha256::Hash::hash(&uncompressed); + XOnlyPublicKey::from_slice(hash.as_ref()).expect("valid x-only pubkey") +} + /// Taproot leaf script data needed for spending #[derive(Debug, Clone)] pub struct TapLeafScript { @@ -33,32 +54,32 @@ pub struct InscriptionRevealData { /// Create inscription reveal data including the commit output script and tap leaf script /// /// # Arguments -/// * `internal_key` - The x-only public key (32 bytes) +/// * `script_pubkey` - The x-only public key for the OP_CHECKSIG in the inscription script /// * `content_type` - MIME type of the inscription /// * `data` - The inscription data /// /// # Returns /// `InscriptionRevealData` containing the commit output script, estimated vsize, and tap leaf script pub fn create_inscription_reveal_data( - internal_key: &XOnlyPublicKey, + script_pubkey: &XOnlyPublicKey, content_type: &str, data: &[u8], ) -> Result { let secp = Secp256k1::new(); - // Build the inscription script - let script = build_inscription_script(internal_key, content_type, data); + // Build the inscription script (pubkey is used for OP_CHECKSIG inside the script) + let script = build_inscription_script(script_pubkey, content_type, data); // Create taproot tree with the inscription script as the only leaf let builder = TaprootBuilder::new() .add_leaf(0, script.clone()) .map_err(|e| WasmUtxoError::new(&format!("Failed to build taproot tree: {:?}", e)))?; - // Finalize the taproot spend info - // Use an unspendable internal key (all zeros XOR'd with script root) - // For simplicity, we use the provided internal_key + // Use NUMS point as internal key (disables key-path spending) + // This matches utxo-lib's behavior for compatibility + let internal_key = nums_point(); let spend_info = builder - .finalize(&secp, *internal_key) + .finalize(&secp, internal_key) .map_err(|e| WasmUtxoError::new(&format!("Failed to finalize taproot: {:?}", e)))?; // Get the output script (network-agnostic) @@ -94,7 +115,7 @@ pub fn create_inscription_reveal_data( /// * `output_value_sats` - Value in satoshis for the inscription output /// /// # Returns -/// A signed PSBT containing the reveal transaction +/// The signed reveal transaction as bytes (ready to broadcast) pub fn sign_reveal_transaction( private_key: &SecretKey, tap_leaf_script: &TapLeafScript, @@ -102,7 +123,7 @@ pub fn sign_reveal_transaction( commit_output_script: &[u8], recipient_output_script: &[u8], output_value_sats: u64, -) -> Result { +) -> Result, WasmUtxoError> { let secp = Secp256k1::new(); // Convert output scripts @@ -187,14 +208,13 @@ pub fn sign_reveal_transaction( witness.push(control_block.serialize()); reveal_tx.input[0].witness = witness; - // Create PSBT from finalized transaction - let psbt = Psbt::from_unsigned_tx(reveal_tx.clone()) - .map_err(|e| WasmUtxoError::new(&format!("Failed to create PSBT: {}", e)))?; + // Serialize the signed transaction + let mut tx_bytes = Vec::new(); + reveal_tx + .consensus_encode(&mut tx_bytes) + .map_err(|e| WasmUtxoError::new(&format!("Failed to serialize transaction: {}", e)))?; - // Note: The PSBT is created from the signed transaction for compatibility - // with the expected return type. In practice, this is already finalized. - - Ok(psbt) + Ok(tx_bytes) } /// Estimate the virtual size of a reveal transaction @@ -224,6 +244,7 @@ fn estimate_reveal_vsize(script: &ScriptBuf, control_block: &ControlBlock) -> us #[cfg(test)] mod tests { use super::*; + use miniscript::bitcoin::hashes::hex::FromHex; fn test_keypair() -> (SecretKey, XOnlyPublicKey) { let secp = Secp256k1::new(); @@ -247,4 +268,123 @@ mod tests { assert!(!data.tap_leaf_script.script.is_empty()); assert!(!data.tap_leaf_script.control_block.is_empty()); } + + /// Test with the same x-only pubkey as utxo-ord test + /// Expected output script: 5120dc8b12eec336e7215fd1213acf66fb0d5dd962813c0616988a12c08493831109 + /// Expected address: tb1pmj939mkrxmnjzh73yyav7ehmp4wajc5p8srpdxy2ztqgfyurzyys4sg9zx + #[test] + fn test_utxo_ord_fixture_short_data() { + // Same x-only pubkey as utxo-ord test + let xonly_hex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3"; + let xonly_bytes = Vec::::from_hex(xonly_hex).unwrap(); + let pubkey = XOnlyPublicKey::from_slice(&xonly_bytes).unwrap(); + + let inscription_data = b"Never Gonna Give You Up"; + let result = create_inscription_reveal_data(&pubkey, "text/plain", inscription_data); + + assert!(result.is_ok()); + let data = result.unwrap(); + + // Log the actual output for debugging + let output_hex = hex::encode(&data.output_script); + println!("X-only pubkey: {}", xonly_hex); + println!( + "Inscription data: {:?}", + String::from_utf8_lossy(inscription_data) + ); + println!("Output script (actual): {}", output_hex); + println!("Output script (expected): 5120dc8b12eec336e7215fd1213acf66fb0d5dd962813c0616988a12c08493831109"); + + // Log the tap leaf script for debugging + println!( + "Tap leaf script hex: {}", + hex::encode(&data.tap_leaf_script.script) + ); + println!( + "Control block hex: {}", + hex::encode(&data.tap_leaf_script.control_block) + ); + + // Basic structure checks + assert_eq!(data.output_script.len(), 34); + assert_eq!(data.output_script[0], 0x51); // OP_1 + assert_eq!(data.output_script[1], 0x20); // PUSH32 + + // Assert byte-exact match with utxo-ord + let expected_hex = "5120dc8b12eec336e7215fd1213acf66fb0d5dd962813c0616988a12c08493831109"; + assert_eq!( + output_hex, expected_hex, + "Output script should match utxo-ord fixture" + ); + } + + /// Test with large data (>520 bytes) - same as utxo-ord test + /// Expected output script: 5120ec90ba87f3e7c5462eb2173afdc50e00cea6fc69166677171d70f45dfb3a31b8 + /// Expected address: tb1pajgt4plnulz5vt4jzua0m3gwqr82dlrfzen8w9cawr69m7e6xxuq7dzypl + #[test] + fn test_utxo_ord_fixture_large_data() { + let xonly_hex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3"; + let xonly_bytes = Vec::::from_hex(xonly_hex).unwrap(); + let pubkey = XOnlyPublicKey::from_slice(&xonly_bytes).unwrap(); + + // "Never Gonna Let You Down" repeated 100 times + let base = b"Never Gonna Let You Down"; + let inscription_data: Vec = base + .iter() + .cycle() + .take(base.len() * 100) + .copied() + .collect(); + + let result = create_inscription_reveal_data(&pubkey, "text/plain", &inscription_data); + + assert!(result.is_ok()); + let data = result.unwrap(); + + let output_hex = hex::encode(&data.output_script); + println!("Output script (actual): {}", output_hex); + println!("Output script (expected): 5120ec90ba87f3e7c5462eb2173afdc50e00cea6fc69166677171d70f45dfb3a31b8"); + + assert_eq!(data.output_script.len(), 34); + assert_eq!(data.output_script[0], 0x51); + assert_eq!(data.output_script[1], 0x20); + + // Assert byte-exact match with utxo-ord + let expected_hex = "5120ec90ba87f3e7c5462eb2173afdc50e00cea6fc69166677171d70f45dfb3a31b8"; + assert_eq!( + output_hex, expected_hex, + "Output script should match utxo-ord fixture" + ); + } + + /// Debug test to understand taproot key tweaking + #[test] + fn test_taproot_tweak_details() { + let xonly_hex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3"; + let xonly_bytes = Vec::::from_hex(xonly_hex).unwrap(); + let internal_key = XOnlyPublicKey::from_slice(&xonly_bytes).unwrap(); + + println!("Internal key: {}", internal_key); + + let secp = Secp256k1::new(); + let script = + build_inscription_script(&internal_key, "text/plain", b"Never Gonna Give You Up"); + + println!("Inscription script hex: {}", hex::encode(script.as_bytes())); + println!("Inscription script len: {}", script.len()); + + // Build taproot tree + let builder = TaprootBuilder::new().add_leaf(0, script.clone()).unwrap(); + + let spend_info = builder.finalize(&secp, internal_key).unwrap(); + + println!("Output key (tweaked): {}", spend_info.output_key()); + println!( + "Merkle root: {:?}", + spend_info.merkle_root().map(|r| r.to_string()) + ); + + let output_script = ScriptBuf::new_p2tr_tweaked(spend_info.output_key()); + println!("Output script: {}", hex::encode(output_script.as_bytes())); + } } diff --git a/packages/wasm-utxo/src/wasm/inscriptions.rs b/packages/wasm-utxo/src/wasm/inscriptions.rs index fa23969..f276d48 100644 --- a/packages/wasm-utxo/src/wasm/inscriptions.rs +++ b/packages/wasm-utxo/src/wasm/inscriptions.rs @@ -85,17 +85,14 @@ impl InscriptionsNamespace { // Parse the tap leaf script from JS using TryFromJsValue trait let tap_leaf = TapLeafScript::try_from_js_value(&tap_leaf_script)?; - // Sign the reveal transaction - let psbt = sign_reveal_impl( + // Sign the reveal transaction and return bytes + sign_reveal_impl( &secret_key, &tap_leaf, &commit_tx.tx, commit_output_script, recipient_output_script, output_value_sats, - )?; - - // Serialize to bytes - Ok(psbt.serialize()) + ) } } diff --git a/packages/wasm-utxo/test/inscriptions.ts b/packages/wasm-utxo/test/inscriptions.ts new file mode 100644 index 0000000..a1b00b2 --- /dev/null +++ b/packages/wasm-utxo/test/inscriptions.ts @@ -0,0 +1,308 @@ +import * as assert from "assert"; +import * as utxolib from "@bitgo/utxo-lib"; +import { ECPair } from "../js/ecpair.js"; +import { Transaction } from "../js/transaction.js"; +import { address, inscriptions } from "../js/index.js"; + +describe("inscriptions (wasm-utxo)", () => { + const contentType = "text/plain"; + + // Test key (same x-only pubkey as utxo-ord test) + const xOnlyPubkeyHex = "af455f4989d122e9185f8c351dbaecd13adca3eef8a9d38ef8ffed6867e342e3"; + + // Create a keypair from a deterministic private key for testing + const testPrivateKey = Buffer.from( + "0000000000000000000000000000000000000000000000000000000000000001", + "hex", + ); + + describe("createInscriptionRevealData", () => { + it("should generate an inscription output script", () => { + const inscriptionData = Buffer.from("Never Gonna Give You Up", "ascii"); + + // Create ECPair from x-only pubkey (need to add parity byte for compressed format) + const compressedPubkey = Buffer.concat([ + Buffer.from([0x02]), + Buffer.from(xOnlyPubkeyHex, "hex"), + ]); + const ecpair = ECPair.fromPublicKey(compressedPubkey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + // P2TR output scripts are 34 bytes: OP_1 (0x51) + PUSH32 (0x20) + 32-byte tweaked key + assert.strictEqual(revealData.outputScript.length, 34); + assert.strictEqual(revealData.outputScript[0], 0x51); // OP_1 + assert.strictEqual(revealData.outputScript[1], 0x20); // PUSH32 + + // Convert to address for testnet + const commitAddress = address.fromOutputScriptWithCoin(revealData.outputScript, "tbtc"); + assert.ok(commitAddress.startsWith("tb1p")); // Taproot address + + // Verify expected address matches utxo-ord test + assert.strictEqual( + commitAddress, + "tb1pmj939mkrxmnjzh73yyav7ehmp4wajc5p8srpdxy2ztqgfyurzyys4sg9zx", + ); + }); + + it("should generate an inscription output script when data length is > 520", () => { + const inscriptionData = Buffer.from("Never Gonna Let You Down".repeat(100), "ascii"); + + // Create ECPair from x-only pubkey + const compressedPubkey = Buffer.concat([ + Buffer.from([0x02]), + Buffer.from(xOnlyPubkeyHex, "hex"), + ]); + const ecpair = ECPair.fromPublicKey(compressedPubkey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + // Should still produce valid P2TR output + assert.strictEqual(revealData.outputScript.length, 34); + assert.strictEqual(revealData.outputScript[0], 0x51); + assert.strictEqual(revealData.outputScript[1], 0x20); + + // Convert to address for testnet + const commitAddress = address.fromOutputScriptWithCoin(revealData.outputScript, "tbtc"); + + // Verify expected address matches utxo-ord test + assert.strictEqual( + commitAddress, + "tb1pajgt4plnulz5vt4jzua0m3gwqr82dlrfzen8w9cawr69m7e6xxuq7dzypl", + ); + }); + + it("should return valid tap leaf script data", () => { + const inscriptionData = Buffer.from("Hello Ordinals", "ascii"); + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + // Validate tap leaf script structure + assert.ok(revealData.tapLeafScript); + assert.strictEqual(revealData.tapLeafScript.leafVersion, 0xc0); // TapScript + assert.ok(revealData.tapLeafScript.script instanceof Uint8Array); + assert.ok(revealData.tapLeafScript.script.length > 0); + assert.ok(revealData.tapLeafScript.controlBlock instanceof Uint8Array); + assert.ok(revealData.tapLeafScript.controlBlock.length > 0); + }); + + it("should return a reasonable vsize estimate", () => { + const inscriptionData = Buffer.from("Hello Ordinals", "ascii"); + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + // vsize should be reasonable (at least 100 vbytes for a simple inscription) + assert.ok(revealData.revealTransactionVSize > 100); + // But not too large for small data + assert.ok(revealData.revealTransactionVSize < 500); + }); + + it("should work with ECPair containing private key", () => { + const inscriptionData = Buffer.from("Test", "ascii"); + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + + // Should not throw - ECPair with private key also has public key + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + assert.ok(revealData.outputScript.length === 34); + }); + }); + + describe("signRevealTransaction", () => { + // Create a mock commit transaction with a P2TR output + function createMockCommitTx(commitOutputScript: Uint8Array): Transaction { + const psbt = new utxolib.Psbt({ network: utxolib.networks.testnet }); + + // Add a dummy input + psbt.addInput({ + hash: Buffer.alloc(32), // dummy txid + index: 0, + witnessUtxo: { + script: Buffer.from(commitOutputScript), + value: BigInt(100_000), + }, + }); + + // Add the commit output + psbt.addOutput({ + script: Buffer.from(commitOutputScript), + value: BigInt(42), + }); + + // Get the unsigned transaction + const txBytes = psbt.data.globalMap.unsignedTx.toBuffer(); + return Transaction.fromBytes(txBytes); + } + + it("should sign a reveal transaction", () => { + const inscriptionData = Buffer.from("And Desert You", "ascii"); + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + // Create a mock commit transaction + const commitTx = createMockCommitTx(revealData.outputScript); + + // Create recipient output script (P2WPKH for simplicity) + // OP_0 OP_PUSH20 <20-byte hash> + const recipientOutputScript = Buffer.alloc(22); + recipientOutputScript[0] = 0x00; // OP_0 + recipientOutputScript[1] = 0x14; // PUSH20 + // Rest is zeros (mock recipient) + + // Sign the reveal transaction + const txBytes = inscriptions.signRevealTransaction( + ecpair, + revealData.tapLeafScript, + commitTx, + revealData.outputScript, + recipientOutputScript, + 10000n, // 10,000 sats output + ); + + // Signed transaction should be non-empty + assert.ok(txBytes instanceof Uint8Array); + assert.ok(txBytes.length > 0); + + // Segwit transaction marker/flag: version(4) + marker(0x00) + flag(0x01) + // Version should be 2 (little-endian) + assert.strictEqual(txBytes[0], 0x02); + assert.strictEqual(txBytes[1], 0x00); + assert.strictEqual(txBytes[2], 0x00); + assert.strictEqual(txBytes[3], 0x00); + // Segwit marker and flag + assert.strictEqual(txBytes[4], 0x00); // marker + assert.strictEqual(txBytes[5], 0x01); // flag + }); + + it("should fail when commit output not found", () => { + const inscriptionData = Buffer.from("Test", "ascii"); + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + // Create commit tx with WRONG output script + const wrongOutputScript = Buffer.alloc(34); + wrongOutputScript[0] = 0x51; + wrongOutputScript[1] = 0x20; + // Rest is zeros (different from revealData.outputScript) + + const commitTx = createMockCommitTx(wrongOutputScript); + + const recipientOutputScript = Buffer.alloc(22); + recipientOutputScript[0] = 0x00; + recipientOutputScript[1] = 0x14; + + // Should throw because commit output script doesn't match + assert.throws(() => { + inscriptions.signRevealTransaction( + ecpair, + revealData.tapLeafScript, + commitTx, + revealData.outputScript, // Looking for this script + recipientOutputScript, + 10000n, + ); + }, /Commit output not found/); + }); + + it("should fail without private key", () => { + const inscriptionData = Buffer.from("Test", "ascii"); + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + const publicOnlyEcpair = ECPair.fromPublicKey(ecpair.publicKey); + + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + inscriptionData, + ); + + const commitTx = createMockCommitTx(revealData.outputScript); + const recipientOutputScript = Buffer.alloc(22); + recipientOutputScript[0] = 0x00; + recipientOutputScript[1] = 0x14; + + // Should throw because we need private key for signing + assert.throws(() => { + inscriptions.signRevealTransaction( + publicOnlyEcpair, + revealData.tapLeafScript, + commitTx, + revealData.outputScript, + recipientOutputScript, + 10000n, + ); + }, /private key/i); + }); + }); + + describe("types", () => { + it("TapLeafScript should have correct property names (camelCase)", () => { + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + Buffer.from("test", "ascii"), + ); + + // Check that properties are camelCase + assert.ok("leafVersion" in revealData.tapLeafScript); + assert.ok("script" in revealData.tapLeafScript); + assert.ok("controlBlock" in revealData.tapLeafScript); + + // Verify types + assert.strictEqual(typeof revealData.tapLeafScript.leafVersion, "number"); + assert.ok(revealData.tapLeafScript.script instanceof Uint8Array); + assert.ok(revealData.tapLeafScript.controlBlock instanceof Uint8Array); + }); + + it("PreparedInscriptionRevealData should have correct property names (camelCase)", () => { + const ecpair = ECPair.fromPrivateKey(testPrivateKey); + const revealData = inscriptions.createInscriptionRevealData( + ecpair, + contentType, + Buffer.from("test", "ascii"), + ); + + // Check that properties are camelCase + assert.ok("outputScript" in revealData); + assert.ok("revealTransactionVSize" in revealData); + assert.ok("tapLeafScript" in revealData); + + // Verify types + assert.ok(revealData.outputScript instanceof Uint8Array); + assert.strictEqual(typeof revealData.revealTransactionVSize, "number"); + assert.strictEqual(typeof revealData.tapLeafScript, "object"); + }); + }); +});