From e3be41f9f64980d6be1e92191251f7ff6021aed3 Mon Sep 17 00:00:00 2001 From: joundy Date: Fri, 1 Dec 2023 15:35:40 +0700 Subject: [PATCH] Joundy/upgrade 0.12.2 (#3) * Fix lost sats bug (#2666) * Add Hindi version of handbook (#2648) * Remove Index::index_block_inscription_numbers (#2667) * Hide protocol inscriptions (#2674) * Don't color links in headers (#2678) * Add inscription charms (#2681) * Group rune server tests (#2685) * Add inscription compression (#1713) * Fix media table formatting (#2686) * Update schema version for charms (#2687) * Fix unbound outpoint server error (#2479) * Add binary media type (#2671) * Clean up install.sh (#2669) * Add /collections Page (#2561) * Preview font inscriptions (#2692) * Only load used language highlight module in code preview (#2696) * Only try to create the database if it wasn't found (#2703) * Move postage into batch file (#2705) * Add destination field to batch (#2701) * Use sequence numbers database keys (#2664) * Update redb to 1.4.0 (#2714) * Refactor inscriptions paginations (#2715) * Display table stats in `ord index info` (#2711) * Use redb's recovery callback API (#2584) * Allow setting CSP origin (#2708) * Remove default file path from `ord index export --tsv` (#2717) * Use icons in nav bar (#2722) * Add Debian packaging instructions (#2725) * Add Homebrew install instructions to readme (#2726) * Add sat recursive endpoints with index and pagination (#2680) * Only accept sat number in recursive endpoint (#2732) * Fix typo in docs/src/inscriptions/metadata.md (#2731) * Add docs for metadata recursive endpoint (#2734) * Remove `RUNE` from

on /rune (#2728) * Add /r/children recursive endpoint (#2431) * Add docs and examples for sat recursive endpoint (#2735) * Ignore flaky test (#2742) * Update docs to include all fields, including content-encoding (#2740) * Add docs for child recursive endpoint (#2743) * Hide JSON and .btc (#2744) * Release 0.12.0 (#2746) * Hide all text (#2753) * Add batch to preview command (#2752) * Add stuttering curse (#2745) * Batch inscribe on same sat (#2749) * Allow setting the sat to inscribe (#2765) * Select further away coins which meet target (#2724) * Fix typos (#2768) * Add ability to specify sat to batch inscribe (#2770) * Add commands to etch and list runes (#2544) * Set CSP origin in deploy script (#2764) Co-authored-by: raph * Add `public` to /content Cache-Control headers (#2773) * Release 0.12.1 (#2776) * Bless cursed inscriptions after Jubilee height (#2656) * Hide /content/ HTML inscriptions (#2778) * Release 0.12.2 (#2780) * fix(test): error test from version 0.12.2 --------- Co-authored-by: raph Co-authored-by: duttydeedz <142775511+duttydeedz@users.noreply.github.com> Co-authored-by: Casey Rodarmor Co-authored-by: liam <31192478+terror@users.noreply.github.com> Co-authored-by: Eloc <42568538+elocremarc@users.noreply.github.com> Co-authored-by: Julian Eager Co-authored-by: ordinally <11798624+veryordinally@users.noreply.github.com> Co-authored-by: Christopher Berner Co-authored-by: Rijndael <115941166+rot13maxi@users.noreply.github.com> Co-authored-by: vuittont60 <81072379+vuittont60@users.noreply.github.com> Co-authored-by: gmart7t2 <49558347+gmart7t2@users.noreply.github.com> Co-authored-by: xiaolou86 <20718693+xiaolou86@users.noreply.github.com> --- CHANGELOG.md | 27 ++ Cargo.lock | 146 +++++-- Cargo.toml | 4 +- batch.yaml | 8 +- deploy/ord.service | 1 + deploy/save-ord-dev-state | 2 +- docs/src/guides/teleburning.md | 2 +- justfile | 1 - src/chain.rs | 9 + src/envelope.rs | 184 +++++++-- src/index.rs | 141 ++++++- src/index/updater.rs | 1 + src/index/updater/inscription_updater.rs | 8 +- src/inscription.rs | 81 ++-- src/inscription_id.rs | 2 +- src/lib.rs | 13 +- src/options.rs | 28 +- src/runes.rs | 6 +- src/runes/rune_id.rs | 35 +- src/runes/runestone.rs | 163 +++++++- src/subcommand.rs | 4 + src/subcommand/find.rs | 4 +- src/subcommand/preview.rs | 104 +++-- src/subcommand/runes.rs | 77 ++++ src/subcommand/server.rs | 6 +- src/subcommand/wallet.rs | 4 + src/subcommand/wallet/balance.rs | 56 ++- src/subcommand/wallet/etch.rs | 131 ++++++ src/subcommand/wallet/inscribe.rs | 104 ++++- src/subcommand/wallet/inscribe/batch.rs | 39 +- src/subcommand/wallet/send.rs | 28 +- src/subcommand/wallet/transaction_builder.rs | 193 +++++++-- src/test.rs | 1 + src/wallet.rs | 1 + test-bitcoincore-rpc/Cargo.toml | 3 +- test-bitcoincore-rpc/src/api.rs | 8 + test-bitcoincore-rpc/src/lib.rs | 35 ++ test-bitcoincore-rpc/src/server.rs | 65 ++- test-bitcoincore-rpc/src/state.rs | 16 +- tests/command_builder.rs | 12 +- tests/core.rs | 41 +- tests/etch.rs | 399 +++++++++++++++++++ tests/expected.rs | 1 + tests/lib.rs | 64 ++- tests/runes.rs | 138 +++++++ tests/wallet/balance.rs | 73 +++- tests/wallet/inscribe.rs | 252 ++++++++++++ tests/wallet/send.rs | 25 +- 48 files changed, 2386 insertions(+), 360 deletions(-) create mode 100644 src/subcommand/runes.rs create mode 100644 src/subcommand/wallet/etch.rs create mode 100644 tests/etch.rs create mode 100644 tests/runes.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a364e533..382b56690b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,33 @@ Changelog ========= +[0.12.2](https://github.com/ordinals/ord/releases/tag/0.12.2) - 2023-11-29 +-------------------------------------------------------------------------- + +### Added +- Bless cursed inscriptions after Jubilee height (#2656) + +### Misc +- Hide /content/ HTML inscriptions (#2778) + +[0.12.1](https://github.com/ordinals/ord/releases/tag/0.12.1) - 2023-11-29 +-------------------------------------------------------------------------- + +### Added +- Add commands to etch and list runes (#2544) +- Add ability to specify sat to batch inscribe (#2770) +- Allow setting the sat to inscribe (#2765) +- Batch inscribe on same sat (#2749) +- Add stuttering curse (#2745) +- Add batch to preview command (#2752) + +### Misc +- Add `public` to /content Cache-Control headers (#2773) +- Set CSP origin in deploy script (#2764) +- Fix typos (#2768) +- Select further away coins which meet target (#2724) +- Hide all text (#2753) + [0.12.0](https://github.com/ordinals/ord/releases/tag/0.12.0) - 2023-11-24 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index b0f317d3e9..7707d87531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.48.5", ] @@ -649,9 +650,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.8" +version = "4.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +checksum = "41fffed7514f420abec6d183b1d3acfd9099c79c3a10a06ade4f8203f1411272" dependencies = [ "clap_builder", "clap_derive", @@ -659,9 +660,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.8" +version = "4.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +checksum = "63361bae7eef3771745f02d8d892bec2fee5f6e34af316ba556e7f97a7069ff1" dependencies = [ "anstream", "anstyle", @@ -1050,12 +1051,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1317,15 +1318,15 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "globset" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", "bstr", - "fnv", "log", - "regex", + "regex-automata", + "regex-syntax", ] [[package]] @@ -1380,9 +1381,9 @@ checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" @@ -1628,9 +1629,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -2064,7 +2065,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.0" +version = "0.12.2" dependencies = [ "anyhow", "async-trait", @@ -2296,9 +2297,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -2520,9 +2521,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866" dependencies = [ "cc", "getrandom", @@ -2636,7 +2637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", - "ring 0.17.5", + "ring 0.17.6", "rustls-webpki", "sct", ] @@ -2686,7 +2687,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.5", + "ring 0.17.6", "untrusted 0.9.0", ] @@ -2732,7 +2733,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.5", + "ring 0.17.6", "untrusted 0.9.0", ] @@ -3002,9 +3003,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.10" +version = "0.29.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -3071,6 +3072,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "tempfile", ] [[package]] @@ -3449,9 +3451,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3459,9 +3461,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -3474,9 +3476,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3486,9 +3488,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3496,9 +3498,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -3509,15 +3511,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3587,6 +3589,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3617,6 +3628,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3629,6 +3655,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3641,6 +3673,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3653,6 +3691,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3665,6 +3709,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3677,6 +3727,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3689,6 +3745,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3701,6 +3763,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index ca19cc9995..1521e1d377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.0" +version = "0.12.2" license = "CC0-1.0" edition = "2021" autotests = false @@ -28,7 +28,7 @@ bip39 = "2.0.0" bitcoin = { version = "0.30.1", features = ["rand"] } boilerplate = { version = "1.0.0", features = ["axum"] } brotli = "3.4.0" -chrono = "0.4.19" +chrono = { version = "0.4.19", features = ["serde"] } ciborium = "0.2.1" clap = { version = "4.4.2", features = ["derive"] } ctrlc = { version = "3.2.1", features = ["termination"] } diff --git a/batch.yaml b/batch.yaml index ffe29ac596..16cd49fd11 100644 --- a/batch.yaml +++ b/batch.yaml @@ -1,8 +1,9 @@ # example batch file # there are two modes: -# - `separate-outputs`: place all inscriptions in separate postage-sized outputs -# - `shared-output`: place inscriptions in a single output separated by postage +# - `separate-outputs`: inscribe on separate postage-sized outputs +# - `shared-output`: inscribe on a single output separated by postage +# - `same-sat`: inscribe on the same sat mode: separate-outputs # parent inscription: @@ -11,6 +12,9 @@ parent: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0 # postage for each inscription: postage: 12345 +# sat to inscribe on, can only be used with `same-sat`: +# sat: 5000000000 + # inscriptions to inscribe # # each inscription has the following fields: diff --git a/deploy/ord.service b/deploy/ord.service index f1d7b74043..32023f3e28 100644 --- a/deploy/ord.service +++ b/deploy/ord.service @@ -16,6 +16,7 @@ ExecStart=/usr/local/bin/ord \ --index-sats \ server \ --acme-contact mailto:casey@rodarmor.com \ + --csp-origin https://ordinals.com \ --http \ --https Group=ord diff --git a/deploy/save-ord-dev-state b/deploy/save-ord-dev-state index 1dba217b7d..d13be4a44e 100755 --- a/deploy/save-ord-dev-state +++ b/deploy/save-ord-dev-state @@ -8,7 +8,7 @@ cd /var/lib/ord mkdir -p /var/lib/ord/$REVISION -# Still have to manually set --index-sats or --heigh-limit +# Still have to manually set --index-sats or --height-limit # --height-limit 100000 \ /usr/local/bin/ord --bitcoin-data-dir /var/lib/bitcoind \ --data-dir /var/lib/ord \ diff --git a/docs/src/guides/teleburning.md b/docs/src/guides/teleburning.md index a4ecb69866..c54029a92f 100644 --- a/docs/src/guides/teleburning.md +++ b/docs/src/guides/teleburning.md @@ -9,7 +9,7 @@ Teleburning an asset means something like, "I'm out. Find me on Bitcoin." Teleburn addresses are derived from inscription IDs. They have no corresponding private key, so assets sent to a teleburn address are burned. Currently, only -Ethereum teleburn addresses are suppported. Pull requests adding teleburn +Ethereum teleburn addresses are supported. Pull requests adding teleburn addresses for other chains are welcome. Ethereum diff --git a/justfile b/justfile index 3ecc95c785..b8c4e6ec4b 100644 --- a/justfile +++ b/justfile @@ -101,7 +101,6 @@ prepare-release revision='master': git checkout -b release-$VERSION git add -u git commit -m "Release $VERSION" - git tag -a $VERSION -m "Release $VERSION" gh pr create --web publish-release revision='master': diff --git a/src/chain.rs b/src/chain.rs index 89c8123bfd..f8ef180cdc 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -47,6 +47,15 @@ impl Chain { } } + pub(crate) fn jubilee_height(self) -> u32 { + match self { + Self::Mainnet => 824544, + Self::Regtest => 110, + Self::Signet => 175392, + Self::Testnet => 2544192, + } + } + pub(crate) fn genesis_block(self) -> Block { bitcoin::blockdata::constants::genesis_block(self.network()) } diff --git a/src/envelope.rs b/src/envelope.rs index 5d5f5c2747..68415799fb 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -2,8 +2,13 @@ use { super::*, bitcoin::blockdata::{ opcodes, - script::{self, Instruction, Instructions}, + script::{ + self, + Instruction::{self, Op, PushBytes}, + Instructions, + }, }, + std::iter::Peekable, }; pub(crate) const PROTOCOL_ID: [u8; 3] = *b"ord"; @@ -22,10 +27,11 @@ pub(crate) type ParsedEnvelope = Envelope; #[derive(Debug, Default, PartialEq, Clone)] pub(crate) struct Envelope { - pub(crate) payload: T, pub(crate) input: u32, pub(crate) offset: u32, + pub(crate) payload: T, pub(crate) pushnum: bool, + pub(crate) stutter: bool, } fn remove_field(fields: &mut BTreeMap<&[u8], Vec<&[u8]>>, field: &[u8]) -> Option> { @@ -111,6 +117,7 @@ impl From for ParsedEnvelope { input: envelope.input, offset: envelope.offset, pushnum: envelope.pushnum, + stutter: envelope.stutter, } } } @@ -142,13 +149,17 @@ impl RawEnvelope { fn from_tapscript(tapscript: &Script, input: usize) -> Result> { let mut envelopes = Vec::new(); - let mut instructions = tapscript.instructions(); + let mut instructions = tapscript.instructions().peekable(); - while let Some(instruction) = instructions.next() { - if instruction? == Instruction::PushBytes((&[]).into()) { - if let Some(envelope) = Self::from_instructions(&mut instructions, input, envelopes.len())? - { + let mut stuttered = false; + while let Some(instruction) = instructions.next().transpose()? { + if instruction == PushBytes((&[]).into()) { + let (stutter, envelope) = + Self::from_instructions(&mut instructions, input, envelopes.len(), stuttered)?; + if let Some(envelope) = envelope { envelopes.push(envelope); + } else { + stuttered = stutter; } } } @@ -156,17 +167,29 @@ impl RawEnvelope { Ok(envelopes) } + fn accept(instructions: &mut Peekable, instruction: Instruction) -> Result { + if instructions.peek() == Some(&Ok(instruction)) { + instructions.next().transpose()?; + Ok(true) + } else { + Ok(false) + } + } + fn from_instructions( - instructions: &mut Instructions, + instructions: &mut Peekable, input: usize, offset: usize, - ) -> Result> { - if instructions.next().transpose()? != Some(Instruction::Op(opcodes::all::OP_IF)) { - return Ok(None); + stutter: bool, + ) -> Result<(bool, Option)> { + if !Self::accept(instructions, Op(opcodes::all::OP_IF))? { + let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into()))); + return Ok((stutter, None)); } - if instructions.next().transpose()? != Some(Instruction::PushBytes((&PROTOCOL_ID).into())) { - return Ok(None); + if !Self::accept(instructions, PushBytes((&PROTOCOL_ID).into()))? { + let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into()))); + return Ok((stutter, None)); } let mut pushnum = false; @@ -175,87 +198,91 @@ impl RawEnvelope { loop { match instructions.next().transpose()? { - None => return Ok(None), - Some(Instruction::Op(opcodes::all::OP_ENDIF)) => { - return Ok(Some(Envelope { - input: input.try_into().unwrap(), - offset: offset.try_into().unwrap(), - payload, - pushnum, - })); + None => return Ok((false, None)), + Some(Op(opcodes::all::OP_ENDIF)) => { + return Ok(( + false, + Some(Envelope { + input: input.try_into().unwrap(), + offset: offset.try_into().unwrap(), + payload, + pushnum, + stutter, + }), + )); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_NEG1)) => { + Some(Op(opcodes::all::OP_PUSHNUM_NEG1)) => { pushnum = true; payload.push(vec![0x81]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_1)) => { + Some(Op(opcodes::all::OP_PUSHNUM_1)) => { pushnum = true; payload.push(vec![1]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_2)) => { + Some(Op(opcodes::all::OP_PUSHNUM_2)) => { pushnum = true; payload.push(vec![2]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_3)) => { + Some(Op(opcodes::all::OP_PUSHNUM_3)) => { pushnum = true; payload.push(vec![3]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_4)) => { + Some(Op(opcodes::all::OP_PUSHNUM_4)) => { pushnum = true; payload.push(vec![4]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_5)) => { + Some(Op(opcodes::all::OP_PUSHNUM_5)) => { pushnum = true; payload.push(vec![5]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_6)) => { + Some(Op(opcodes::all::OP_PUSHNUM_6)) => { pushnum = true; payload.push(vec![6]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_7)) => { + Some(Op(opcodes::all::OP_PUSHNUM_7)) => { pushnum = true; payload.push(vec![7]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_8)) => { + Some(Op(opcodes::all::OP_PUSHNUM_8)) => { pushnum = true; payload.push(vec![8]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_9)) => { + Some(Op(opcodes::all::OP_PUSHNUM_9)) => { pushnum = true; payload.push(vec![9]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_10)) => { + Some(Op(opcodes::all::OP_PUSHNUM_10)) => { pushnum = true; payload.push(vec![10]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_11)) => { + Some(Op(opcodes::all::OP_PUSHNUM_11)) => { pushnum = true; payload.push(vec![11]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_12)) => { + Some(Op(opcodes::all::OP_PUSHNUM_12)) => { pushnum = true; payload.push(vec![12]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_13)) => { + Some(Op(opcodes::all::OP_PUSHNUM_13)) => { pushnum = true; payload.push(vec![13]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_14)) => { + Some(Op(opcodes::all::OP_PUSHNUM_14)) => { pushnum = true; payload.push(vec![14]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_15)) => { + Some(Op(opcodes::all::OP_PUSHNUM_15)) => { pushnum = true; payload.push(vec![15]); } - Some(Instruction::Op(opcodes::all::OP_PUSHNUM_16)) => { + Some(Op(opcodes::all::OP_PUSHNUM_16)) => { pushnum = true; payload.push(vec![16]); } - Some(Instruction::PushBytes(push)) => { + Some(PushBytes(push)) => { payload.push(push.as_bytes().to_vec()); } - Some(_) => return Ok(None), + Some(_) => return Ok((false, None)), } } } @@ -895,4 +922,81 @@ mod tests { ); } } + + #[test] + fn stuttering() { + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: true, + ..Default::default() + }], + ); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: true, + ..Default::default() + }], + ); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: true, + ..Default::default() + }], + ); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_AND) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + assert_eq!( + parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), + vec![ParsedEnvelope { + payload: Default::default(), + stutter: false, + ..Default::default() + }], + ); + } } diff --git a/src/index.rs b/src/index.rs index 14ba28a4d7..7b2ffe1396 100644 --- a/src/index.rs +++ b/src/index.rs @@ -260,6 +260,7 @@ impl Index { .unwrap() .value() != 0; + index_sats = statistics .get(&Statistic::IndexSats.key())? .unwrap() @@ -312,9 +313,11 @@ impl Index { statistics.insert( &Statistic::IndexRunes.key(), - &u64::from(options.index_runes()), + &u64::from(index_runes), )?; - statistics.insert(&Statistic::IndexSats.key(), &u64::from(options.index_sats))?; + + statistics.insert(&Statistic::IndexSats.key(), &u64::from(index_sats))?; + statistics.insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; } @@ -422,6 +425,10 @@ impl Index { .collect() } + pub(crate) fn has_rune_index(&self) -> bool { + self.index_runes + } + pub(crate) fn has_sat_index(&self) -> bool { self.index_sats } @@ -830,6 +837,22 @@ impl Index { Ok(balances) } + pub(crate) fn get_runic_outputs(&self, outpoints: &[OutPoint]) -> Result> { + let rtx = self.database.begin_read()?; + + let outpoint_to_balances = rtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; + + let mut runic = BTreeSet::new(); + + for outpoint in outpoints { + if outpoint_to_balances.get(&outpoint.store())?.is_some() { + runic.insert(*outpoint); + } + } + + Ok(runic) + } + #[cfg(test)] pub(crate) fn get_rune_balances(&self) -> Vec<(OutPoint, Vec<(RuneId, u128)>)> { let mut result = Vec::new(); @@ -1282,7 +1305,8 @@ impl Index { ) } - pub(crate) fn find(&self, sat: u64) -> Result> { + pub(crate) fn find(&self, sat: Sat) -> Result> { + let sat = sat.0; let rtx = self.begin_read()?; if rtx.block_count()? <= Sat(sat).height().n() { @@ -1311,9 +1335,11 @@ impl Index { pub(crate) fn find_range( &self, - range_start: u64, - range_end: u64, + range_start: Sat, + range_end: Sat, ) -> Result>> { + let range_start = range_start.0; + let range_end = range_end.0; let rtx = self.begin_read()?; if rtx.block_count()? < Sat(range_end - 1).height().n() + 1 { @@ -2029,7 +2055,7 @@ mod tests { fn find_first_sat() { let context = Context::builder().arg("--index-sats").build(); assert_eq!( - context.index.find(0).unwrap().unwrap(), + context.index.find(Sat(0)).unwrap().unwrap(), SatPoint { outpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0" .parse() @@ -2043,7 +2069,7 @@ mod tests { fn find_second_sat() { let context = Context::builder().arg("--index-sats").build(); assert_eq!( - context.index.find(1).unwrap().unwrap(), + context.index.find(Sat(1)).unwrap().unwrap(), SatPoint { outpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0" .parse() @@ -2058,7 +2084,7 @@ mod tests { let context = Context::builder().arg("--index-sats").build(); context.mine_blocks(1); assert_eq!( - context.index.find(50 * COIN_VALUE).unwrap().unwrap(), + context.index.find(Sat(50 * COIN_VALUE)).unwrap().unwrap(), SatPoint { outpoint: "30f2f037629c6a21c1f40ed39b9bd6278df39762d68d07f49582b23bcb23386a:0" .parse() @@ -2071,7 +2097,7 @@ mod tests { #[test] fn find_unmined_sat() { let context = Context::builder().arg("--index-sats").build(); - assert_eq!(context.index.find(50 * COIN_VALUE).unwrap(), None); + assert_eq!(context.index.find(Sat(50 * COIN_VALUE)).unwrap(), None); } #[test] @@ -2085,7 +2111,7 @@ mod tests { }); context.mine_blocks(1); assert_eq!( - context.index.find(50 * COIN_VALUE).unwrap().unwrap(), + context.index.find(Sat(50 * COIN_VALUE)).unwrap().unwrap(), SatPoint { outpoint: OutPoint::new(spend_txid, 0), offset: 0, @@ -3017,6 +3043,63 @@ mod tests { } } + #[test] + fn inscriptions_are_uncursed_after_jubilee() { + for context in Context::configurations() { + context.mine_blocks(108); + + let witness = envelope(&[ + b"ord", + &[1], + b"text/plain;charset=utf-8", + &[1], + b"text/plain;charset=utf-8", + ]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness.clone())], + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + assert_eq!(context.rpc_server.height(), 109); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .inscription_number, + -1 + ); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, witness)], + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + assert_eq!(context.rpc_server.height(), 110); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .inscription_number, + 0 + ); + } + } + #[test] fn duplicate_field_inscriptions_are_cursed() { for context in Context::configurations() { @@ -3117,7 +3200,45 @@ mod tests { } #[test] + fn inscriptions_with_stutter_are_cursed() { + for context in Context::configurations() { + context.mine_blocks(1); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([]) + .push_opcode(opcodes::all::OP_PUSHNUM_1) + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + assert_eq!( + context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap() + .inscription_number, + -1 + ); + } + } + // https://github.com/ordinals/ord/issues/2062 + #[test] fn zero_value_transaction_inscription_not_cursed_but_unbound() { for context in Context::configurations() { context.mine_blocks(1); diff --git a/src/index/updater.rs b/src/index/updater.rs index 515869255a..077628c9a5 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -422,6 +422,7 @@ impl<'index> Updater<'_> { let mut inscription_updater = InscriptionUpdater { blessed_inscription_count, + chain: self.index.options.chain(), cursed_inscription_count, flotsam: Vec::new(), height: self.height, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index a224da67cb..5d4cb3a1c5 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -9,6 +9,7 @@ enum Curse { Pointer, Pushnum, Reinscription, + Stutter, UnrecognizedEvenField, } @@ -37,6 +38,7 @@ enum Origin { pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) blessed_inscription_count: u64, + pub(super) chain: Chain, pub(super) cursed_inscription_count: u64, pub(super) flotsam: Vec, pub(super) height: u32, @@ -134,7 +136,9 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let inscribed_offset = inscribed_offsets.get(&offset); - let curse = if inscription.payload.unrecognized_even_field { + let curse = if self.height >= self.chain.jubilee_height() { + None + } else if inscription.payload.unrecognized_even_field { Some(Curse::UnrecognizedEvenField) } else if inscription.payload.duplicate_field { Some(Curse::DuplicateField) @@ -148,6 +152,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Some(Curse::Pointer) } else if inscription.pushnum { Some(Curse::Pushnum) + } else if inscription.stutter { + Some(Curse::Stutter) } else if let Some((id, count)) = inscribed_offset { if *count > 1 { Some(Curse::Reinscription) diff --git a/src/inscription.rs b/src/inscription.rs index d08a012840..b2e415204f 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -308,41 +308,29 @@ impl Inscription { } pub(crate) fn hidden(&self) -> bool { - let Some(content_type) = self.content_type() else { - return false; - }; - - if content_type.starts_with("application/json") { - return true; - } - - if !content_type.starts_with("text/plain") { - return false; + lazy_static! { + static ref CONTENT: Regex = Regex::new(r"^\s*/content/[[:xdigit:]]{64}i\d+\s*$").unwrap(); } - let Some(body) = &self.body else { - return false; - }; - - let Ok(text) = str::from_utf8(body) else { - return false; - }; - - let trimmed = text.trim(); - - if trimmed.starts_with('{') && trimmed.ends_with('}') { + let Some(content_type) = self.content_type() else { return true; - } + }; - if trimmed.starts_with("gib bc1") { + if content_type.starts_with("application/json") { return true; } - if trimmed.ends_with(".bitmap") { + if content_type.starts_with("text/plain") { return true; } - if trimmed.ends_with(".btc") { + if content_type.starts_with("text/html") + && self + .body() + .and_then(|body| str::from_utf8(body).ok()) + .map(|body| CONTENT.is_match(body)) + .unwrap_or_default() + { return true; } @@ -811,22 +799,39 @@ mod tests { ); } - case(None, None, false); + case(None, None, true); case(Some("foo"), None, false); - case(Some("foo"), Some("{}"), false); - case(Some("text/plain"), None, false); - case(Some("text/plain"), Some("foo{}bar"), false); - case(Some("text/plain"), Some("foo.btc"), true); - - case(Some("text/plain"), Some("foo.bitmap"), true); - case(Some("text/plain"), Some("gib bc1"), true); - case(Some("text/plain"), Some("{}"), true); - case(Some("text/plain"), Some(" {} "), true); - case(Some("text/plain;charset=utf-8"), Some("foo.bitmap"), true); - case(Some("text/plain;charset=cn-big5"), Some("foo.bitmap"), true); + case(Some("text/plain"), None, true); + case( + Some("text/plain"), + Some("The fox jumped. The cow danced."), + true, + ); + case(Some("text/plain;charset=utf-8"), Some("foo"), true); + case(Some("text/plain;charset=cn-big5"), Some("foo"), true); case(Some("application/json"), Some("foo"), true); + case( + Some("text/markdown"), + Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"), + false, + ); + case( + Some("text/html"), + Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"), + true, + ); + case( + Some("text/html;charset=utf-8"), + Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"), + true, + ); + case( + Some("text/html"), + Some(" /content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0 \n"), + true, + ); - assert!(!Inscription { + assert!(Inscription { content_type: Some("text/plain".as_bytes().into()), body: Some(b"{\xc3\x28}".as_slice().into()), ..Default::default() diff --git a/src/inscription_id.rs b/src/inscription_id.rs index 36b05495a2..c51eddb172 100644 --- a/src/inscription_id.rs +++ b/src/inscription_id.rs @@ -72,7 +72,7 @@ impl Display for ParseError { match self { Self::Character(c) => write!(f, "invalid character: '{c}'"), Self::Length(len) => write!(f, "invalid length: {len}"), - Self::Separator(c) => write!(f, "invalid seprator: `{c}`"), + Self::Separator(c) => write!(f, "invalid separator: `{c}`"), Self::Txid(err) => write!(f, "invalid txid: {err}"), Self::Index(err) => write!(f, "invalid index: {err}"), } diff --git a/src/lib.rs b/src/lib.rs index a3d0dccfc9..ca99e6f627 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,11 +28,11 @@ use { options::Options, outgoing::Outgoing, representation::Representation, - runes::{Pile, Rune, RuneId}, + runes::{Edict, Etching, Pile, Runestone}, subcommand::{Subcommand, SubcommandResult}, tally::Tally, }, - anyhow::{anyhow, bail, Context, Error}, + anyhow::{anyhow, bail, ensure, Context, Error}, bip39::Mnemonic, bitcoin::{ address::{Address, NetworkUnchecked}, @@ -56,7 +56,7 @@ use { serde::{Deserialize, Deserializer, Serialize, Serializer}, std::{ cmp, - collections::{BTreeMap, HashMap, HashSet, VecDeque}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, env, ffi::OsString, fmt::{self, Display, Formatter}, @@ -79,11 +79,12 @@ use { tokio::{runtime::Runtime, task}, }; -pub use crate::{ +pub use self::{ fee_rate::FeeRate, inscription::Inscription, object::Object, rarity::Rarity, + runes::{Rune, RuneId}, sat::Sat, sat_point::SatPoint, subcommand::wallet::transaction_builder::{Target, TransactionBuilder}, @@ -148,13 +149,15 @@ static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); static LISTENERS: Mutex> = Mutex::new(Vec::new()); static INDEXER: Mutex>> = Mutex::new(Option::None); +const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); + fn integration_test() -> bool { env::var_os("ORD_INTEGRATION_TEST") .map(|value| value.len() > 0) .unwrap_or(false) } -fn timestamp(seconds: u32) -> DateTime { +pub fn timestamp(seconds: u32) -> DateTime { Utc.timestamp_opt(seconds.into(), 0).unwrap() } diff --git a/src/options.rs b/src/options.rs index a10ad40739..31267d8546 100644 --- a/src/options.rs +++ b/src/options.rs @@ -217,6 +217,12 @@ impl Options { "Using credentials from cookie file at `{}`", cookie_file.display() ); + + ensure!( + cookie_file.is_file(), + "cookie file `{}` does not exist", + cookie_file.display() + ); } let client = Client::new(&rpc_url, auth) @@ -532,15 +538,10 @@ mod tests { .network(Network::Testnet) .build(); - let tempdir = TempDir::new().unwrap(); - - let cookie_file = tempdir.path().join(".cookie"); - fs::write(&cookie_file, "username:password").unwrap(); - let options = Options::try_parse_from([ "ord", "--cookie-file", - cookie_file.to_str().unwrap(), + rpc_server.cookie_file().to_str().unwrap(), "--rpc-url", &rpc_server.url(), ]) @@ -825,4 +826,19 @@ mod tests { .options .index_runes(),); } + + #[test] + fn cookie_file_does_not_exist_error() { + assert_eq!( + Options { + cookie_file: Some("/foo/bar/baz/qux/.cookie".into()), + ..Default::default() + } + .bitcoin_rpc_client() + .map(|_| "") + .unwrap_err() + .to_string(), + "cookie file `/foo/bar/baz/qux/.cookie` does not exist" + ); + } } diff --git a/src/runes.rs b/src/runes.rs index 15281943a5..a06cc02c38 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1,11 +1,11 @@ use {self::error::Error, super::*}; -pub use runestone::Runestone; +pub use {rune::Rune, rune_id::RuneId}; -pub(crate) use {edict::Edict, etching::Etching, pile::Pile, rune::Rune, rune_id::RuneId}; +pub(crate) use {edict::Edict, etching::Etching, pile::Pile, runestone::Runestone}; +pub const MAX_DIVISIBILITY: u8 = 38; pub(crate) const CLAIM_BIT: u128 = 1 << 48; -const MAX_DIVISIBILITY: u8 = 38; pub(crate) const MAX_LIMIT: u128 = 1 << 64; mod edict; diff --git a/src/runes/rune_id.rs b/src/runes/rune_id.rs index 36e12b7ee2..d5a4e3760c 100644 --- a/src/runes/rune_id.rs +++ b/src/runes/rune_id.rs @@ -1,9 +1,9 @@ use {super::*, std::num::TryFromIntError}; #[derive(Debug, PartialEq, Copy, Clone, Hash, Eq, Ord, PartialOrd)] -pub(crate) struct RuneId { - pub(crate) height: u32, - pub(crate) index: u16, +pub struct RuneId { + pub height: u32, + pub index: u16, } impl TryFrom for RuneId { @@ -44,6 +44,24 @@ impl FromStr for RuneId { } } +impl Serialize for RuneId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for RuneId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(DeserializeFromStr::deserialize(deserializer)?.0) + } +} + #[cfg(test)] mod tests { use super::*; @@ -100,4 +118,15 @@ mod tests { assert!(RuneId::try_from(0x07060504030201).is_err()); } + + #[test] + fn serde() { + let rune_id = RuneId { + height: 1, + index: 2, + }; + let json = "\"1/2\""; + assert_eq!(serde_json::to_string(&rune_id).unwrap(), json); + assert_eq!(serde_json::from_str::(json).unwrap(), rune_id); + } } diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index ad0275e453..f38673927e 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -64,14 +64,7 @@ impl Runestone { return Ok(None); }; - let mut integers = Vec::new(); - let mut i = 0; - - while i < payload.len() { - let (integer, length) = varint::decode(&payload[i..])?; - integers.push(integer); - i += length; - } + let integers = Runestone::integers(&payload)?; let Message { mut fields, body } = Message::from_integers(&integers); @@ -101,7 +94,6 @@ impl Runestone { })) } - #[cfg(test)] pub(crate) fn encipher(&self) -> ScriptBuf { let mut payload = Vec::new(); @@ -109,7 +101,7 @@ impl Runestone { varint::encode_to_vec(TAG_RUNE, &mut payload); varint::encode_to_vec(etching.rune.0, &mut payload); - if etching.divisibility != 0 && etching.divisibility <= MAX_DIVISIBILITY { + if etching.divisibility != 0 { varint::encode_to_vec(TAG_DIVISIBILITY, &mut payload); varint::encode_to_vec(etching.divisibility.into(), &mut payload); } @@ -187,6 +179,19 @@ impl Runestone { Ok(None) } + + fn integers(payload: &[u8]) -> Result> { + let mut integers = Vec::new(); + let mut i = 0; + + while i < payload.len() { + let (integer, length) = varint::decode(&payload[i..])?; + integers.push(integer); + i += length; + } + + Ok(integers) + } } #[cfg(test)] @@ -1337,4 +1342,142 @@ mod tests { })) ); } + + #[test] + fn encipher() { + #[track_caller] + fn case(runestone: Runestone, expected: &[u128]) { + let script_pubkey = runestone.encipher(); + + let transaction = Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey, + value: 0, + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }; + + let payload = Runestone::payload(&transaction).unwrap().unwrap(); + + assert_eq!(Runestone::integers(&payload).unwrap(), expected); + + let runestone = { + let mut edicts = runestone.edicts; + edicts.sort_by_key(|edict| edict.id); + Runestone { + edicts, + ..runestone + } + }; + + assert_eq!( + Runestone::from_transaction(&transaction).unwrap(), + runestone + ); + } + + case(Runestone::default(), &[]); + + case( + Runestone { + etching: Some(Etching { + divisibility: 1, + limit: Some(2), + symbol: Some('@'), + rune: Rune(3), + term: Some(4), + }), + edicts: vec![ + Edict { + amount: 8, + id: 9, + output: 10, + }, + Edict { + amount: 5, + id: 6, + output: 7, + }, + ], + burn: false, + }, + &[ + TAG_RUNE, + 3, + TAG_DIVISIBILITY, + 1, + TAG_SYMBOL, + '@'.into(), + TAG_LIMIT, + 2, + TAG_TERM, + 4, + TAG_BODY, + 6, + 5, + 7, + 3, + 8, + 10, + ], + ); + + case( + Runestone { + etching: Some(Etching { + divisibility: 0, + limit: None, + symbol: None, + rune: Rune(3), + term: None, + }), + burn: false, + ..Default::default() + }, + &[TAG_RUNE, 3], + ); + + case( + Runestone { + burn: true, + ..Default::default() + }, + &[TAG_BURN, 0], + ); + } + + #[test] + fn runestone_payload_is_chunked() { + let script = Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 0, + output: 0 + }; + 173 + ], + ..Default::default() + } + .encipher(); + + assert_eq!(script.instructions().count(), 3); + + let script = Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 0, + output: 0 + }; + 174 + ], + ..Default::default() + } + .encipher(); + + assert_eq!(script.instructions().count(), 4); + } } diff --git a/src/subcommand.rs b/src/subcommand.rs index 687fa187e8..89de4b3b37 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -7,6 +7,7 @@ pub mod index; pub mod list; pub mod parse; mod preview; +pub mod runes; mod server; pub mod subsidy; pub mod supply; @@ -30,6 +31,8 @@ pub(crate) enum Subcommand { Parse(parse::Parse), #[command(about = "Run an explorer server populated with inscriptions")] Preview(preview::Preview), + #[command(about = "List all runes")] + Runes, #[command(about = "Run the explorer server")] Server(server::Server), #[command(about = "Display information about a block's subsidy")] @@ -54,6 +57,7 @@ impl Subcommand { Self::List(list) => list.run(options), Self::Parse(parse) => parse.run(), Self::Preview(preview) => preview.run(), + Self::Runes => runes::run(options), Self::Server(server) => { let index = Arc::new(Index::open(&options)?); let handle = axum_server::Handle::new(); diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index deb28c44f2..68884679df 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -31,11 +31,11 @@ impl Find { index.update()?; match self.end { - Some(end) => match index.find_range(self.sat.0, end.0)? { + Some(end) => match index.find_range(self.sat, end)? { Some(result) => Ok(Box::new(result)), None => Err(anyhow!("range has not been mined as of index height")), }, - None => match index.find(self.sat.0)? { + None => match index.find(self.sat)? { Some(satpoint) => Ok(Box::new(Output { satpoint })), None => Err(anyhow!("sat has not been mined as of index height")), }, diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 0bb4d4c0ee..eea71af653 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -4,7 +4,24 @@ use {super::*, fee_rate::FeeRate}; pub(crate) struct Preview { #[command(flatten)] server: super::server::Server, - inscriptions: Vec, + #[arg( + num_args = 0.., + long, + help = "Inscribe inscriptions defined in ." + )] + batches: Option>, + #[arg(num_args = 0.., long, help = "Inscribe contents of .")] + files: Option>, +} + +#[derive(Debug, Parser)] +pub(crate) struct Batch { + batch_files: Vec, +} + +#[derive(Debug, Parser)] +pub(crate) struct File { + files: Vec, } struct KillOnDrop(process::Child); @@ -74,33 +91,68 @@ impl Preview { rpc_client.generate_to_address(101, &address)?; - for file in self.inscriptions { - Arguments { - options: options.clone(), - subcommand: Subcommand::Wallet(super::wallet::Wallet::Inscribe( - super::wallet::inscribe::Inscribe { - batch: None, - cbor_metadata: None, - commit_fee_rate: None, - compress: false, - destination: None, - dry_run: false, - fee_rate: FeeRate::try_from(1.0).unwrap(), - file: Some(file), - json_metadata: None, - metaprotocol: None, - no_backup: true, - no_limit: false, - parent: None, - postage: Some(TransactionBuilder::TARGET_POSTAGE), - reinscribe: false, - satpoint: None, - }, - )), + if let Some(files) = self.files { + for file in files { + Arguments { + options: options.clone(), + subcommand: Subcommand::Wallet(super::wallet::Wallet::Inscribe( + super::wallet::inscribe::Inscribe { + batch: None, + cbor_metadata: None, + commit_fee_rate: None, + compress: false, + destination: None, + dry_run: false, + fee_rate: FeeRate::try_from(1.0).unwrap(), + file: Some(file), + json_metadata: None, + metaprotocol: None, + no_backup: true, + no_limit: false, + parent: None, + postage: Some(TARGET_POSTAGE), + reinscribe: false, + satpoint: None, + sat: None, + }, + )), + } + .run()?; + + rpc_client.generate_to_address(1, &address)?; } - .run()?; + } - rpc_client.generate_to_address(1, &address)?; + if let Some(batches) = self.batches { + for batch in batches { + Arguments { + options: options.clone(), + subcommand: Subcommand::Wallet(super::wallet::Wallet::Inscribe( + super::wallet::inscribe::Inscribe { + batch: Some(batch), + cbor_metadata: None, + commit_fee_rate: None, + compress: false, + destination: None, + dry_run: false, + fee_rate: FeeRate::try_from(1.0).unwrap(), + file: None, + json_metadata: None, + metaprotocol: None, + no_backup: true, + no_limit: false, + parent: None, + postage: Some(TARGET_POSTAGE), + reinscribe: false, + satpoint: None, + sat: None, + }, + )), + } + .run()?; + + rpc_client.generate_to_address(1, &address)?; + } } rpc_client.generate_to_address(1, &address)?; diff --git a/src/subcommand/runes.rs b/src/subcommand/runes.rs new file mode 100644 index 0000000000..5e88e02c80 --- /dev/null +++ b/src/subcommand/runes.rs @@ -0,0 +1,77 @@ +use super::*; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Output { + pub runes: BTreeMap, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct RuneInfo { + pub burned: u128, + pub divisibility: u8, + pub etching: Txid, + pub height: u32, + pub id: RuneId, + pub index: u16, + pub rune: Rune, + pub supply: u128, + pub symbol: Option, + pub end: Option, + pub limit: Option, + pub number: u64, + pub timestamp: DateTime, +} + +pub(crate) fn run(options: Options) -> SubcommandResult { + let index = Index::open(&options)?; + + ensure!( + index.has_rune_index(), + "`ord runes` requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag", + ); + + index.update()?; + + Ok(Box::new(Output { + runes: index + .runes()? + .into_iter() + .map( + |( + id, + RuneEntry { + burned, + divisibility, + etching, + rune, + supply, + symbol, + end, + limit, + number, + timestamp, + }, + )| { + ( + rune, + RuneInfo { + burned, + divisibility, + etching, + height: id.height, + id, + index: id.index, + end, + limit, + number, + timestamp: crate::timestamp(timestamp), + rune, + supply, + symbol, + }, + ) + }, + ) + .collect::>(), + })) +} diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 3e49f793db..7397303dbf 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1090,7 +1090,7 @@ impl Server { headers.insert( header::CACHE_CONTROL, - HeaderValue::from_static("max-age=31536000, immutable"), + HeaderValue::from_static("public, max-age=31536000, immutable"), ); let Some(body) = inscription.into_body() else { @@ -2845,7 +2845,7 @@ mod tests { for i in 0..101 { let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], + inputs: &[(i + 1, 0, 0, inscription("foo", "hello").to_witness())], ..Default::default() }); ids.push(InscriptionId { txid, index: 0 }); @@ -3728,7 +3728,7 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); assert_eq!( response.headers().get(header::CACHE_CONTROL).unwrap(), - "max-age=31536000, immutable" + "public, max-age=31536000, immutable" ); } diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index a526ac7b05..1923791829 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -17,6 +17,7 @@ use { pub mod balance; pub mod cardinals; pub mod create; +pub mod etch; pub mod inscribe; pub mod inscriptions; pub mod outputs; @@ -33,6 +34,8 @@ pub(crate) enum Wallet { Balance, #[command(about = "Create new wallet")] Create(create::Create), + #[command(about = "Create rune")] + Etch(etch::Etch), #[command(about = "Create inscription")] Inscribe(inscribe::Inscribe), #[command(about = "List wallet inscriptions")] @@ -58,6 +61,7 @@ impl Wallet { match self { Self::Balance => balance::run(options), Self::Create(create) => create.run(options), + Self::Etch(etch) => etch.run(options), Self::Inscribe(inscribe) => inscribe.run(options), Self::Inscriptions => inscriptions::run(options), Self::Receive => receive::run(options), diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 61cdb9cfcd..996eb903b6 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -1,8 +1,14 @@ use {super::*, crate::wallet::Wallet, std::collections::BTreeSet}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Output { pub cardinal: u64, + pub ordinal: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runes: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runic: Option, + pub total: u64, } pub(crate) fn run(options: Options) -> SubcommandResult { @@ -17,12 +23,50 @@ pub(crate) fn run(options: Options) -> SubcommandResult { .map(|satpoint| satpoint.outpoint) .collect::>(); - let mut balance = 0; - for (outpoint, amount) in index.get_unspent_outputs(Wallet::load(&options)?)? { - if !inscription_outputs.contains(&outpoint) { - balance += amount.to_sat() + let mut cardinal = 0; + let mut ordinal = 0; + let mut runes = BTreeMap::new(); + let mut runic = 0; + for (outpoint, amount) in unspent_outputs { + let rune_balances = index.get_rune_balances_for_outpoint(outpoint)?; + + if inscription_outputs.contains(&outpoint) { + ordinal += amount.to_sat(); + } else if !rune_balances.is_empty() { + for (rune, pile) in rune_balances { + *runes.entry(rune).or_default() += pile.amount; + } + runic += amount.to_sat(); + } else { + cardinal += amount.to_sat(); } } - Ok(Box::new(Output { cardinal: balance })) + Ok(Box::new(Output { + cardinal, + ordinal, + runes: index.has_rune_index().then_some(runes), + runic: index.has_rune_index().then_some(runic), + total: cardinal + ordinal + runic, + })) +} + +#[cfg(test)] +mod tests { + use crate::subcommand::wallet::balance::Output; + + #[test] + fn runes_and_runic_fields_are_not_present_if_none() { + assert_eq!( + serde_json::to_string(&Output { + cardinal: 0, + ordinal: 0, + runes: None, + runic: None, + total: 0 + }) + .unwrap(), + r#"{"cardinal":0,"ordinal":0,"total":0}"# + ); + } } diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs new file mode 100644 index 0000000000..1a32d1a09f --- /dev/null +++ b/src/subcommand/wallet/etch.rs @@ -0,0 +1,131 @@ +use {super::*, bitcoin::blockdata::locktime::absolute::LockTime}; + +#[derive(Debug, Parser)] +pub(crate) struct Etch { + #[clap(long, help = "Set divisibility to .")] + divisibility: u8, + #[clap(long, help = "Etch with fee rate of sats/vB.")] + fee_rate: FeeRate, + #[clap(long, help = "Etch rune .")] + rune: Rune, + #[clap(long, help = "Set supply to .")] + supply: u128, + #[clap(long, help = "Set currency symbol to .")] + symbol: char, +} + +#[derive(Serialize, Deserialize)] +pub struct Output { + pub transaction: Txid, +} + +impl Etch { + pub(crate) fn run(self, options: Options) -> SubcommandResult { + let index = Index::open(&options)?; + + ensure!( + index.has_rune_index(), + "`ord wallet etch` requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag", + ); + + index.update()?; + + let client = options.bitcoin_rpc_client_for_wallet_command(false)?; + + let count = client.get_block_count()?; + + ensure!( + index.rune(self.rune)?.is_none(), + "rune `{}` has already been etched", + self.rune + ); + + let minimum_at_height = Rune::minimum_at_height(Height(u32::try_from(count).unwrap() + 1)); + + ensure!( + self.rune >= minimum_at_height, + "rune is less than minimum for next block: {} < {minimum_at_height}", + self.rune + ); + + ensure!( + self.divisibility <= crate::runes::MAX_DIVISIBILITY, + " must be equal to or less than 38" + ); + + let destination = get_change_address(&client, options.chain())?; + + let runestone = Runestone { + etching: Some(Etching { + divisibility: self.divisibility, + rune: self.rune, + limit: None, + symbol: Some(self.symbol), + term: None, + }), + edicts: vec![Edict { + amount: self.supply, + id: 0, + output: 1, + }], + burn: false, + }; + + let script_pubkey = runestone.encipher(); + + ensure!( + script_pubkey.len() <= 82, + "runestone greater than maximum OP_RETURN size: {} > 82", + script_pubkey.len() + ); + + let unfunded_transaction = Transaction { + version: 1, + lock_time: LockTime::ZERO, + input: Vec::new(), + output: vec![ + TxOut { + script_pubkey, + value: 0, + }, + TxOut { + script_pubkey: destination.script_pubkey(), + value: TARGET_POSTAGE.to_sat(), + }, + ], + }; + + let unspent_outputs = index.get_unspent_outputs(crate::wallet::Wallet::load(&options)?)?; + + let inscriptions = index + .get_inscriptions(&unspent_outputs)? + .keys() + .map(|satpoint| satpoint.outpoint) + .collect::>(); + + if !client.lock_unspent(&inscriptions)? { + bail!("failed to lock UTXOs"); + } + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + let unsigned_transaction = client.fund_raw_transaction( + &unfunded_transaction, + Some(&bitcoincore_rpc::json::FundRawTransactionOptions { + // NB. This is `fundrawtransaction`'s `feeRate`, which is fee per kvB + // and *not* fee per vB. So, we multiply the fee rate given by the user + // by 1000. + fee_rate: Some(Amount::from_sat((self.fee_rate.n() * 1000.0).ceil() as u64)), + ..Default::default() + }), + Some(false), + )?; + + let signed_tx = client + .sign_raw_transaction_with_wallet(&unsigned_transaction.hex, None, None)? + .hex; + + let transaction = client.send_raw_transaction(&signed_tx)?; + + Ok(Box::new(Output { transaction })) + } +} diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index b12b5e67a8..51c962a5a7 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -16,7 +16,6 @@ use { }, bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, SignRawTransactionInput, Timestamp}, bitcoincore_rpc::Client, - std::collections::BTreeSet, }; mod batch; @@ -53,7 +52,7 @@ pub(crate) struct ParentInfo { pub(crate) struct Inscribe { #[arg( long, - help = "Inscribe a multiple inscriptions defines in a yaml .", + help = "Inscribe multiple inscriptions defined in a yaml .", conflicts_with_all = &[ "cbor_metadata", "destination", "file", "json_metadata", "metaprotocol", "parent", "postage", "reinscribe", "satpoint" ] @@ -82,7 +81,7 @@ pub(crate) struct Inscribe { pub(crate) file: Option, #[arg( long, - help = "Include JSON in file at convered to CBOR as inscription metadata", + help = "Include JSON in file at converted to CBOR as inscription metadata", conflicts_with = "cbor_metadata" )] pub(crate) json_metadata: Option, @@ -106,6 +105,8 @@ pub(crate) struct Inscribe { pub(crate) reinscribe: bool, #[arg(long, help = "Inscribe .")] pub(crate) satpoint: Option, + #[arg(long, help = "Inscribe .", conflicts_with = "satpoint")] + pub(crate) sat: Option, } impl Inscribe { @@ -115,9 +116,13 @@ impl Inscribe { let index = Index::open(&options)?; index.update()?; - let utxos = index.get_unspent_outputs(Wallet::load(&options)?)?; + let wallet = Wallet::load(&options)?; - let locked_utxos = index.get_locked_outputs(Wallet::load(&options)?)?; + let utxos = index.get_unspent_outputs(wallet)?; + + let locked_utxos = index.get_locked_outputs(wallet)?; + + let runic_utxos = index.get_runic_outputs(&utxos.keys().cloned().collect::>())?; let client = options.bitcoin_rpc_client_for_wallet_command(false)?; @@ -128,12 +133,13 @@ impl Inscribe { let inscriptions; let mode; let parent_info; + let sat; match (self.file, self.batch) { (Some(file), None) => { parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain)?; - postage = self.postage.unwrap_or(TransactionBuilder::TARGET_POSTAGE); + postage = self.postage.unwrap_or(TARGET_POSTAGE); inscriptions = vec![Inscription::from_file( chain, @@ -147,6 +153,8 @@ impl Inscribe { mode = Mode::SeparateOutputs; + sat = self.sat; + destinations = vec![match self.destination.clone() { Some(destination) => destination.require_network(chain.network())?, None => get_change_address(&client, chain)?, @@ -160,7 +168,7 @@ impl Inscribe { postage = batchfile .postage .map(Amount::from_sat) - .unwrap_or(TransactionBuilder::TARGET_POSTAGE); + .unwrap_or(TARGET_POSTAGE); (inscriptions, destinations) = batchfile.inscriptions( &client, @@ -172,10 +180,30 @@ impl Inscribe { )?; mode = batchfile.mode; + + if batchfile.sat.is_some() && mode != Mode::SameSat { + return Err(anyhow!("`sat` can only be set in `same-sat` mode")); + } + + sat = batchfile.sat; } _ => unreachable!(), } + let satpoint = if let Some(sat) = sat { + if !index.has_sat_index() { + return Err(anyhow!( + "index must be built with `--index-sats` to use `--sat`" + )); + } + match index.find(sat)? { + Some(satpoint) => Some(satpoint), + None => return Err(anyhow!(format!("could not find sat `{sat}`"))), + } + } else { + self.satpoint + }; + Batch { commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), destinations, @@ -188,9 +216,9 @@ impl Inscribe { postage, reinscribe: self.reinscribe, reveal_fee_rate: self.fee_rate, - satpoint: self.satpoint, + satpoint, } - .inscribe(chain, &index, &client, &locked_utxos, &utxos) + .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &utxos) } fn parse_metadata(cbor: Option, json: Option) -> Result>> { @@ -271,7 +299,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -279,6 +307,7 @@ mod tests { BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), change, ) @@ -295,7 +324,7 @@ mod tests { } #[test] - fn inscribe_tansactions_opt_in_to_rbf() { + fn inscribe_transactions_opt_in_to_rbf() { let utxos = vec![(outpoint(1), Amount::from_sat(20000))]; let inscription = inscription("text/plain", "ord"); let commit_address = change(0); @@ -311,7 +340,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -319,6 +348,7 @@ mod tests { BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), change, ) @@ -354,7 +384,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -362,6 +392,7 @@ mod tests { inscriptions, Chain::Mainnet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], ) @@ -404,7 +435,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -412,6 +443,7 @@ mod tests { inscriptions, Chain::Mainnet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], ) @@ -448,7 +480,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -456,6 +488,7 @@ mod tests { inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], ) @@ -529,7 +562,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -537,6 +570,7 @@ mod tests { inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ) @@ -610,7 +644,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -618,6 +652,7 @@ mod tests { inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], ) @@ -667,7 +702,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), no_limit: false, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -675,6 +710,7 @@ mod tests { BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], ) @@ -706,7 +742,7 @@ mod tests { reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), no_limit: true, reinscribe: false, - postage: TransactionBuilder::TARGET_POSTAGE, + postage: TARGET_POSTAGE, mode: Mode::SharedOutput, ..Default::default() } @@ -714,6 +750,7 @@ mod tests { BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], ) @@ -883,6 +920,7 @@ inscriptions: wallet_inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ) @@ -979,6 +1017,7 @@ inscriptions: wallet_inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ) @@ -1053,6 +1092,7 @@ inscriptions: wallet_inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ); @@ -1090,6 +1130,7 @@ inscriptions: wallet_inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ) @@ -1142,6 +1183,7 @@ inscriptions: wallet_inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ) @@ -1151,7 +1193,7 @@ inscriptions: assert!(reveal_tx .output .iter() - .all(|output| output.value == TransactionBuilder::TARGET_POSTAGE.to_sat())); + .all(|output| output.value == TARGET_POSTAGE.to_sat())); } #[test] @@ -1218,6 +1260,7 @@ inscriptions: wallet_inscriptions, Chain::Signet, BTreeSet::new(), + BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], ) @@ -1322,4 +1365,25 @@ inscriptions: .contains("error: the following required arguments were not provided:\n <--file |--batch >") ); } + + #[test] + fn satpoint_and_sat_flags_conflict() { + assert_regex_match!( + Arguments::try_parse_from([ + "ord", + "--index-sats", + "wallet", + "inscribe", + "--sat", + "50000000000", + "--satpoint", + "038112028c55f3f77cc0b8b413df51f70675f66be443212da0642b7636f68a00:1:0", + "--file", + "baz", + ]) + .unwrap_err() + .to_string(), + ".*--sat.*cannot be used with.*--satpoint.*" + ); + } } diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index cc38055cc5..7dc70420fb 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -41,6 +41,7 @@ impl Batch { index: &Index, client: &Client, locked_utxos: &BTreeSet, + runic_utxos: BTreeSet, utxos: &BTreeMap, ) -> SubcommandResult { let wallet_inscriptions = index.get_inscriptions(utxos)?; @@ -55,6 +56,7 @@ impl Batch { wallet_inscriptions, chain, locked_utxos.clone(), + runic_utxos, utxos.clone(), commit_tx_change, )?; @@ -132,7 +134,7 @@ impl Batch { let index = u32::try_from(index).unwrap(); let vout = match self.mode { - Mode::SharedOutput => { + Mode::SharedOutput | Mode::SameSat => { if self.parent_info.is_some() { 1 } else { @@ -150,7 +152,7 @@ impl Batch { let offset = match self.mode { Mode::SharedOutput => u64::from(index) * self.postage.to_sat(), - Mode::SeparateOutputs => 0, + Mode::SeparateOutputs | Mode::SameSat => 0, }; inscriptions_output.push(InscriptionInfo { @@ -179,6 +181,7 @@ impl Batch { wallet_inscriptions: BTreeMap, chain: Chain, locked_utxos: BTreeSet, + runic_utxos: BTreeSet, mut utxos: BTreeMap, change: [Address; 2], ) -> Result<(Transaction, Transaction, TweakedKeyPair, u64)> { @@ -189,15 +192,12 @@ impl Batch { .all(|inscription| inscription.parent().unwrap() == parent_info.id)) } - if self.satpoint.is_some() { - assert_eq!( - self.inscriptions.len(), - 1, - "invariant: satpoint may only be specified when making a single inscription", - ); - } - match self.mode { + Mode::SameSat => assert_eq!( + self.destinations.len(), + 1, + "invariant: same-sat has only one destination" + ), Mode::SeparateOutputs => assert_eq!( self.destinations.len(), self.inscriptions.len(), @@ -219,9 +219,14 @@ impl Batch { .collect::>(); utxos - .keys() - .find(|outpoint| !inscribed_utxos.contains(outpoint) && !locked_utxos.contains(outpoint)) - .map(|outpoint| SatPoint { + .iter() + .find(|(outpoint, amount)| { + amount.to_sat() > 0 + && !inscribed_utxos.contains(outpoint) + && !locked_utxos.contains(outpoint) + && !runic_utxos.contains(outpoint) + }) + .map(|(outpoint, _amount)| SatPoint { outpoint: *outpoint, offset: 0, }) @@ -286,7 +291,7 @@ impl Batch { .map(|destination| TxOut { script_pubkey: destination.script_pubkey(), value: match self.mode { - Mode::SeparateOutputs => self.postage.to_sat(), + Mode::SeparateOutputs | Mode::SameSat => self.postage.to_sat(), Mode::SharedOutput => total_postage.to_sat(), }, }) @@ -325,6 +330,7 @@ impl Batch { wallet_inscriptions, utxos.clone(), locked_utxos.clone(), + runic_utxos, commit_tx_address.clone(), change, self.commit_fee_rate, @@ -520,6 +526,8 @@ impl Batch { #[derive(PartialEq, Debug, Copy, Clone, Serialize, Deserialize, Default)] pub(crate) enum Mode { + #[serde(rename = "same-sat")] + SameSat, #[default] #[serde(rename = "separate-outputs")] SeparateOutputs, @@ -556,6 +564,7 @@ pub(crate) struct Batchfile { pub(crate) mode: Mode, pub(crate) parent: Option, pub(crate) postage: Option, + pub(crate) sat: Option, } impl Batchfile { @@ -619,7 +628,7 @@ impl Batchfile { } let destinations = match self.mode { - Mode::SharedOutput => vec![get_change_address(client, chain)?], + Mode::SharedOutput | Mode::SameSat => vec![get_change_address(client, chain)?], Mode::SeparateOutputs => self .inscriptions .iter() diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 32646dfeda..d2cf6ee4f7 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -32,12 +32,17 @@ impl Send { let client = options.bitcoin_rpc_client_for_wallet_command(false)?; - let unspent_outputs = index.get_unspent_outputs(Wallet::load(&options)?)?; + let wallet = Wallet::load(&options)?; - let locked_outputs = index.get_locked_outputs(Wallet::load(&options)?)?; + let unspent_outputs = index.get_unspent_outputs(wallet)?; + + let locked_outputs = index.get_locked_outputs(wallet)?; let inscriptions = index.get_inscriptions(&unspent_outputs)?; + let runic_outputs = + index.get_runic_outputs(&unspent_outputs.keys().cloned().collect::>())?; + let satpoint = match self.outgoing { Outgoing::SatPoint(satpoint) => { for inscription_satpoint in inscriptions.keys() { @@ -45,13 +50,19 @@ impl Send { bail!("inscriptions must be sent by inscription ID"); } } + + ensure!( + !runic_outputs.contains(&satpoint.outpoint), + "runic outpoints may not be sent by satpoint" + ); + satpoint } Outgoing::InscriptionId(id) => index .get_inscription_satpoint_by_id(id)? .ok_or_else(|| anyhow!("Inscription {id} not found"))?, Outgoing::Amount(amount) => { - Self::lock_inscriptions(&client, inscriptions, unspent_outputs)?; + Self::lock_inscriptions(&client, inscriptions, runic_outputs, unspent_outputs)?; let txid = Self::send_amount(&client, amount, address, self.fee_rate.n())?; return Ok(Box::new(Output { transaction: txid })); } @@ -73,6 +84,7 @@ impl Send { inscriptions, unspent_outputs, locked_outputs, + runic_outputs, address.clone(), change, self.fee_rate, @@ -92,21 +104,23 @@ impl Send { fn lock_inscriptions( client: &Client, inscriptions: BTreeMap, - unspent_outputs: BTreeMap, + runic_outputs: BTreeSet, + unspent_outputs: BTreeMap, ) -> Result { let all_inscription_outputs = inscriptions .keys() .map(|satpoint| satpoint.outpoint) .collect::>(); - let wallet_inscription_outputs = unspent_outputs + let locked_outputs = unspent_outputs .keys() .filter(|utxo| all_inscription_outputs.contains(utxo)) + .chain(runic_outputs.iter()) .cloned() .collect::>(); - if !client.lock_unspent(&wallet_inscription_outputs)? { - bail!("failed to lock ordinal UTXOs"); + if !client.lock_unspent(&locked_outputs)? { + bail!("failed to lock UTXOs"); } Ok(()) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index ebdecf6a79..bd1694e861 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -8,18 +8,17 @@ //! constructing ordinal-aware transactions that take these additional //! conditions into account. //! -//! The external interface is -//! `TransactionBuilder::new`, which returns a +//! The external interface is `TransactionBuilder::new`, which returns a //! constructed transaction given the `Target`, which include the outgoing sat //! to send, the wallets current UTXOs and their sat ranges, and the -//! recipient's address. To build the transaction call `Transaction::build_transaction`. +//! recipient's address. To build the transaction call +//! `Transaction::build_transaction`. //! -//! `Target::Postage` ensures that the -//! outgoing value is at most 20,000 sats, reducing it to 10,000 sats if coin -//! selection requires adding excess value. +//! `Target::Postage` ensures that the outgoing value is at most 20,000 sats, +//! reducing it to 10,000 sats if coin selection requires adding excess value. //! -//! `Target::Value(Amount)` ensures that the -//! outgoing value is exactly the requested amount, +//! `Target::Value(Amount)` ensures that the outgoing value is exactly the +//! requested amount, //! //! Internally, `TransactionBuilder` calls multiple methods that implement //! transformations responsible for individual concerns, such as ensuring that @@ -27,9 +26,10 @@ //! //! This module is heavily tested. For all features of transaction //! construction, there should be a positive test that checks that the feature -//! is implemented correctly, an assertion in the final `Transaction::build_transaction` -//! method that the built transaction is correct with respect to the feature, -//! and a test that the assertion fires as expected. +//! is implemented correctly, an assertion in the final +//! `Transaction::build_transaction` method that the built transaction is +//! correct with respect to the feature, and a test that the assertion fires as +//! expected. use { super::*, @@ -37,10 +37,7 @@ use { blockdata::{locktime::absolute::LockTime, witness::Witness}, Amount, ScriptBuf, }, - std::{ - cmp::{max, min}, - collections::{BTreeMap, BTreeSet}, - }, + std::cmp::{max, min}, }; #[derive(Debug, PartialEq)] @@ -104,13 +101,14 @@ pub struct TransactionBuilder { fee_rate: FeeRate, inputs: Vec, inscriptions: BTreeMap, + locked_utxos: BTreeSet, outgoing: SatPoint, outputs: Vec<(Address, Amount)>, recipient: Address, + runic_utxos: BTreeSet, + target: Target, unused_change_addresses: Vec
, utxos: BTreeSet, - locked_utxos: BTreeSet, - target: Target, } type Result = std::result::Result; @@ -119,7 +117,6 @@ impl TransactionBuilder { const ADDITIONAL_INPUT_VBYTES: usize = 58; const ADDITIONAL_OUTPUT_VBYTES: usize = 43; const SCHNORR_SIGNATURE_SIZE: usize = 64; - pub(crate) const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); pub(crate) const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); pub fn new( @@ -127,6 +124,7 @@ impl TransactionBuilder { inscriptions: BTreeMap, amounts: BTreeMap, locked_utxos: BTreeSet, + runic_utxos: BTreeSet, recipient: Address, change: [Address; 2], fee_rate: FeeRate, @@ -134,17 +132,18 @@ impl TransactionBuilder { ) -> Self { Self { utxos: amounts.keys().cloned().collect(), - locked_utxos, amounts, change_addresses: change.iter().cloned().collect(), fee_rate, inputs: Vec::new(), inscriptions, + locked_utxos, outgoing, outputs: Vec::new(), recipient, - unused_change_addresses: change.to_vec(), + runic_utxos, target, + unused_change_addresses: change.to_vec(), } } @@ -351,7 +350,7 @@ impl TransactionBuilder { if let Some(excess) = value.checked_sub(self.fee_rate.fee(self.estimate_vbytes())) { let (max, target) = match self.target { Target::ExactPostage(postage) => (postage, postage), - Target::Postage => (Self::MAX_POSTAGE, Self::TARGET_POSTAGE), + Target::Postage => (Self::MAX_POSTAGE, TARGET_POSTAGE), Target::Value(value) => (value, value), }; @@ -646,10 +645,11 @@ impl TransactionBuilder { panic!("Could not find outgoing sat in inputs"); } - /// Cardinal UTXOs are those that contain no inscriptions and can therefore - /// be used to pad transactions. Sometimes multiple of these UTXOs are needed - /// and depending on the context we want to select either ones above or - /// under (when trying to consolidate dust outputs) the target value. + /// Cardinal UTXOs are those that are unlocked, contain no inscriptions, and + /// contain no runes, can therefore be used to pad transactions and pay fees. + /// Sometimes multiple cardinal UTXOs are needed and depending on the context + /// we want to select either ones above or under (when trying to consolidate + /// dust outputs) the target value. fn select_cardinal_utxo( &mut self, target_value: Amount, @@ -668,7 +668,10 @@ impl TransactionBuilder { let mut best_match = None; for utxo in &self.utxos { - if inscribed_utxos.contains(utxo) || self.locked_utxos.contains(utxo) { + if self.runic_utxos.contains(utxo) + || inscribed_utxos.contains(utxo) + || self.locked_utxos.contains(utxo) + { continue; } @@ -697,7 +700,13 @@ impl TransactionBuilder { current_value >= target_value && is_closer }; - if is_preference_and_closer || not_preference_but_closer { + let newly_meets_preference = if prefer_under { + best_value > target_value && current_value <= target_value + } else { + best_value < target_value && current_value >= target_value + }; + + if is_preference_and_closer || not_preference_but_closer || newly_meets_preference { best_match = Some((*utxo, current_value)) } } @@ -728,6 +737,7 @@ mod tests { BTreeMap::new(), utxos.clone().into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -765,6 +775,7 @@ mod tests { outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), locked_utxos: BTreeSet::new(), + runic_utxos: BTreeSet::new(), recipient: recipient(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), @@ -801,6 +812,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -821,6 +833,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -846,6 +859,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -871,6 +885,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -896,6 +911,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -919,6 +935,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -942,6 +959,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -954,7 +972,7 @@ mod tests { input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))], output: vec![ tx_out(4_950, change(1)), - tx_out(TransactionBuilder::TARGET_POSTAGE.to_sat(), recipient()), + tx_out(TARGET_POSTAGE.to_sat(), recipient()), tx_out(14_831, change(0)), ], }) @@ -971,6 +989,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -990,6 +1009,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1009,6 +1029,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1028,6 +1049,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1054,6 +1076,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1077,6 +1100,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1088,7 +1112,7 @@ mod tests { lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], output: vec![ - tx_out(TransactionBuilder::TARGET_POSTAGE.to_sat(), recipient()), + tx_out(TARGET_POSTAGE.to_sat(), recipient()), tx_out(989_870, change(1)) ], }) @@ -1105,6 +1129,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1126,6 +1151,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1154,6 +1180,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1179,6 +1206,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1207,6 +1235,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1233,6 +1262,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1256,6 +1286,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1281,6 +1312,7 @@ mod tests { fee_rate: FeeRate::try_from(1.0).unwrap(), utxos: BTreeSet::new(), locked_utxos: BTreeSet::new(), + runic_utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), recipient: recipient(), @@ -1311,6 +1343,7 @@ mod tests { fee_rate: FeeRate::try_from(1.0).unwrap(), utxos: BTreeSet::new(), locked_utxos: BTreeSet::new(), + runic_utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), recipient: recipient(), @@ -1341,6 +1374,31 @@ mod tests { BTreeMap::from([(satpoint(2, 10 * COIN_VALUE), inscription_id(1))]), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), + recipient(), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Postage, + ) + .build_transaction(), + Err(Error::NotEnoughCardinalUtxos) + ) + } + + #[test] + fn do_not_select_runic_utxos_for_cardinal_utxos() { + let utxos = vec![ + (outpoint(1), Amount::from_sat(100)), + (outpoint(2), Amount::from_sat(49 * COIN_VALUE)), + ]; + + pretty_assert_eq!( + TransactionBuilder::new( + satpoint(1, 0), + BTreeMap::new(), + utxos.into_iter().collect(), + BTreeSet::new(), + vec![outpoint(2)].into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1361,6 +1419,7 @@ mod tests { BTreeMap::from([(satpoint(1, 500), inscription_id(1))]), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1386,6 +1445,7 @@ mod tests { BTreeMap::from([(satpoint(1, 0), inscription_id(1))]), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], fee_rate, @@ -1418,6 +1478,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1446,6 +1507,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1471,6 +1533,7 @@ mod tests { BTreeMap::from([(satpoint(1, 500), inscription_id(1))]), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1497,6 +1560,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1520,6 +1584,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(4.0).unwrap(), @@ -1562,6 +1627,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1587,6 +1653,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1612,6 +1679,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(5.0).unwrap(), @@ -1637,6 +1705,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(6.0).unwrap(), @@ -1657,6 +1726,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [recipient(), change(1)], FeeRate::try_from(0.0).unwrap(), @@ -1677,6 +1747,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(0)], FeeRate::try_from(0.0).unwrap(), @@ -1697,6 +1768,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(2.0).unwrap(), @@ -1722,6 +1794,7 @@ mod tests { .into_iter() .collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(250.0).unwrap(), @@ -1753,6 +1826,7 @@ mod tests { BTreeMap::new(), utxos.clone().into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1798,6 +1872,7 @@ mod tests { BTreeMap::new(), utxos.clone().into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1850,6 +1925,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1905,6 +1981,7 @@ mod tests { BTreeMap::from([(satpoint(1, 0), inscription_id(1))]), utxos.into_iter().collect(), BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], fee_rate, @@ -1940,6 +2017,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), locked_utxos.into_iter().collect(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), @@ -1965,6 +2043,63 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), locked_utxos.into_iter().collect(), + BTreeSet::new(), + recipient(), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(10_000)), + ); + + assert_eq!( + tx_builder + .select_cardinal_utxo(Amount::from_sat(500), false) + .unwrap() + .0, + outpoint(2), + ); + } + + #[test] + fn prefer_further_away_utxos_if_they_are_newly_under_target() { + let utxos = vec![ + (outpoint(1), Amount::from_sat(510)), + (outpoint(2), Amount::from_sat(400)), + ]; + + let mut tx_builder = TransactionBuilder::new( + satpoint(0, 0), + BTreeMap::new(), + utxos.into_iter().collect(), + BTreeSet::new(), + BTreeSet::new(), + recipient(), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(10_000)), + ); + + assert_eq!( + tx_builder + .select_cardinal_utxo(Amount::from_sat(500), true) + .unwrap() + .0, + outpoint(2), + ); + } + + #[test] + fn prefer_further_away_utxos_if_they_are_newly_over_target() { + let utxos = vec![ + (outpoint(1), Amount::from_sat(490)), + (outpoint(2), Amount::from_sat(600)), + ]; + + let mut tx_builder = TransactionBuilder::new( + satpoint(0, 0), + BTreeMap::new(), + utxos.into_iter().collect(), + BTreeSet::new(), + BTreeSet::new(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), diff --git a/src/test.rs b/src/test.rs index 622dcbe3fd..60d98c1dad 100644 --- a/src/test.rs +++ b/src/test.rs @@ -111,6 +111,7 @@ pub(crate) fn tx_out(value: u64, address: Address) -> TxOut { } } +#[derive(Default, Debug)] pub(crate) struct InscriptionTemplate { pub(crate) parent: Option, } diff --git a/src/wallet.rs b/src/wallet.rs index d97435a83f..b2b975440f 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,5 +1,6 @@ use super::*; +#[derive(Copy, Clone)] pub(crate) struct Wallet { _private: (), } diff --git a/test-bitcoincore-rpc/Cargo.toml b/test-bitcoincore-rpc/Cargo.toml index bbb0ec79b4..d20b7d4aa7 100644 --- a/test-bitcoincore-rpc/Cargo.toml +++ b/test-bitcoincore-rpc/Cargo.toml @@ -13,7 +13,8 @@ hex = "0.4.3" jsonrpc-core = "18.0.0" jsonrpc-derive = "18.0.0" jsonrpc-http-server = "18.0.0" -ord-bitcoincore-rpc = "0.17.0" +ord-bitcoincore-rpc = "0.17.1" reqwest = { version = "0.11.10", features = ["blocking"] } serde = { version = "1.0.137", features = ["derive"] } serde_json = { version = "1.0.81" } +tempfile = "3.2.0" diff --git a/test-bitcoincore-rpc/src/api.rs b/test-bitcoincore-rpc/src/api.rs index 3477faa0e7..ecf763ef1b 100644 --- a/test-bitcoincore-rpc/src/api.rs +++ b/test-bitcoincore-rpc/src/api.rs @@ -50,6 +50,14 @@ pub trait Api { avoid_reuse: Option, ) -> Result; + #[rpc(name = "fundrawtransaction")] + fn fund_raw_transaction( + &self, + tx: String, + options: Option, + is_witness: Option, + ) -> Result; + #[rpc(name = "signrawtransactionwithwallet")] fn sign_raw_transaction_with_wallet( &self, diff --git a/test-bitcoincore-rpc/src/lib.rs b/test-bitcoincore-rpc/src/lib.rs index 56c50e45a8..7da28b1fdd 100644 --- a/test-bitcoincore-rpc/src/lib.rs +++ b/test-bitcoincore-rpc/src/lib.rs @@ -32,10 +32,13 @@ use { state::State, std::{ collections::{BTreeMap, BTreeSet, HashMap}, + fs, + path::PathBuf, sync::{Arc, Mutex, MutexGuard}, thread, time::Duration, }, + tempfile::TempDir, }; mod api; @@ -105,8 +108,13 @@ impl Builder { thread::sleep(Duration::from_millis(25)); } + let tempdir = TempDir::new().unwrap(); + + fs::write(tempdir.path().join(".cookie"), "username:password").unwrap(); + Handle { close_handle: Some(close_handle), + tempdir, port, state, } @@ -149,6 +157,24 @@ impl From for JsonOutPoint { } } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FundRawTransactionOptions { + #[serde(with = "bitcoin::amount::serde::as_btc::opt")] + fee_rate: Option, +} + +#[derive(Deserialize, Clone, PartialEq, Eq, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FundRawTransactionResult { + #[serde(with = "bitcoincore_rpc::json::serde_hex")] + pub hex: Vec, + #[serde(with = "bitcoin::amount::serde::as_btc")] + pub fee: Amount, + #[serde(rename = "changepos")] + pub change_position: i32, +} + impl<'a> Default for TransactionTemplate<'a> { fn default() -> Self { Self { @@ -166,6 +192,7 @@ pub struct Handle { close_handle: Option, port: u16, state: Arc>, + tempdir: TempDir, } impl Handle { @@ -196,6 +223,10 @@ impl Handle { self.state().broadcast_tx(template) } + pub fn height(&self) -> u64 { + u64::try_from(self.state().blocks.len()).unwrap() - 1 + } + pub fn invalidate_tip(&self) -> BlockHash { self.state().pop_block() } @@ -246,6 +277,10 @@ impl Handle { pub fn get_change_addresses(&self) -> Vec
{ self.state().change_addresses.clone() } + + pub fn cookie_file(&self) -> PathBuf { + self.tempdir.path().join(".cookie") + } } impl Drop for Handle { diff --git a/test-bitcoincore-rpc/src/server.rs b/test-bitcoincore-rpc/src/server.rs index 006f04b028..c4edeef079 100644 --- a/test-bitcoincore-rpc/src/server.rs +++ b/test-bitcoincore-rpc/src/server.rs @@ -4,7 +4,6 @@ use { secp256k1::{rand, KeyPair, Secp256k1, XOnlyPublicKey}, Witness, }, - bitcoincore_rpc::RawTx, }; pub(crate) struct Server { @@ -238,6 +237,60 @@ impl Api for Server { }) } + fn fund_raw_transaction( + &self, + tx: String, + options: Option, + _is_witness: Option, + ) -> Result { + let mut transaction: Transaction = deserialize(&hex::decode(tx).unwrap()).unwrap(); + + let state = self.state(); + + let output_value = transaction + .output + .iter() + .map(|txout| txout.value) + .sum::(); + + let (outpoint, input_value) = state + .utxos + .iter() + .find(|(outpoint, value)| value.to_sat() >= output_value && !state.locked.contains(outpoint)) + .ok_or_else(Self::not_found)?; + + transaction.input.push(TxIn { + previous_output: *outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + }); + + let change_position = transaction.output.len() as i32; + + transaction.output.push(TxOut { + value: input_value.to_sat() - output_value, + script_pubkey: ScriptBuf::new(), + }); + + let fee = if let Some(fee_rate) = options.and_then(|options| options.fee_rate) { + // increase vsize to account for the witness that `fundrawtransaction` will add + let funded_vsize = transaction.vsize() as f64 + 68.0 / 4.0; + let funded_kwu = funded_vsize / 1000.0; + let fee = (funded_kwu * fee_rate.to_sat() as f64) as u64; + transaction.output.last_mut().unwrap().value -= fee; + fee + } else { + 0 + }; + + Ok(FundRawTransactionResult { + hex: serialize(&transaction), + fee: Amount::from_sat(fee), + change_position, + }) + } + fn sign_raw_transaction_with_wallet( &self, tx: String, @@ -255,7 +308,7 @@ impl Api for Server { Ok( serde_json::to_value(SignRawTransactionResult { - hex: hex::decode(transaction.raw_hex()).unwrap(), + hex: serialize(&transaction), complete: true, errors: None, }) @@ -334,6 +387,8 @@ impl Api for Server { transaction.output[1].value -= fee; + let txid = transaction.txid(); + state.mempool.push(transaction); state.sent.push(Sent { @@ -342,11 +397,7 @@ impl Api for Server { locked, }); - Ok( - "0000000000000000000000000000000000000000000000000000000000000000" - .parse() - .unwrap(), - ) + Ok(txid) } fn get_transaction( diff --git a/test-bitcoincore-rpc/src/state.rs b/test-bitcoincore-rpc/src/state.rs index 870794c471..1875b335c5 100644 --- a/test-bitcoincore-rpc/src/state.rs +++ b/test-bitcoincore-rpc/src/state.rs @@ -106,13 +106,15 @@ impl State { } for (vout, txout) in tx.output.iter().enumerate() { - self.utxos.insert( - OutPoint { - txid: tx.txid(), - vout: vout.try_into().unwrap(), - }, - Amount::from_sat(txout.value), - ); + if !txout.script_pubkey.is_op_return() { + self.utxos.insert( + OutPoint { + txid: tx.txid(), + vout: vout.try_into().unwrap(), + }, + Amount::from_sat(txout.value), + ); + } } } diff --git a/tests/command_builder.rs b/tests/command_builder.rs index d6b354a1fd..32bc77246b 100644 --- a/tests/command_builder.rs +++ b/tests/command_builder.rs @@ -33,6 +33,7 @@ pub(crate) struct CommandBuilder { expected_exit_code: i32, expected_stderr: Expected, expected_stdout: Expected, + rpc_server_cookie_file: Option, rpc_server_url: Option, stdin: Vec, tempdir: TempDir, @@ -45,6 +46,7 @@ impl CommandBuilder { expected_exit_code: 0, expected_stderr: Expected::String(String::new()), expected_stdout: Expected::String(String::new()), + rpc_server_cookie_file: None, rpc_server_url: None, stdin: Vec::new(), tempdir: TempDir::new().unwrap(), @@ -59,6 +61,7 @@ impl CommandBuilder { pub(crate) fn rpc_server(self, rpc_server: &test_bitcoincore_rpc::Handle) -> Self { Self { rpc_server_url: Some(rpc_server.url()), + rpc_server_cookie_file: Some(rpc_server.cookie_file()), ..self } } @@ -103,13 +106,16 @@ impl CommandBuilder { let mut command = Command::new(executable_path("ord")); if let Some(rpc_server_url) = &self.rpc_server_url { - let cookiefile = self.tempdir.path().join("cookie"); - fs::write(&cookiefile, "username:password").unwrap(); command.args([ "--rpc-url", rpc_server_url, "--cookie-file", - cookiefile.to_str().unwrap(), + self + .rpc_server_cookie_file + .as_ref() + .unwrap() + .to_str() + .unwrap(), ]); } diff --git a/tests/core.rs b/tests/core.rs index 8e4c951d38..827da9f14e 100644 --- a/tests/core.rs +++ b/tests/core.rs @@ -21,29 +21,22 @@ fn preview() { .unwrap() .port(); - let examples = fs::read_dir("examples") - .unwrap() - .map(|entry| { - entry - .unwrap() - .path() - .canonicalize() - .unwrap() - .to_str() - .unwrap() - .into() - }) - .filter(|example| example != "examples/av1.mp4") - .collect::>(); - - let mut args = vec![ - "preview".to_string(), - "--http-port".to_string(), - port.to_string(), - ]; - args.extend(examples.clone()); - - let builder = CommandBuilder::new(args); + let builder = CommandBuilder::new(format!( + "preview --http-port {port} --files alert.html inscription.txt --batches batch_1.yaml batch_2.yaml" + )) + .write("inscription.txt", "Hello World") + .write("alert.html", "") + .write("poem.txt", "Sphinx of black quartz, judge my vow.") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch_1.yaml", + "mode: shared-output\ninscriptions:\n- file: poem.txt\n- file: tulip.png\n", + ) + .write( + "batch_2.yaml", + "mode: shared-output\ninscriptions:\n- file: meow.wav\n", + ); let _child = KillOnDrop(builder.command().spawn().unwrap()); @@ -67,6 +60,6 @@ fn preview() { .unwrap() .text() .unwrap(), - format!(".*( must be equal to or less than 38\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn supply_over_max_is_an_error() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 340282366920938463463374607431768211456 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .stderr_regex(r"error: invalid value '\d+' for '--supply ': number too large to fit in target type\n.*") + .expected_exit_code(2) + .run_and_extract_stdout(); +} + +#[test] +fn rune_below_minimum_is_an_error() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢", + Rune(RUNE - 1), + )) + .rpc_server(&rpc_server) + .expected_stderr("error: rune is less than minimum for next block: ZZZZZZZZZZZZ < AAAAAAAAAAAAA\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn trying_to_etch_an_existing_rune_is_an_error() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + etch(&rpc_server, Rune(RUNE)); + + rpc_server.mine_blocks(1); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn runes_can_be_etched() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + let output = CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 1 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + assert_eq!( + runes(&rpc_server), + vec![( + Rune(RUNE), + RuneInfo { + burned: 0, + divisibility: 1, + end: None, + etching: output.transaction, + height: 2, + id: RuneId { + height: 2, + index: 1 + }, + index: 1, + limit: None, + number: 0, + rune: Rune(RUNE), + supply: 1000, + symbol: Some('¢'), + timestamp: ord::timestamp(2), + } + )] + .into_iter() + .collect() + ); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!(output.runes.unwrap()[&Rune(RUNE)], 1000); +} + +#[test] +fn etch_sets_integer_fee_rate_correctly() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + let output = CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 100 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + let tx = rpc_server.tx(2, 1); + + assert_eq!(tx.txid(), output.transaction); + + let output = tx.output.iter().map(|tx_out| tx_out.value).sum::(); + + assert_eq!(output, 50 * COIN_VALUE - tx.vsize() as u64 * 100); +} + +#[test] +fn etch_sets_decimal_fee_rate_correctly() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + let output = CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 100.5 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + let tx = rpc_server.tx(2, 1); + + assert_eq!(tx.txid(), output.transaction); + + let output = tx.output.iter().map(|tx_out| tx_out.value).sum::(); + + assert_eq!(output, 50 * COIN_VALUE - (tx.vsize() as f64 * 100.5) as u64); +} + +#[test] +fn etch_does_not_select_inscribed_utxos() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks(1); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!(output.cardinal, 5000000000); + + CommandBuilder::new("--regtest wallet inscribe --fee-rate 0 --file foo.txt --postage 50btc") + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks_with_subsidy(1, 0); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!(output.cardinal, 0); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 1 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .stderr_regex("error: JSON-RPC error: .*") + .expected_exit_code(1) + .run_and_extract_stdout(); + + rpc_server.mine_blocks(1); +} + +#[test] +fn inscribe_does_not_select_runic_utxos() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks_with_subsidy(1, 10000); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks_with_subsidy(1, 0); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!(output.cardinal, 0); + assert_eq!(output.ordinal, 0); + assert_eq!(output.runic, Some(10000)); + + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet inscribe --fee-rate 0 --file foo.txt") + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: wallet contains no cardinal utxos\n") + .run_and_extract_stdout(); +} + +#[test] +fn send_amount_does_not_select_runic_utxos() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks_with_subsidy(1, 10000); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks_with_subsidy(1, 0); + + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 600sat") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .stderr_regex("error: JSON-RPC error: .*") + .run_and_extract_stdout(); +} + +#[test] +fn send_satpoint_does_not_send_runic_utxos() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks_with_subsidy(1, 10000); + + let output = CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks_with_subsidy(1, 0); + + CommandBuilder::new(format!("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}:1:0", output.transaction)) + .rpc_server(&rpc_server) + .expected_stderr("error: runic outpoints may not be sent by satpoint\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn send_inscription_does_not_select_runic_utxos() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + rpc_server.mine_blocks_with_subsidy(1, 10000); + + CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", + Rune(RUNE), + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks_with_subsidy(1, 10000); + + let inscribe = CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet inscribe --fee-rate 0 --file foo.txt") + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks_with_subsidy(1, 0); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!(output.cardinal, 0); + assert_eq!(output.ordinal, 10000); + assert_eq!(output.runic, Some(10000)); + + CommandBuilder::new(format!("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet send --postage 10001sat --fee-rate 0 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}", inscribe.inscriptions[0].id)) + .rpc_server(&rpc_server) + .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} diff --git a/tests/expected.rs b/tests/expected.rs index 3a2801196c..3c37912b9e 100644 --- a/tests/expected.rs +++ b/tests/expected.rs @@ -11,6 +11,7 @@ impl Expected { Self::Regex(Regex::new(&format!("^(?s){pattern}$")).unwrap()) } + #[track_caller] pub(crate) fn assert_match(&self, output: &str) { match self { Self::String(string) => pretty_assert_eq!(output, string), diff --git a/tests/lib.rs b/tests/lib.rs index 52eb296fc7..2004566e76 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -11,11 +11,12 @@ use { ord::{ inscription_id::InscriptionId, rarity::Rarity, + subcommand::runes::RuneInfo, templates::{ block::BlockJson, inscription::InscriptionJson, inscriptions::InscriptionsJson, output::OutputJson, sat::SatJson, }, - SatPoint, + Rune, RuneId, SatPoint, }, pretty_assertions::assert_eq as pretty_assert_eq, regex::Regex, @@ -26,7 +27,7 @@ use { fs, io::Write, net::TcpListener, - path::Path, + path::{Path, PathBuf}, process::{Child, Command, Stdio}, str::{self, FromStr}, thread, @@ -50,21 +51,15 @@ macro_rules! assert_regex_match { }; } -type Inscribe = ord::subcommand::wallet::inscribe::Output; +const RUNE: u128 = 99246114928149462; -fn inscribe(rpc_server: &test_bitcoincore_rpc::Handle) -> (InscriptionId, Txid) { - rpc_server.mine_blocks(1); +type Inscribe = ord::subcommand::wallet::inscribe::Output; +type Etch = ord::subcommand::wallet::etch::Output; - let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --file foo.txt") - .write("foo.txt", "FOO") +fn create_wallet(rpc_server: &test_bitcoincore_rpc::Handle) { + CommandBuilder::new(format!("--chain {} wallet create", rpc_server.network())) .rpc_server(rpc_server) - .run_and_deserialize_output::(); - - rpc_server.mine_blocks(1); - - assert_eq!(output.inscriptions.len(), 1); - - (output.inscriptions[0].id, output.reveal) + .run_and_deserialize_output::(); } fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { @@ -85,10 +80,43 @@ fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { bitcoin::Witness::from_slice(&[script.into_bytes(), Vec::new()]) } -fn create_wallet(rpc_server: &test_bitcoincore_rpc::Handle) { - CommandBuilder::new(format!("--chain {} wallet create", rpc_server.network())) +fn etch(rpc_server: &test_bitcoincore_rpc::Handle, rune: Rune) -> Etch { + rpc_server.mine_blocks(1); + + let output = CommandBuilder::new( + format!( + "--index-runes-pre-alpha-i-agree-to-get-rekt --regtest wallet etch --rune {} --divisibility 0 --fee-rate 0 --supply 1000 --symbol ¢", + rune + ) + ) + .rpc_server(rpc_server) + .run_and_deserialize_output(); + + rpc_server.mine_blocks(1); + + output +} + +fn runes(rpc_server: &test_bitcoincore_rpc::Handle) -> BTreeMap { + CommandBuilder::new("--index-runes-pre-alpha-i-agree-to-get-rekt --regtest runes") .rpc_server(rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::() + .runes +} + +fn inscribe(rpc_server: &test_bitcoincore_rpc::Handle) -> (InscriptionId, Txid) { + rpc_server.mine_blocks(1); + + let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --file foo.txt") + .write("foo.txt", "FOO") + .rpc_server(rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + assert_eq!(output.inscriptions.len(), 1); + + (output.inscriptions[0].id, output.reveal) } mod command_builder; @@ -98,12 +126,14 @@ mod test_server; mod core; mod decode; mod epochs; +mod etch; mod find; mod index; mod info; mod json_api; mod list; mod parse; +mod runes; mod server; mod subsidy; mod supply; diff --git a/tests/runes.rs b/tests/runes.rs new file mode 100644 index 0000000000..b974ecbba7 --- /dev/null +++ b/tests/runes.rs @@ -0,0 +1,138 @@ +use {super::*, ord::subcommand::runes::Output}; + +#[test] +fn flag_is_required() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + CommandBuilder::new("--regtest runes") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: `ord runes` requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag\n") + .run_and_extract_stdout(); +} + +#[test] +fn no_runes() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + assert_eq!( + CommandBuilder::new("--index-runes-pre-alpha-i-agree-to-get-rekt --regtest runes") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(), + Output { + runes: BTreeMap::new(), + } + ); +} + +#[test] +fn one_rune() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + let etch = etch(&rpc_server, Rune(RUNE)); + + assert_eq!( + CommandBuilder::new("--index-runes-pre-alpha-i-agree-to-get-rekt --regtest runes") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(), + Output { + runes: vec![( + Rune(RUNE), + RuneInfo { + burned: 0, + divisibility: 0, + end: None, + etching: etch.transaction, + height: 2, + id: RuneId { + height: 2, + index: 1 + }, + index: 1, + limit: None, + number: 0, + rune: Rune(RUNE), + supply: 1000, + symbol: Some('¢'), + timestamp: ord::timestamp(2), + } + )] + .into_iter() + .collect(), + } + ); +} + +#[test] +fn two_runes() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + let a = etch(&rpc_server, Rune(RUNE)); + let b = etch(&rpc_server, Rune(RUNE + 1)); + + assert_eq!( + CommandBuilder::new("--index-runes-pre-alpha-i-agree-to-get-rekt --regtest runes") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(), + Output { + runes: vec![ + ( + Rune(RUNE), + RuneInfo { + burned: 0, + divisibility: 0, + end: None, + etching: a.transaction, + height: 2, + id: RuneId { + height: 2, + index: 1 + }, + index: 1, + limit: None, + number: 0, + rune: Rune(RUNE), + supply: 1000, + symbol: Some('¢'), + timestamp: ord::timestamp(2), + } + ), + ( + Rune(RUNE + 1), + RuneInfo { + burned: 0, + divisibility: 0, + end: None, + etching: b.transaction, + height: 4, + id: RuneId { + height: 4, + index: 1 + }, + index: 1, + limit: None, + number: 1, + rune: Rune(RUNE + 1), + supply: 1000, + symbol: Some('¢'), + timestamp: ord::timestamp(4), + } + ) + ] + .into_iter() + .collect(), + } + ); +} diff --git a/tests/wallet/balance.rs b/tests/wallet/balance.rs index 79557aa562..7b67825eab 100644 --- a/tests/wallet/balance.rs +++ b/tests/wallet/balance.rs @@ -18,23 +18,34 @@ fn wallet_balance() { assert_eq!( CommandBuilder::new("wallet balance") .rpc_server(&rpc_server) - .run_and_deserialize_output::() - .cardinal, - 50 * COIN_VALUE + .run_and_deserialize_output::(), + Output { + cardinal: 50 * COIN_VALUE, + ordinal: 0, + runic: None, + runes: None, + total: 50 * COIN_VALUE, + } ); } #[test] -fn wallet_balance_only_counts_cardinal_utxos() { +fn inscribed_utxos_are_deducted_from_cardinal() { let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); assert_eq!( CommandBuilder::new("wallet balance") .rpc_server(&rpc_server) - .run_and_deserialize_output::() - .cardinal, - 0 + .run_and_deserialize_output::(), + Output { + cardinal: 0, + ordinal: 0, + runic: None, + runes: None, + total: 0, + } ); inscribe(&rpc_server); @@ -42,8 +53,50 @@ fn wallet_balance_only_counts_cardinal_utxos() { assert_eq!( CommandBuilder::new("wallet balance") .rpc_server(&rpc_server) - .run_and_deserialize_output::() - .cardinal, - 100 * COIN_VALUE - 10_000 + .run_and_deserialize_output::(), + Output { + cardinal: 100 * COIN_VALUE - 10_000, + ordinal: 10_000, + runic: None, + runes: None, + total: 100 * COIN_VALUE, + } + ); +} + +#[test] +fn runic_utxos_are_deducted_from_cardinal() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + assert_eq!( + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(), + Output { + cardinal: 0, + ordinal: 0, + runic: Some(0), + runes: Some(BTreeMap::new()), + total: 0, + } + ); + + etch(&rpc_server, Rune(RUNE)); + + assert_eq!( + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt wallet balance") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(), + Output { + cardinal: 100 * COIN_VALUE - 10_000, + ordinal: 0, + runic: Some(10_000), + runes: Some(vec![(Rune(RUNE), 1000)].into_iter().collect()), + total: 100 * COIN_VALUE, + } ); } diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 3f189ffb1b..d5ed2f7c44 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1434,3 +1434,255 @@ fn batch_inscribe_works_with_some_destinations_set_and_others_not() {
bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
.*", ); } + +#[test] +fn batch_same_sat() { + let rpc_server = test_bitcoincore_rpc::spawn(); + rpc_server.mine_blocks(1); + + create_wallet(&rpc_server); + + let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: same-sat\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + rpc_server.mine_blocks(1); + + let ord_server = TestServer::spawn_with_args(&rpc_server, &[]); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:0
.*", + outpoint + ), + ); + + ord_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*
.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_same_sat_with_parent() { + let rpc_server = test_bitcoincore_rpc::spawn(); + rpc_server.mine_blocks(1); + + create_wallet(&rpc_server); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("mode: same-sat\nparent: {parent_id}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + rpc_server.mine_blocks(1); + + let ord_server = TestServer::spawn_with_args(&rpc_server, &[]); + + let txid = output.inscriptions[0].location.outpoint.txid; + + ord_server.assert_response_regex( + format!("/inscription/{}", parent_id), + format!( + r".*
location
.*
{}:0:0
.*", + txid + ), + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
location
.*
{}:1:0
.*", + txid + ), + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
location
.*
{}:1:0
.*", + txid + ), + ); + + ord_server.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
location
.*
{}:1:0
.*", + txid + ), + ); + + ord_server.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*", output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn inscribe_with_sat_arg() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(2); + + let Inscribe { inscriptions, .. } = CommandBuilder::new( + "--index-sats wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1", + ) + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .run_and_deserialize_output(); + + let inscription = inscriptions[0].id; + + rpc_server.mine_blocks(1); + + TestServer::spawn_with_args(&rpc_server, &["--index-sats"]).assert_response_regex( + "/sat/5010000000", + format!(".*.*"), + ); + + TestServer::spawn_with_args(&rpc_server, &[]) + .assert_response_regex(format!("/content/{inscription}",), "FOO"); +} + +#[test] +fn inscribe_with_sat_arg_fails_if_no_index_or_not_found() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + + CommandBuilder::new("wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1") + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: index must be built with `--index-sats` to use `--sat`\n") + .run_and_extract_stdout(); + + CommandBuilder::new("--index-sats wallet inscribe --sat 5000000000 --file foo.txt --fee-rate 1") + .write("foo.txt", "FOO") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: could not find sat `5000000000`\n") + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_with_sat_argument_with_parent() { + let rpc_server = test_bitcoincore_rpc::spawn(); + rpc_server.mine_blocks(1); + + assert_eq!(rpc_server.descriptors().len(), 0); + + create_wallet(&rpc_server); + + let parent_output = + CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + assert_eq!(rpc_server.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("--index-sats wallet inscribe --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + format!("parent: {parent_id}\nmode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + TestServer::spawn_with_args(&rpc_server, &["--index-sats"]).assert_response_regex( + "/sat/5000111111", + format!( + ".*.*.*.*", + output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id + ), + ); +} + +#[test] +fn batch_inscribe_with_sat_arg_fails_if_wrong_mode() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + CommandBuilder::new("wallet inscribe --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: shared-output\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: `sat` can only be set in `same-sat` mode\n") + .run_and_extract_stdout(); +} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 084608c72e..da3e106030 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -330,7 +330,7 @@ fn send_btc_with_fee_rate() { rpc_server.mine_blocks(1); - let output = CommandBuilder::new( + CommandBuilder::new( "wallet send --fee-rate 13.3 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", ) .rpc_server(&rpc_server) @@ -352,13 +352,6 @@ fn send_btc_with_fee_rate() { assert!(f64::abs(fee_rate - 13.3) < 0.1); - assert_eq!( - output.transaction, - "0000000000000000000000000000000000000000000000000000000000000000" - .parse() - .unwrap() - ); - assert_eq!( rpc_server.sent(), &[Sent { @@ -381,17 +374,9 @@ fn send_btc_locks_inscriptions() { let (_, reveal) = inscribe(&rpc_server); - let output = - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); - - assert_eq!( - output.transaction, - "0000000000000000000000000000000000000000000000000000000000000000" - .parse() - .unwrap() - ); + CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); assert_eq!( rpc_server.sent(), @@ -420,7 +405,7 @@ fn send_btc_fails_if_lock_unspent_fails() { CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") .rpc_server(&rpc_server) - .expected_stderr("error: failed to lock ordinal UTXOs\n") + .expected_stderr("error: failed to lock UTXOs\n") .expected_exit_code(1) .run_and_extract_stdout(); }