diff --git a/.gitignore b/.gitignore index 78e6436..ce5b920 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ node_modules *.d.ts.map /test/build /test/compiled +/.regtest/ +/.bitcoin-core/ +/logs/ +.btcdl +.regtest \ No newline at end of file diff --git a/examples/batch.ts b/examples/batch.ts new file mode 100644 index 0000000..c2c8537 --- /dev/null +++ b/examples/batch.ts @@ -0,0 +1,58 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +const insA: Inscription = { + tags: { contentType: 'text/plain;charset=utf-8' }, + body: utf8.decode('First inscription in batch'), +}; +const insB: Inscription = { + tags: { contentType: 'application/json' }, + body: utf8.decode(JSON.stringify({ name: 'second', kind: 'json' })), +}; +const insC: Inscription = { + tags: { contentType: 'text/markdown;charset=utf-8' }, + body: utf8.decode('# Third inscription\nSome markdown text.'), +}; + +const inscriptions: Inscription[] = [insA, insB, insC]; + +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, inscriptions), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +// After funding the address above with enough sats to cover fees, build the reveal tx +const tx = new btc.Transaction({ customScripts }); +const FUNDING_TXID = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 4000n; // higher since there are multiple inscriptions +const FEE = 1200n; // adjust per tx.vsize * feerate + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/examples/compressed-json.ts b/examples/compressed-json.ts new file mode 100644 index 0000000..0d9c096 --- /dev/null +++ b/examples/compressed-json.ts @@ -0,0 +1,57 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; +import { brotliCompressSync, constants as zlibc } from 'node:zlib'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +const json = JSON.stringify({ example: 'brotli compressed json', count: 3, ok: true }); +const compressed = brotliCompressSync(utf8.decode(json), { + params: { + [zlibc.BROTLI_PARAM_MODE]: zlibc.BROTLI_MODE_TEXT, + [zlibc.BROTLI_PARAM_QUALITY]: zlibc.BROTLI_MAX_QUALITY, + [zlibc.BROTLI_PARAM_SIZE_HINT]: json.length, + }, +}); + +const inscription: Inscription = { + tags: { contentType: 'application/json', contentEncoding: 'br' }, + body: compressed, +}; + +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, [inscription]), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +const tx = new btc.Transaction({ customScripts }); +const FUNDING_TXID = '0000000000000000000000000000000000000000000000000000000000000000'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 3500n; +const FEE = 800n; + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/examples/html.ts b/examples/html.ts new file mode 100644 index 0000000..6470ffe --- /dev/null +++ b/examples/html.ts @@ -0,0 +1,49 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +const html = `
This is a basic HTML example.
`; + +const inscription: Inscription = { + tags: { contentType: 'text/html;charset=utf-8' }, + body: utf8.decode(html), +}; + +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, [inscription]), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +const tx = new btc.Transaction({ customScripts }); +const FUNDING_TXID = '0000000000000000000000000000000000000000000000000000000000000000'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 3000n; +const FEE = 700n; + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/examples/parent-child.ts b/examples/parent-child.ts new file mode 100644 index 0000000..7a5d56b --- /dev/null +++ b/examples/parent-child.ts @@ -0,0 +1,53 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +// Provide a parent inscription id via env: e.g., 0123..abci0 +const PARENT_ID = process.env.PARENT_ID || undefined; + +const child: Inscription = { + tags: { + contentType: 'text/plain;charset=utf-8', + ...(PARENT_ID ? { parent: PARENT_ID } : {}), + }, + body: utf8.decode('This inscription declares a parent reference.'), +}; + +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, [child]), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +const tx = new btc.Transaction({ customScripts }); +const FUNDING_TXID = '0000000000000000000000000000000000000000000000000000000000000000'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 3000n; +const FEE = 700n; + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/examples/rune.ts b/examples/rune.ts new file mode 100644 index 0000000..cd4317a --- /dev/null +++ b/examples/rune.ts @@ -0,0 +1,50 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +// Example rune number (U128). Use a BigInt. +const RUNE = 12345678901234567890n; + +const inscription: Inscription = { + tags: { contentType: 'text/plain;charset=utf-8', rune: RUNE }, + body: utf8.decode('Inscription associated with a rune value.'), +}; + +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, [inscription]), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +const tx = new btc.Transaction({ customScripts }); +const FUNDING_TXID = '0000000000000000000000000000000000000000000000000000000000000000'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 3000n; +const FEE = 700n; + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/examples/simple.ts b/examples/simple.ts new file mode 100644 index 0000000..71b871f --- /dev/null +++ b/examples/simple.ts @@ -0,0 +1,51 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +const inscription: Inscription = { + tags: { contentType: 'text/plain;charset=utf-8' }, + body: utf8.decode('Hello, Ordinals!'), +}; + +// Address where you must first send funds before revealing inscription +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, [inscription]), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +// After funding the address above with enough sats to cover fees, build the reveal tx +const tx = new btc.Transaction({ customScripts }); +// Replace the following three values with your funding UTXO details +const FUNDING_TXID = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 2000n; // sats +const FEE = 500n; // sats (ensure >= tx.vsize * feerate) + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +// Optional: parse inscriptions from witness to verify +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/examples/with-metadata.ts b/examples/with-metadata.ts new file mode 100644 index 0000000..dcef32d --- /dev/null +++ b/examples/with-metadata.ts @@ -0,0 +1,59 @@ +import * as btc from '@scure/btc-signer'; +import { hex, utf8 } from '@scure/base'; +import { OutOrdinalReveal, p2tr_ord_reveal, type Inscription, parseWitness } from '../src/index.ts'; + +const NETWORK = btc.utils.TEST_NETWORK; // or btc.NETWORK for mainnet +const customScripts = [OutOrdinalReveal]; + +// Demo key (do not use in production) +const privKey = hex.decode('0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a'); +const pubKey = btc.utils.pubSchnorr(privKey); + +const metadata = { + app: 'my.cool.app', + version: 1, + author: 'alice', + description: 'Asset with metadata stored via CBOR tag', +}; + +const inscription: Inscription = { + tags: { + contentType: 'application/json', + metadata, + // metaprotocol: 'my-proto-v1', // optional if you need to declare a protocol + }, + body: utf8.decode(JSON.stringify({ hello: 'world', features: ['meta', 'tags'] })), +}; + +const revealPayment = btc.p2tr( + undefined, + p2tr_ord_reveal(pubKey, [inscription]), + NETWORK, + false, + customScripts +); +console.log('Reveal address:', revealPayment.address); + +// After funding the address above with enough sats to cover fees, build the reveal tx +const tx = new btc.Transaction({ customScripts }); +const FUNDING_TXID = '75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858'; +const FUNDING_INDEX = 0; +const FUNDING_AMOUNT = 2500n; +const FEE = 600n; + +tx.addInput({ + ...revealPayment, + txid: FUNDING_TXID, + index: FUNDING_INDEX, + witnessUtxo: { script: revealPayment.script, amount: FUNDING_AMOUNT }, +}); +const changeAddr = revealPayment.address; +tx.addOutputAddress(changeAddr, FUNDING_AMOUNT - FEE, NETWORK); +tx.sign(privKey, undefined, new Uint8Array(32)); +tx.finalize(); + +const txHex = hex.encode(tx.extract()); +console.log('Reveal tx hex:', txHex); + +const parsed = parseWitness(tx.inputs[0].finalScriptWitness); +console.log('Parsed inscriptions:', parsed); \ No newline at end of file diff --git a/scripts/regtest.sh b/scripts/regtest.sh new file mode 100755 index 0000000..b8a50b0 --- /dev/null +++ b/scripts/regtest.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Requirements: bitcoind and bitcoin-cli in PATH. +# This script initializes a fresh regtest datadir, starts bitcoind, funds reveal addresses for each mode, +# and asks the Node helper to build reveal tx hex. Logs are saved under logs/. + +DIR=$(cd "$(dirname "$0")/.." && pwd) +LOGS="$DIR/logs" +DATADIR="$DIR/.regtest" +mkdir -p "$LOGS" "$DATADIR" + +# Prefer locally downloaded Bitcoin Core if present +if [ -d "/workspace/.bitcoin-core/bin" ]; then + export PATH="/workspace/.bitcoin-core/bin:$PATH" +fi + +BITCOIND=${BITCOIND:-bitcoind} +CLI=${CLI:-bitcoin-cli} +RPCPORT=${RPCPORT:-18443} +PORT=${PORT:-18444} +WALLET_NAME=${WALLET_NAME:-w1} +WALLET_ARG="-rpcwallet=$WALLET_NAME" + +start_node() { + "$BITCOIND" -regtest \ + -datadir="$DATADIR" \ + -server -daemon \ + -txindex=1 \ + -rpcuser=user -rpcpassword=pass \ + -rpcallowip=127.0.0.1 \ + -rpcport=$RPCPORT \ + -port=$PORT \ + -fallbackfee=0.0002 \ + -prune=0 >/dev/null + # Wait for RPC + for i in {1..30}; do + if "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT getblockchaininfo >/dev/null 2>&1; then + break + fi + sleep 1 + done +} + +ensure_wallet() { + # Create wallet if not exists; ignore error if already exists + if ! "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT listwallets | jq -e '.[] | select(.=="'$WALLET_NAME'")' >/dev/null; then + "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT createwallet "$WALLET_NAME" >/dev/null + fi + # Load wallet (ignore error if already loaded) + "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT loadwallet "$WALLET_NAME" >/dev/null 2>&1 || true +} + +stop_node() { + "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT stop >/dev/null || true +} + +new_address() { + "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT $WALLET_ARG getnewaddress '' bech32m +} + +mine() { + local blocks=${1:-1} + local addr + addr=$(new_address) + "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT generatetoaddress "$blocks" "$addr" >/dev/null +} + +send_to() { + local addr=$1 + local amount=$2 # BTC + "$CLI" -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT $WALLET_ARG -named sendtoaddress address="$addr" amount="$amount" +} + +# Clean previous run +stop_node || true +rm -rf "$DATADIR/regtest" +start_node +ensure_wallet + +# Prefund wallet +mine 101 + +run_mode() { + local mode=$1 + local fee_sat=$2 + local log="$LOGS/$mode.log" + echo "Running $mode ..." | tee "$log" + local out + out=$(MODE="$mode" node "$DIR/scripts/reveal.js" | tee -a "$log") + local addr + addr=$(echo "$out" | grep '^REVEAL_ADDRESS=' | cut -d= -f2) + echo "Funding $addr" | tee -a "$log" + local txid + txid=$(send_to "$addr" 0.001) + echo "FUNDING_TXID=$txid" | tee -a "$log" + mine 1 + # Fetch vout and amount + local vout amount + vout=$($CLI -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT $WALLET_ARG gettransaction "$txid" | jq -r '.details[] | select(.address=="'$addr'") | .vout' | head -n1) + amount=$($CLI -regtest -rpcuser=user -rpcpassword=pass -rpcport=$RPCPORT gettxout "$txid" "$vout" | jq -r '.value' ) + # Convert BTC to sat + local amount_sat + amount_sat=$(python3 - <