diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2f6a24dd..3a3335f2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,108 @@ Changelog ========= +[0.17.1](https://github.com/ordinals/ord/releases/tag/0.17.1) - 2023-04-01 +-------------------------------------------------------------------------- + +## Fixed +- Ignore invalid script pubkeys (#3432) + +## Misc +- Fix typo (#3429) +- Relax deployed Bitcoin Core relay rules (#3431) + +[0.17.0](https://github.com/ordinals/ord/releases/tag/0.17.0) - 2023-03-31 +-------------------------------------------------------------------------- + +### Added +- Allow pausing and resuming etchings ([#3374](https://github.com/ordinals/ord/pull/3374) by [raphjaph](https://github.com/raphjaph)) +- Seed index with genesis rune ([#3426](https://github.com/ordinals/ord/pull/3426) by [casey](https://github.com/casey)) +- Add `ord wallet batch` command ([#3401](https://github.com/ordinals/ord/pull/3401) by [casey](https://github.com/casey)) +- Return effective content type in JSON API ([#3289](https://github.com/ordinals/ord/pull/3289) by [arik-so](https://github.com/arik-so)) +- Mint terms ([#3375](https://github.com/ordinals/ord/pull/3375) by [casey](https://github.com/casey)) +- Allow supply-capped mints ([#3365](https://github.com/ordinals/ord/pull/3365) by [casey](https://github.com/casey)) +- Return runestone from `ord decode` ([#3349](https://github.com/ordinals/ord/pull/3349) by [casey](https://github.com/casey)) +- Display charms on /sat ([#3340](https://github.com/ordinals/ord/pull/3340) by [markovichecha](https://github.com/markovichecha)) +- Allow sending sat ([#3200](https://github.com/ordinals/ord/pull/3200) by [bingryan](https://github.com/bingryan)) +- Display mintability on /rune ([#3324](https://github.com/ordinals/ord/pull/3324) by [raphjaph](https://github.com/raphjaph)) +- Mint runes with wallet ([#3298](https://github.com/ordinals/ord/pull/3298) by [raphjaph](https://github.com/raphjaph)) +- Index multiple parents ([#3227](https://github.com/ordinals/ord/pull/3227) by [arik-so](https://github.com/arik-so)) +- Add fallback route ([#3288](https://github.com/ordinals/ord/pull/3288) by [casey](https://github.com/casey)) +- Allow looking up inscriptions by sat name ([#3286](https://github.com/ordinals/ord/pull/3286) by [casey](https://github.com/casey)) +- Allow generating multiple receive addresses ([#3277](https://github.com/ordinals/ord/pull/3277) by [bingryan](https://github.com/bingryan)) + +### Changed +- Recognized field without required flag produce cenotaphs ([#3422](https://github.com/ordinals/ord/pull/3422) by [casey](https://github.com/casey)) +- Rename test-bitcoincore-rpc to mockcore ([#3415](https://github.com/ordinals/ord/pull/3415) by [casey](https://github.com/casey)) +- Derive reserved rune names from rune ID ([#3412](https://github.com/ordinals/ord/pull/3412) by [casey](https://github.com/casey)) +- Don't complain about large runestones if --no-limit is passed ([#3402](https://github.com/ordinals/ord/pull/3402) by [casey](https://github.com/casey)) +- Move runes types into ordinals crate ([#3391](https://github.com/ordinals/ord/pull/3391) by [casey](https://github.com/casey)) +- Disambiguate when sending runes ([#3368](https://github.com/ordinals/ord/pull/3368) by [raphjaph](https://github.com/raphjaph)) +- Only allow sending sats by name ([#3344](https://github.com/ordinals/ord/pull/3344) by [casey](https://github.com/casey)) +- Downgrade from `beta` to `alpha` ([#3315](https://github.com/ordinals/ord/pull/3315) by [casey](https://github.com/casey)) + +### Misc +- Add links to status page ([#3361](https://github.com/ordinals/ord/pull/3361) by [lugondev](https://github.com/lugondev)) +- Document sending runes ([#3405](https://github.com/ordinals/ord/pull/3405) by [rot13maxi](https://github.com/rot13maxi)) +- Use checked arithmetic in RuneUpdater ([#3423](https://github.com/ordinals/ord/pull/3423) by [casey](https://github.com/casey)) +- Update Dockerfile Rust version ([#3425](https://github.com/ordinals/ord/pull/3425) by [0xspyop](https://github.com/0xspyop)) +- Don't conflate cenotaphs and runestones ([#3417](https://github.com/ordinals/ord/pull/3417) by [casey](https://github.com/casey)) +- Fix typos ([#3418](https://github.com/ordinals/ord/pull/3418) by [xiaoxianBoy](https://github.com/xiaoxianBoy)) +- Set pointer in etching runestone ([#3420](https://github.com/ordinals/ord/pull/3420) by [casey](https://github.com/casey)) +- Fix fuzz tests ([#3416](https://github.com/ordinals/ord/pull/3416) by [casey](https://github.com/casey)) +- Set relative lock height on etching transactions ([#3414](https://github.com/ordinals/ord/pull/3414) by [casey](https://github.com/casey)) +- Add CTRL-C test ([#3413](https://github.com/ordinals/ord/pull/3413) by [raphjaph](https://github.com/raphjaph)) +- Add etching to example batchfile ([#3407](https://github.com/ordinals/ord/pull/3407) by [casey](https://github.com/casey)) +- Fix inscribe_with_no_limit test ([#3403](https://github.com/ordinals/ord/pull/3403) by [casey](https://github.com/casey)) +- Rename Inscribe to Batch in integration tests ([#3404](https://github.com/ordinals/ord/pull/3404) by [casey](https://github.com/casey)) +- Distinguish invalid opcode and invalid script ([#3400](https://github.com/ordinals/ord/pull/3400) by [casey](https://github.com/casey)) +- Fix rune ID delta-encoding table ([#3393](https://github.com/ordinals/ord/pull/3393) by [chendatony31](https://github.com/chendatony31)) +- Handle invalid scripts correctly ([#3390](https://github.com/ordinals/ord/pull/3390) by [casey](https://github.com/casey)) +- Fix typo: Eching -> Etching ([#3397](https://github.com/ordinals/ord/pull/3397) by [gmart7t2](https://github.com/gmart7t2)) +- Fix typo: transactions -> transaction's ([#3398](https://github.com/ordinals/ord/pull/3398) by [gmart7t2](https://github.com/gmart7t2)) +- Fix typo: an -> a ([#3395](https://github.com/ordinals/ord/pull/3395) by [gmart7t2](https://github.com/gmart7t2)) +- Fix runes docs table ([#3389](https://github.com/ordinals/ord/pull/3389) by [casey](https://github.com/casey)) +- Document runes ([#3380](https://github.com/ordinals/ord/pull/3380) by [casey](https://github.com/casey)) +- Check mint runestone ([#3388](https://github.com/ordinals/ord/pull/3388) by [casey](https://github.com/casey)) +- Check send runestone ([#3386](https://github.com/ordinals/ord/pull/3386) by [casey](https://github.com/casey)) +- Decimal::to_amount → Decimal::to_integer ([#3382](https://github.com/ordinals/ord/pull/3382) by [casey](https://github.com/casey)) +- Add SpacedRune test case ([#3379](https://github.com/ordinals/ord/pull/3379) by [casey](https://github.com/casey)) +- Add Runestone::cenotaph() ([#3381](https://github.com/ordinals/ord/pull/3381) by [casey](https://github.com/casey)) +- Terms::limit → Terms::amount ([#3383](https://github.com/ordinals/ord/pull/3383) by [casey](https://github.com/casey)) +- Use default() as shorthand for Default::default() ([#3371](https://github.com/ordinals/ord/pull/3371) by [casey](https://github.com/casey)) +- Add batch module to wallet ([#3359](https://github.com/ordinals/ord/pull/3359) by [casey](https://github.com/casey)) +- Make rune parent clickable ([#3358](https://github.com/ordinals/ord/pull/3358) by [raphjaph](https://github.com/raphjaph)) +- Assert etched runestone is correct ([#3354](https://github.com/ordinals/ord/pull/3354) by [casey](https://github.com/casey)) +- Display spaced runes in balances ([#3353](https://github.com/ordinals/ord/pull/3353) by [casey](https://github.com/casey)) +- Cleanup ([#3348](https://github.com/ordinals/ord/pull/3348) by [lugondev](https://github.com/lugondev)) +- Fetch etching inputs using Bitcoin Core RPC ([#3336](https://github.com/ordinals/ord/pull/3336) by [raphjaph](https://github.com/raphjaph)) +- Update Chinese version of handbook ([#3334](https://github.com/ordinals/ord/pull/3334) by [DrJingLee](https://github.com/DrJingLee)) +- Use serde_with::DeserializeFromStr ([#3343](https://github.com/ordinals/ord/pull/3343) by [casey](https://github.com/casey)) +- Remove quotes from example ord env command ([#3335](https://github.com/ordinals/ord/pull/3335) by [casey](https://github.com/casey)) +- Initial runes review ([#3331](https://github.com/ordinals/ord/pull/3331) by [casey](https://github.com/casey)) +- Fix redundant locking ([#3342](https://github.com/ordinals/ord/pull/3342) by [raphjaph](https://github.com/raphjaph)) +- Derive Deserialize for Runestone ([#3339](https://github.com/ordinals/ord/pull/3339) by [emilcondrea](https://github.com/emilcondrea)) +- Update redb to 2.0.0 ([#3341](https://github.com/ordinals/ord/pull/3341) by [cberner](https://github.com/cberner)) +- Runestones with unknown semantics are cenotaphs ([#3325](https://github.com/ordinals/ord/pull/3325) by [casey](https://github.com/casey)) +- Reserve rune IDs with zero block and nonzero tx ([#3323](https://github.com/ordinals/ord/pull/3323) by [casey](https://github.com/casey)) +- Display rune premine ([#3313](https://github.com/ordinals/ord/pull/3313) by [raphjaph](https://github.com/raphjaph)) +- Make max mint limit u64::MAX ([#3316](https://github.com/ordinals/ord/pull/3316) by [casey](https://github.com/casey)) +- Change rune protocol identifier to OP_PUSHNUM_13 ([#3314](https://github.com/ordinals/ord/pull/3314) by [casey](https://github.com/casey)) +- Strict edicts ([#3312](https://github.com/ordinals/ord/pull/3312) by [casey](https://github.com/casey)) +- Allow premining with open etchings ([#3311](https://github.com/ordinals/ord/pull/3311) by [raphjaph](https://github.com/raphjaph)) +- Rename RuneID fields ([#3310](https://github.com/ordinals/ord/pull/3310) by [casey](https://github.com/casey)) +- Prevent front-running rune etchings ([#3212](https://github.com/ordinals/ord/pull/3212) by [casey](https://github.com/casey)) +- Clarify build instructions ([#3304](https://github.com/ordinals/ord/pull/3304) by [raphjaph](https://github.com/raphjaph)) +- Add test to choose the the earliest of deadline or end ([#3254](https://github.com/ordinals/ord/pull/3254) by [sondotpin](https://github.com/sondotpin)) +- Ensure inscription tags are unique ([#3296](https://github.com/ordinals/ord/pull/3296) by [casey](https://github.com/casey)) +- Include CSP origin in preview content security policy headers ([#3276](https://github.com/ordinals/ord/pull/3276) by [bingryan](https://github.com/bingryan)) +- Add pre-commit hook ([#3262](https://github.com/ordinals/ord/pull/3262) by [bingryan](https://github.com/bingryan)) +- Fix querying for inscriptions by sat names containing `i` ([#3287](https://github.com/ordinals/ord/pull/3287) by [casey](https://github.com/casey)) +- Switch recommended flag usage from `--data-dir` to `--datadir` ([#3281](https://github.com/ordinals/ord/pull/3281) by [chasefleming](https://github.com/chasefleming)) +- Better wallet error message ([#3272](https://github.com/ordinals/ord/pull/3272) by [bingryan](https://github.com/bingryan)) +- Add recipe to delete indices ([#3266](https://github.com/ordinals/ord/pull/3266) by [casey](https://github.com/casey)) +- Bump ordinals version: 0.0.3 → 0.0.4 ([#3267](https://github.com/ordinals/ord/pull/3267) by [casey](https://github.com/casey)) + [0.16.0](https://github.com/ordinals/ord/releases/tag/0.16.0) - 2023-03-11 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 891bdfcf54..cf098c6f1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -112,18 +112,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" dependencies = [ "backtrace", ] [[package]] name = "arc-swap" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "array-init" @@ -181,16 +181,16 @@ checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener-strategy 0.5.1", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "86a9249d1447a85f95810c620abea82e001fe58a31713fcce614caf52499f905" dependencies = [ "brotli", "flate2", @@ -272,13 +272,13 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -297,7 +297,7 @@ dependencies = [ "js-sys", "lazy_static", "log", - "rustls 0.22.2", + "rustls 0.22.3", "rustls-pki-types", "thiserror", "wasm-bindgen", @@ -363,9 +363,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axum" @@ -438,9 +438,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -536,9 +536,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -558,9 +558,9 @@ dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", - "fastrand 2.0.1", + "fastrand 2.0.2", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.3.0", "piper", "tracing", ] @@ -576,14 +576,14 @@ dependencies = [ "new_mime_guess", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] name = "brotli" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -624,9 +624,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cast" @@ -660,9 +660,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -702,9 +702,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -724,14 +724,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -950,7 +950,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -972,7 +972,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core 0.20.8", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -1002,6 +1002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1102,7 +1103,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -1205,9 +1206,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" dependencies = [ "event-listener 5.2.0", "pin-project-lite", @@ -1230,9 +1231,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "flate2" @@ -1339,9 +1340,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "futures-core", "pin-project-lite", @@ -1355,7 +1356,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -1365,7 +1366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" dependencies = [ "futures-io", - "rustls 0.22.2", + "rustls 0.22.3", "rustls-pki-types", ] @@ -1484,9 +1485,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -1494,7 +1495,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util 0.7.10", @@ -1511,6 +1512,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -1523,6 +1530,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1682,12 +1695,24 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", + "serde", ] [[package]] @@ -1751,9 +1776,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -1859,13 +1884,12 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", - "redox_syscall 0.4.1", ] [[package]] @@ -1910,9 +1934,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -1966,6 +1990,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockcore" +version = "0.0.1" +dependencies = [ + "base64 0.21.7", + "bitcoin", + "hex", + "jsonrpc-core", + "jsonrpc-derive", + "jsonrpc-http-server", + "ord-bitcoincore-rpc", + "reqwest", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "mp4" version = "0.14.0" @@ -2031,7 +2072,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if 1.0.0", "cfg_aliases", "libc", @@ -2162,7 +2203,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if 1.0.0", "foreign-types", "libc", @@ -2179,7 +2220,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -2190,9 +2231,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2208,7 +2249,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.16.0" +version = "0.17.1" dependencies = [ "anyhow", "async-trait", @@ -2241,7 +2282,9 @@ dependencies = [ "mime", "mime_guess", "miniscript", + "mockcore", "mp4", + "nix", "ord-bitcoincore-rpc", "ordinals", "pretty_assertions", @@ -2251,21 +2294,22 @@ dependencies = [ "reqwest", "rss", "rust-embed", - "rustls 0.22.2", + "rustls 0.22.3", "rustls-acme", "serde", "serde-hex", "serde_json", + "serde_with", "serde_yaml", "sha3", "sysinfo", "tempfile", - "test-bitcoincore-rpc", "tokio", "tokio-stream", "tokio-util 0.7.10", "tower-http", "unindent", + "urlencoding", ] [[package]] @@ -2296,12 +2340,14 @@ dependencies = [ [[package]] name = "ordinals" -version = "0.0.4" +version = "0.0.6" dependencies = [ "bitcoin", "derive_more", + "pretty_assertions", "serde", "serde_json", + "serde_with", "thiserror", ] @@ -2331,8 +2377,8 @@ dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.16", - "smallvec 1.13.1", + "redox_syscall", + "smallvec 1.13.2", "winapi", ] @@ -2368,14 +2414,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2390,7 +2436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.0.2", "futures-io", ] @@ -2483,9 +2529,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -2496,7 +2542,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -2560,9 +2606,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2592,9 +2638,9 @@ dependencies = [ [[package]] name = "redb" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72623e6275cd430215b741f41ebda34db93a13ebde253f908b70871c46afc5ba" +checksum = "a1100a056c5dcdd4e5513d5333385223b26ef1bf92f31eb38f407e8c20549256" dependencies = [ "libc", ] @@ -2608,20 +2654,11 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2630,9 +2667,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -2653,15 +2690,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.25" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "async-compression", "base64 0.21.7", @@ -2761,7 +2798,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.52", + "syn 2.0.57", "walkdir", ] @@ -2815,11 +2852,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys 0.4.13", @@ -2840,9 +2877,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" dependencies = [ "log", "ring 0.17.8", @@ -2892,9 +2929,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] name = "rustls-webpki" @@ -2986,9 +3023,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -2999,9 +3036,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3041,16 +3078,16 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -3078,13 +3115,43 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +dependencies = [ + "darling 0.20.8", + "proc-macro2", + "quote", + "syn 2.0.57", +] + [[package]] name = "serde_yaml" -version = "0.9.32" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -3132,9 +3199,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -3195,7 +3262,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -3221,9 +3288,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", @@ -3265,20 +3332,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ "core-foundation-sys", "libc", @@ -3291,46 +3358,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if 1.0.0", - "fastrand 2.0.1", - "rustix 0.38.31", + "fastrand 2.0.2", + "rustix 0.38.32", "windows-sys 0.52.0", ] -[[package]] -name = "test-bitcoincore-rpc" -version = "0.0.1" -dependencies = [ - "base64 0.21.7", - "bitcoin", - "hex", - "jsonrpc-core", - "jsonrpc-derive", - "jsonrpc-http-server", - "ord-bitcoincore-rpc", - "reqwest", - "serde", - "serde_json", - "tempfile", -] - [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -3391,9 +3441,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -3414,7 +3464,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", ] [[package]] @@ -3439,9 +3489,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -3510,7 +3560,7 @@ checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "async-compression", "base64 0.21.7", - "bitflags 2.4.2", + "bitflags 2.5.0", "bytes", "futures-core", "futures-util", @@ -3619,9 +3669,9 @@ checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "unsafe-libyaml" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" @@ -3653,6 +3703,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -3729,7 +3785,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", "wasm-bindgen-shared", ] @@ -3763,7 +3819,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.57", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index a3cb953593..1a734153c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.16.0" +version = "0.17.1" license = "CC0-1.0" edition = "2021" autotests = false @@ -49,8 +49,8 @@ mime_guess = "2.0.4" miniscript = "10.0.0" mp4 = "0.14.0" ord-bitcoincore-rpc = "0.17.2" -ordinals = { version = "0.0.4", path = "crates/ordinals" } -redb = "1.5.0" +ordinals = { version = "0.0.6", path = "crates/ordinals" } +redb = "2.0.0" regex = "1.6.0" reqwest = { version = "0.11.23", features = ["blocking", "json"] } rss = "2.0.1" @@ -60,6 +60,7 @@ rustls-acme = { version = "0.8.1", features = ["axum"] } serde = { version = "1.0.137", features = ["derive"] } serde-hex = "0.1.0" serde_json = { version = "1.0.81", features = ["preserve_order"] } +serde_with = "3.7.0" serde_yaml = "0.9.17" sha3 = "0.10.8" sysinfo = "0.30.3" @@ -68,19 +69,17 @@ tokio = { version = "1.17.0", features = ["rt-multi-thread"] } tokio-stream = "0.1.9" tokio-util = {version = "0.7.3", features = ["compat"] } tower-http = { version = "0.4.0", features = ["auth", "compression-br", "compression-gzip", "cors", "set-header"] } +urlencoding = "2.1.3" [dev-dependencies] criterion = "0.5.1" executable-path = "1.0.0" +nix = { version = "0.28.0", features = ["signal"] } pretty_assertions = "1.2.1" reqwest = { version = "0.11.10", features = ["blocking", "brotli", "json"] } -test-bitcoincore-rpc = { path = "crates/test-bitcoincore-rpc" } +mockcore = { path = "crates/mockcore" } unindent = "0.2.1" -[[bench]] -name = "server" -harness = false - [[bin]] name = "ord" path = "src/bin/main.rs" diff --git a/Dockerfile b/Dockerfile index 779e5f794a..af187a7b87 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.75.0-bookworm as builder +FROM rust:1.76.0-bookworm as builder WORKDIR /usr/src/ord diff --git a/README.md b/README.md index 2db3362b05..3ce34f8fec 100644 --- a/README.md +++ b/README.md @@ -99,11 +99,22 @@ You'll also need Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -To build `ord` from source: +Clone the `ord` repo: ``` git clone https://github.com/ordinals/ord.git cd ord +``` + +To build a specific version of `ord`, first checkout that version: + +``` +git checkout +``` + +And finally to actually build `ord`: + +``` cargo build --release ``` diff --git a/batch.yaml b/batch.yaml index 9c9e94561f..707d2ef76b 100644 --- a/batch.yaml +++ b/batch.yaml @@ -19,6 +19,33 @@ reinscribe: true # sat to inscribe on, can only be used with `same-sat`: # sat: 5000000000 +# rune to etch (optional) +etching: + # rune name + rune: THE•BEST•RUNE + # allow subdividing super-unit into `10^divisibility` sub-units + divisibility: 2 + # premine + premine: 1000.00 + # total supply, must be equal to `premine + terms.cap * terms.amount` + supply: 10000.00 + # currency symbol + symbol: $ + # mint terms (optional) + terms: + # amount per mint + amount: 100.00 + # maximum number of mints + cap: 90 + # mint start and end absolute block height (optional) + height: + start: 840000 + end: 850000 + # mint start and end block height relative to etching height (optional) + offset: + start: 1000 + end: 9000 + # inscriptions to inscribe inscriptions: # path to inscription content @@ -36,10 +63,10 @@ inscriptions: metus est et odio. Nullam venenatis, urna et molestie vestibulum, orci mi efficitur risus, eu malesuada diam lorem sed velit. Nam fermentum dolor et luctus euismod. - # inscription metaprotocol (optional) - metaprotocol: DOPEPROTOCOL-42069 - file: token.json + # inscription metaprotocol (optional) + metaprotocol: DOPEPROTOCOL-42069 - file: tulip.png destination: bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6 diff --git a/benches/server.rs b/benches/server.rs deleted file mode 100644 index b6cc2fdace..0000000000 --- a/benches/server.rs +++ /dev/null @@ -1,16 +0,0 @@ -use {criterion::Criterion, ord::Index}; - -fn main() { - let mut criterion = Criterion::default().configure_from_args(); - let index = Index::open(&Default::default()).unwrap(); - let mut i = 0; - - criterion.bench_function("inscription", |b| { - b.iter(|| { - Index::inscription_info_benchmark(&index, i); - i += 1; - }); - }); - - Criterion::default().configure_from_args().final_summary(); -} diff --git a/bin/benchmark b/bin/benchmark index a159a6444e..78370f784d 100755 --- a/bin/benchmark +++ b/bin/benchmark @@ -12,4 +12,4 @@ cp $INDEX_SNAPSHOT tmp/benchmark/index.redb cargo build --release -time ./target/release/ord --data-dir tmp/benchmark --height-limit $HEIGHT_LIMIT index +time ./target/release/ord --datadir tmp/benchmark --height-limit $HEIGHT_LIMIT index diff --git a/bin/flamegraph b/bin/flamegraph index fe4a3b9f61..e81ccdb4d6 100755 --- a/bin/flamegraph +++ b/bin/flamegraph @@ -13,7 +13,7 @@ sudo \ --bin ord \ -- \ --chain signet \ - --data-dir . \ + --datadir . \ --height-limit 0 \ index @@ -27,7 +27,7 @@ rm -f flamegraph.svg --bin ord \ -- \ --chain signet \ - --data-dir . \ + --datadir . \ --height-limit 5000 \ index diff --git a/bin/forbid b/bin/forbid index a5a82f116e..4eded0bf2f 100755 --- a/bin/forbid +++ b/bin/forbid @@ -2,7 +2,10 @@ set -euo pipefail -which rg > /dev/null +if ! which rg > /dev/null; then + echo "error: ripgrep (rg) not found" + exit 1 +fi ! rg \ --glob '!bin/forbid' \ diff --git a/crates/test-bitcoincore-rpc/Cargo.toml b/crates/mockcore/Cargo.toml similarity index 87% rename from crates/test-bitcoincore-rpc/Cargo.toml rename to crates/mockcore/Cargo.toml index c66f81ef2b..9f76e6ee19 100644 --- a/crates/test-bitcoincore-rpc/Cargo.toml +++ b/crates/mockcore/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "test-bitcoincore-rpc" -description = "Test Bitcoin Core RPC server" +name = "mockcore" +description = "Mock Bitcoin Core RPC server" version = "0.0.1" edition = "2021" license = "CC0-1.0" diff --git a/crates/test-bitcoincore-rpc/src/api.rs b/crates/mockcore/src/api.rs similarity index 98% rename from crates/test-bitcoincore-rpc/src/api.rs rename to crates/mockcore/src/api.rs index 8ca8f32a37..2ebe549ea6 100644 --- a/crates/test-bitcoincore-rpc/src/api.rs +++ b/crates/mockcore/src/api.rs @@ -12,6 +12,9 @@ pub trait Api { #[rpc(name = "getbalances")] fn get_balances(&self) -> Result; + #[rpc(name = "getbestblockhash")] + fn get_best_block_hash(&self) -> Result; + #[rpc(name = "getblockhash")] fn get_block_hash(&self, height: usize) -> Result; diff --git a/crates/test-bitcoincore-rpc/src/lib.rs b/crates/mockcore/src/lib.rs similarity index 85% rename from crates/test-bitcoincore-rpc/src/lib.rs rename to crates/mockcore/src/lib.rs index e13a1e0c81..d6fc17808c 100644 --- a/crates/test-bitcoincore-rpc/src/lib.rs +++ b/crates/mockcore/src/lib.rs @@ -135,9 +135,14 @@ pub struct TransactionTemplate<'a> { pub op_return_index: Option, pub output_values: &'a [u64], pub outputs: usize, + pub p2tr: bool, } +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/lib.rs #[derive(Serialize, Deserialize)] +======== +#[derive(Serialize, Deserialize, Debug)] +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/lib.rs pub struct JsonOutPoint { txid: Txid, vout: u32, @@ -181,6 +186,7 @@ impl<'a> Default for TransactionTemplate<'a> { op_return_index: None, output_values: &[], outputs: 1, + p2tr: false, } } } @@ -197,7 +203,17 @@ impl Handle { format!("http://127.0.0.1:{}", self.port) } - fn state(&self) -> MutexGuard { + pub fn address(&self, output: OutPoint) -> Address { + let state = self.state(); + + Address::from_script( + &state.transactions.get(&output.txid).unwrap().output[output.vout as usize].script_pubkey, + state.network, + ) + .unwrap() + } + + pub fn state(&self) -> MutexGuard { self.state.lock().unwrap() } @@ -209,15 +225,19 @@ impl Handle { self.state().wallets.clone() } + #[track_caller] pub fn mine_blocks(&self, n: u64) -> Vec { self.mine_blocks_with_subsidy(n, 50 * COIN_VALUE) } + #[track_caller] pub fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec { let mut bitcoin_rpc_data = self.state(); - (0..n) - .map(|_| bitcoin_rpc_data.push_block(subsidy)) - .collect() + let mut blocks = Vec::new(); + for _ in 0..n { + blocks.push(bitcoin_rpc_data.mine_block(subsidy)); + } + blocks } pub fn broadcast_tx(&self, template: TransactionTemplate) -> Txid { @@ -236,9 +256,25 @@ impl Handle { self.state().utxos.get(outpoint).cloned() } - pub fn tx(&self, bi: usize, ti: usize) -> Transaction { + #[track_caller] + pub fn tx(&self, block: usize, transaction: usize) -> Transaction { let state = self.state(); - state.blocks[&state.hashes[bi]].txdata[ti].clone() + let blockhash = state.hashes.get(block).expect("block index out of bounds"); + state.blocks[blockhash] + .txdata + .get(transaction) + .expect("transaction index out of bounds") + .clone() + } + + #[track_caller] + pub fn tx_by_id(&self, txid: Txid) -> Transaction { + self + .state() + .transactions + .get(&txid) + .expect("unknown transaction") + .clone() } pub fn mempool(&self) -> Vec { @@ -271,10 +307,6 @@ impl Handle { self.state().loaded_wallets.clone() } - pub fn change_addresses(&self) -> Vec
{ - self.state().change_addresses.clone() - } - pub fn cookie_file(&self) -> PathBuf { self.tempdir.path().join(".cookie") } diff --git a/crates/test-bitcoincore-rpc/src/server.rs b/crates/mockcore/src/server.rs similarity index 77% rename from crates/test-bitcoincore-rpc/src/server.rs rename to crates/mockcore/src/server.rs index 23a7af2c50..4b28bdf0e0 100644 --- a/crates/test-bitcoincore-rpc/src/server.rs +++ b/crates/mockcore/src/server.rs @@ -1,12 +1,16 @@ use { super::*, base64::Engine, +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs bitcoin::{ consensus::Decodable, psbt::Psbt, secp256k1::{rand, KeyPair, Secp256k1, XOnlyPublicKey}, Witness, }, +======== + bitcoin::{consensus::Decodable, psbt::Psbt, Witness}, +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs std::io::Cursor, }; @@ -46,6 +50,13 @@ impl Api for Server { }) } + fn get_best_block_hash(&self) -> Result { + match self.state().hashes.last() { + Some(block_hash) => Ok(*block_hash), + None => Err(Self::not_found()), + } + } + fn get_blockchain_info(&self) -> Result { Ok(GetBlockchainInfoResult { chain: String::from(match self.network { @@ -221,6 +232,7 @@ impl Api for Server { vout: u32, _include_mempool: Option, ) -> Result, jsonrpc_core::Error> { +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs Ok( self .state() @@ -241,6 +253,38 @@ impl Api for Server { coinbase: false, }), ) +======== + let state = self.state(); + + let Some(value) = state.utxos.get(&OutPoint { txid, vout }) else { + return Ok(None); + }; + + let mut confirmations = None; + + for (height, hash) in state.hashes.iter().enumerate() { + for tx in &state.blocks[hash].txdata { + if tx.txid() == txid { + confirmations = Some(state.hashes.len() - height); + } + } + } + + Ok(Some(GetTxOutResult { + bestblock: BlockHash::all_zeros(), + coinbase: false, + confirmations: confirmations.unwrap().try_into().unwrap(), + script_pub_key: GetRawTransactionResultVoutScriptPubKey { + asm: String::new(), + hex: Vec::new(), + req_sigs: None, + type_: None, + addresses: Vec::new(), + address: None, + }, + value: *value, + })) +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs } fn get_wallet_info(&self) -> Result { @@ -343,7 +387,7 @@ impl Api for Server { Some(transaction.output.len().try_into().unwrap()) ); - let state = self.state(); + let mut state = self.state(); let output_value = transaction .output @@ -364,33 +408,60 @@ impl Api for Server { .map(|txin| state.utxos.get(&txin.previous_output).unwrap().to_sat()) .sum::(); - let shortfall = output_value.saturating_sub(input_value); - utxos.sort(); utxos.reverse(); - if shortfall > 0 { - let (additional_input_value, outpoint) = utxos - .iter() - .find(|(value, outpoint)| value.to_sat() >= shortfall && !state.locked.contains(outpoint)) - .ok_or_else(Self::not_found)?; + if output_value > input_value { + for (value, outpoint) in utxos { + if state.locked.contains(&outpoint) { + continue; + } - transaction.input.push(TxIn { - previous_output: *outpoint, - script_sig: ScriptBuf::new(), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - witness: Witness::default(), - }); + let tx = state.transactions.get(&outpoint.txid).unwrap(); + + let tx_out = &tx.output[usize::try_from(outpoint.vout).unwrap()]; - input_value += additional_input_value.to_sat(); + let Ok(address) = Address::from_script(&tx_out.script_pubkey, state.network) else { + continue; + }; + + if !state.is_wallet_address(&address) { + continue; + } + + transaction.input.push(TxIn { + previous_output: outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + }); + + input_value += value.to_sat(); + + if input_value > output_value { + break; + } + } + + if output_value > input_value { + return Err(jsonrpc_core::Error { + code: jsonrpc_core::ErrorCode::ServerError(-6), + message: "insufficent funds".into(), + data: None, + }); + } } let change_position = transaction.output.len() as i32; - transaction.output.push(TxOut { - value: input_value - output_value, - script_pubkey: ScriptBuf::new(), - }); + let change = input_value - output_value; + + if change > 0 { + transaction.output.push(TxOut { + value: change, + script_pubkey: state.new_address(true).into(), + }); + } let fee = if let Some(fee_rate) = options.fee_rate { // increase vsize to account for the witness that `fundrawtransaction` will add @@ -438,7 +509,34 @@ impl Api for Server { fn send_raw_transaction(&self, tx: String) -> Result { let tx: Transaction = deserialize(&hex::decode(tx).unwrap()).unwrap(); +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs self.state.lock().unwrap().mempool.push(tx.clone()); +======== + let mut state = self.state.lock().unwrap(); + + for tx_in in &tx.input { + if let Some(lock_time) = tx_in.sequence.to_relative_lock_time() { + match lock_time { + bitcoin::relative::LockTime::Blocks(blocks) => { + if state + .txid_to_block_height + .get(&tx_in.previous_output.txid) + .expect("input has not been miined") + + u32::from(blocks.value()) + > u32::try_from(state.hashes.len()).unwrap() + { + panic!("input is locked"); + } + } + bitcoin::relative::LockTime::Time(_) => { + panic!("time-based relative locktimes are not implemented") + } + } + } + } + + state.mempool.push(tx.clone()); +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs Ok(tx.txid().to_string()) } @@ -519,32 +617,46 @@ impl Api for Server { txid: Txid, _include_watchonly: Option, ) -> Result { - match self.state.lock().unwrap().transactions.get(&txid) { - Some(tx) => Ok( - serde_json::to_value(GetTransactionResult { - info: WalletTxInfo { - txid, - confirmations: 0, - time: 0, - timereceived: 0, - blockhash: None, - blockindex: None, - blockheight: None, - blocktime: None, - wallet_conflicts: Vec::new(), - bip125_replaceable: Bip125Replaceable::Unknown, - }, - amount: SignedAmount::from_sat(0), - fee: None, - details: Vec::new(), - hex: serialize(tx), - }) - .unwrap(), - ), - None => Err(jsonrpc_core::Error::new( + let state = self.state(); + + let Some(tx) = state.transactions.get(&txid) else { + return Err(jsonrpc_core::Error::new( jsonrpc_core::types::error::ErrorCode::ServerError(-8), - )), + )); + }; + + let mut confirmations = None; + + 'outer: for (height, hash) in state.hashes.iter().enumerate() { + for tx in &state.blocks[hash].txdata { + if tx.txid() == txid { + confirmations = Some(state.hashes.len() - height); + break 'outer; + } + } } + + Ok( + serde_json::to_value(GetTransactionResult { + info: WalletTxInfo { + txid, + confirmations: confirmations.unwrap().try_into().unwrap(), + time: 0, + timereceived: 0, + blockhash: None, + blockindex: None, + blockheight: None, + blocktime: None, + wallet_conflicts: Vec::new(), + bip125_replaceable: Bip125Replaceable::Unknown, + }, + amount: SignedAmount::from_sat(0), + fee: None, + details: Vec::new(), + hex: serialize(tx), + }) + .unwrap(), + ) } fn get_raw_transaction( @@ -554,8 +666,19 @@ impl Api for Server { blockhash: Option, ) -> Result { assert_eq!(blockhash, None, "Blockhash param is unsupported"); + let state = self.state(); + let current_height: u32 = (state.hashes.len() - 1).try_into().unwrap(); + let confirmations = state + .txid_to_block_height + .get(&txid) + .map(|tx_height| current_height - tx_height); + if verbose.unwrap_or(false) { +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs match self.state().transactions.get(&txid) { +======== + match state.transactions.get(&txid) { +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs Some(transaction) => Ok( serde_json::to_value(GetRawTransactionResult { in_active_chain: Some(true), @@ -575,8 +698,13 @@ impl Api for Server { n: n.try_into().unwrap(), value: Amount::from_sat(output.value), script_pub_key: GetRawTransactionResultVoutScriptPubKey { +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs asm: String::new(), hex: Vec::new(), +======== + asm: output.script_pubkey.to_asm_string(), + hex: output.script_pubkey.clone().into(), +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs req_sigs: None, type_: None, addresses: Vec::new(), @@ -585,7 +713,7 @@ impl Api for Server { }) .collect(), blockhash: None, - confirmations: Some(1), + confirmations, time: None, blocktime: None, }) @@ -594,7 +722,7 @@ impl Api for Server { None => Err(Self::not_found()), } } else { - match self.state().transactions.get(&txid) { + match state.transactions.get(&txid) { Some(tx) => Ok(Value::String(hex::encode(serialize(tx)))), None => Err(Self::not_found()), } @@ -617,28 +745,43 @@ impl Api for Server { let state = self.state(); - Ok( - state - .utxos - .iter() - .filter(|(outpoint, _amount)| !state.locked.contains(outpoint)) - .map(|(outpoint, &amount)| ListUnspentResultEntry { - txid: outpoint.txid, - vout: outpoint.vout, - address: None, - label: None, - redeem_script: None, - witness_script: None, - script_pub_key: ScriptBuf::new(), - amount, - confirmations: 0, - spendable: true, - solvable: true, - descriptor: None, - safe: true, - }) - .collect(), - ) + let mut unspent = Vec::new(); + + for (outpoint, &amount) in &state.utxos { + if state.locked.contains(outpoint) { + continue; + } + + let tx = state.transactions.get(&outpoint.txid).unwrap(); + + let tx_out = &tx.output[usize::try_from(outpoint.vout).unwrap()]; + + let Ok(address) = Address::from_script(&tx_out.script_pubkey, state.network) else { + continue; + }; + + if !state.is_wallet_address(&address) { + continue; + } + + unspent.push(ListUnspentResultEntry { + txid: outpoint.txid, + vout: outpoint.vout, + address: None, + label: None, + redeem_script: None, + witness_script: None, + script_pub_key: ScriptBuf::new(), + amount, + confirmations: 0, + spendable: true, + solvable: true, + descriptor: None, + safe: true, + }); + } + + Ok(unspent) } fn list_lock_unspent(&self) -> Result, jsonrpc_core::Error> { @@ -656,13 +799,7 @@ impl Api for Server { &self, _address_type: Option, ) -> Result { - let secp256k1 = Secp256k1::new(); - let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); - let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); - let address = Address::p2tr(&secp256k1, public_key, None, self.network); - self.state().change_addresses.push(address.clone()); - - Ok(address) + Ok(self.state().new_address(true)) } fn get_descriptor_info( @@ -699,12 +836,16 @@ impl Api for Server { _label: Option, _address_type: Option, ) -> Result { +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs let secp256k1 = Secp256k1::new(); let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); let address = Address::p2tr(&secp256k1, public_key, None, self.network); Ok(address) +======== + Ok(self.state().new_address(false)) +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs } fn list_transactions( @@ -770,7 +911,7 @@ impl Api for Server { txid: output.txid, }; assert!(state.utxos.contains_key(&output)); - state.locked.insert(output); + assert!(state.locked.insert(output)); } Ok(true) diff --git a/crates/test-bitcoincore-rpc/src/state.rs b/crates/mockcore/src/state.rs similarity index 64% rename from crates/test-bitcoincore-rpc/src/state.rs rename to crates/mockcore/src/state.rs index 72bf46bd7c..c317462d51 100644 --- a/crates/test-bitcoincore-rpc/src/state.rs +++ b/crates/mockcore/src/state.rs @@ -1,6 +1,14 @@ -use super::*; +use { + super::*, + bitcoin::{ + key::{KeyPair, Secp256k1, XOnlyPublicKey}, + secp256k1::rand, + WPubkeyHash, + }, +}; #[derive(Debug)] +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/state.rs pub(crate) struct State { pub(crate) blocks: BTreeMap, pub(crate) change_addresses: Vec
, @@ -16,6 +24,25 @@ pub(crate) struct State { pub(crate) utxos: BTreeMap, pub(crate) version: usize, pub(crate) wallets: BTreeSet, +======== +pub struct State { + pub blocks: BTreeMap, + pub descriptors: Vec, + pub fail_lock_unspent: bool, + pub hashes: Vec, + pub loaded_wallets: BTreeSet, + pub locked: BTreeSet, + pub mempool: Vec, + pub network: Network, + pub nonce: u32, + pub transactions: BTreeMap, + pub txid_to_block_height: BTreeMap, + pub utxos: BTreeMap, + pub version: usize, + pub receive_addresses: Vec
, + pub change_addresses: Vec
, + pub wallets: BTreeSet, +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/state.rs } impl State { @@ -34,23 +61,70 @@ impl State { descriptors: Vec::new(), fail_lock_unspent, hashes, + loaded_wallets: BTreeSet::new(), locked: BTreeSet::new(), mempool: Vec::new(), network, nonce: 0, +<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/state.rs +======== + receive_addresses: Vec::new(), +>>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/state.rs transactions: BTreeMap::new(), + txid_to_block_height: BTreeMap::new(), utxos: BTreeMap::new(), version, wallets: BTreeSet::new(), - loaded_wallets: BTreeSet::new(), } } + pub(crate) fn new_address(&mut self, change: bool) -> Address { + let secp256k1 = Secp256k1::new(); + let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); + let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); + let address = Address::p2tr(&secp256k1, public_key, None, self.network); + if change { + &mut self.change_addresses + } else { + &mut self.receive_addresses + } + .push(address.clone()); + address + } + + pub fn is_wallet_address(&self, address: &Address) -> bool { + self.receive_addresses.contains(address) || self.change_addresses.contains(address) + } + pub(crate) fn clear(&mut self) { *self = Self::new(self.network, self.version, self.fail_lock_unspent); } - pub(crate) fn push_block(&mut self, subsidy: u64) -> Block { + #[track_caller] + pub fn balances(&self) -> BTreeMap> { + let mut addresses: BTreeMap> = BTreeMap::new(); + + for (&outpoint, &amount) in &self.utxos { + let transaction = self.transactions.get(&outpoint.txid).unwrap(); + let tx_out = &transaction.output[usize::try_from(outpoint.vout).unwrap()]; + + if tx_out.script_pubkey == ScriptBuf::new() { + continue; + } + + let address = Address::from_script(&tx_out.script_pubkey, self.network).unwrap(); + + addresses + .entry(address) + .or_default() + .push((outpoint, amount)); + } + + addresses + } + + #[track_caller] + pub(crate) fn mine_block(&mut self, subsidy: u64) -> Block { let coinbase = Transaction { version: 2, lock_time: LockTime::ZERO, @@ -83,7 +157,7 @@ impl State { fee }) .sum::(), - script_pubkey: ScriptBuf::new(), + script_pubkey: self.new_address(false).into(), }], }; @@ -104,8 +178,14 @@ impl State { }; for tx in block.txdata.iter() { + self + .txid_to_block_height + .insert(tx.txid(), self.hashes.len().try_into().unwrap()); + for input in tx.input.iter() { - self.utxos.remove(&input.previous_output); + if !input.previous_output.is_null() { + assert!(self.utxos.remove(&input.previous_output).is_some()); + } } for (vout, txout) in tx.output.iter().enumerate() { @@ -173,7 +253,14 @@ impl State { .get(i) .cloned() .unwrap_or(value_per_output), - script_pubkey: script::Builder::new().into_script(), + script_pubkey: if template.p2tr { + let secp = Secp256k1::new(); + let keypair = KeyPair::new(&secp, &mut rand::thread_rng()); + let internal_key = XOnlyPublicKey::from_keypair(&keypair); + ScriptBuf::new_v1_p2tr(&secp, internal_key.0, None) + } else { + ScriptBuf::new_v0_p2wpkh(&WPubkeyHash::all_zeros()) + }, }) .collect(), }; diff --git a/crates/ordinals/Cargo.toml b/crates/ordinals/Cargo.toml index bb13ec2729..200ecd7bd9 100644 --- a/crates/ordinals/Cargo.toml +++ b/crates/ordinals/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ordinals" -version = "0.0.4" +version = "0.0.6" edition = "2021" description = "Library for interoperating with ordinals and inscriptions" homepage = "https://github.com/ordinals/ord" @@ -12,7 +12,9 @@ rust-version = "1.67" bitcoin = { version = "0.30.1", features = ["rand"] } derive_more = "0.99.17" serde = { version = "1.0.137", features = ["derive"] } +serde_with = "3.7.0" thiserror = "1.0.56" [dev-dependencies] serde_json = { version = "1.0.81", features = ["preserve_order"] } +pretty_assertions = "1.2.1" diff --git a/crates/ordinals/src/artifact.rs b/crates/ordinals/src/artifact.rs new file mode 100644 index 0000000000..c45cd8355c --- /dev/null +++ b/crates/ordinals/src/artifact.rs @@ -0,0 +1,16 @@ +use super::*; + +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] +pub enum Artifact { + Cenotaph(Cenotaph), + Runestone(Runestone), +} + +impl Artifact { + pub fn mint(&self) -> Option { + match self { + Self::Cenotaph(cenotaph) => cenotaph.mint, + Self::Runestone(runestone) => runestone.mint, + } + } +} diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs new file mode 100644 index 0000000000..d03eda4cd8 --- /dev/null +++ b/crates/ordinals/src/cenotaph.rs @@ -0,0 +1,34 @@ +use super::*; + +#[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Default)] +pub struct Cenotaph { + pub etching: Option, + pub flaws: u32, + pub mint: Option, +} + +impl Cenotaph { + pub fn flaws(&self) -> Vec { + Flaw::ALL + .into_iter() + .filter(|flaw| self.flaws & flaw.flag() != 0) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flaws() { + assert_eq!( + Cenotaph { + flaws: Flaw::Opcode.flag() | Flaw::Varint.flag(), + ..default() + } + .flaws(), + vec![Flaw::Opcode, Flaw::Varint], + ); + } +} diff --git a/crates/ordinals/src/charm.rs b/crates/ordinals/src/charm.rs new file mode 100644 index 0000000000..0331be9306 --- /dev/null +++ b/crates/ordinals/src/charm.rs @@ -0,0 +1,147 @@ +use super::*; + +#[derive(Copy, Clone, Debug, PartialEq, DeserializeFromStr, SerializeDisplay)] +pub enum Charm { + Coin = 0, + Cursed = 1, + Epic = 2, + Legendary = 3, + Lost = 4, + Nineball = 5, + Rare = 6, + Reinscription = 7, + Unbound = 8, + Uncommon = 9, + Vindicated = 10, + Mythic = 11, +} + +impl Charm { + pub const ALL: [Self; 12] = [ + Self::Coin, + Self::Uncommon, + Self::Rare, + Self::Epic, + Self::Legendary, + Self::Mythic, + Self::Nineball, + Self::Reinscription, + Self::Cursed, + Self::Unbound, + Self::Lost, + Self::Vindicated, + ]; + + fn flag(self) -> u16 { + 1 << self as u16 + } + + pub fn set(self, charms: &mut u16) { + *charms |= self.flag(); + } + + pub fn is_set(self, charms: u16) -> bool { + charms & self.flag() != 0 + } + + pub fn unset(self, charms: u16) -> u16 { + charms & !self.flag() + } + + pub fn icon(self) -> &'static str { + match self { + Self::Coin => "🪙", + Self::Cursed => "👹", + Self::Epic => "🪻", + Self::Legendary => "🌝", + Self::Lost => "🤔", + Self::Mythic => "🎃", + Self::Nineball => "9️⃣", + Self::Rare => "🧿", + Self::Reinscription => "♻️", + Self::Unbound => "🔓", + Self::Uncommon => "🌱", + Self::Vindicated => "❤️‍🔥", + } + } + + pub fn charms(charms: u16) -> Vec { + Self::ALL + .into_iter() + .filter(|charm| charm.is_set(charms)) + .collect() + } +} + +impl Display for Charm { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Coin => "coin", + Self::Cursed => "cursed", + Self::Epic => "epic", + Self::Legendary => "legendary", + Self::Lost => "lost", + Self::Mythic => "mythic", + Self::Nineball => "nineball", + Self::Rare => "rare", + Self::Reinscription => "reinscription", + Self::Unbound => "unbound", + Self::Uncommon => "uncommon", + Self::Vindicated => "vindicated", + } + ) + } +} + +impl FromStr for Charm { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(match s { + "coin" => Self::Coin, + "cursed" => Self::Cursed, + "epic" => Self::Epic, + "legendary" => Self::Legendary, + "lost" => Self::Lost, + "mythic" => Self::Mythic, + "nineball" => Self::Nineball, + "rare" => Self::Rare, + "reinscription" => Self::Reinscription, + "unbound" => Self::Unbound, + "uncommon" => Self::Uncommon, + "vindicated" => Self::Vindicated, + _ => return Err(format!("invalid charm `{s}`")), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn flag() { + assert_eq!(Charm::Coin.flag(), 0b1); + assert_eq!(Charm::Cursed.flag(), 0b10); + } + + #[test] + fn set() { + let mut flags = 0; + assert!(!Charm::Coin.is_set(flags)); + Charm::Coin.set(&mut flags); + assert!(Charm::Coin.is_set(flags)); + } + + #[test] + fn unset() { + let mut flags = 0; + Charm::Coin.set(&mut flags); + assert!(Charm::Coin.is_set(flags)); + let flags = Charm::Coin.unset(flags); + assert!(!Charm::Coin.is_set(flags)); + } +} diff --git a/crates/ordinals/src/edict.rs b/crates/ordinals/src/edict.rs new file mode 100644 index 0000000000..3bc6113884 --- /dev/null +++ b/crates/ordinals/src/edict.rs @@ -0,0 +1,22 @@ +use super::*; + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] +pub struct Edict { + pub id: RuneId, + pub amount: u128, + pub output: u32, +} + +impl Edict { + pub fn from_integers(tx: &Transaction, id: RuneId, amount: u128, output: u128) -> Option { + let Ok(output) = u32::try_from(output) else { + return None; + }; + + if output > u32::try_from(tx.output.len()).unwrap() { + return None; + } + + Some(Self { id, amount, output }) + } +} diff --git a/crates/ordinals/src/etching.rs b/crates/ordinals/src/etching.rs new file mode 100644 index 0000000000..eab0421848 --- /dev/null +++ b/crates/ordinals/src/etching.rs @@ -0,0 +1,128 @@ +use super::*; + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] +pub struct Etching { + pub divisibility: Option, + pub premine: Option, + pub rune: Option, + pub spacers: Option, + pub symbol: Option, + pub terms: Option, +} + +impl Etching { + pub const MAX_DIVISIBILITY: u8 = 38; + pub const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; + + pub fn supply(&self) -> Option { + let premine = self.premine.unwrap_or_default(); + let cap = self.terms.and_then(|terms| terms.cap).unwrap_or_default(); + let amount = self + .terms + .and_then(|terms| terms.amount) + .unwrap_or_default(); + premine.checked_add(cap.checked_mul(amount)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn max_spacers() { + let mut rune = String::new(); + + for (i, c) in Rune(u128::MAX).to_string().chars().enumerate() { + if i > 0 { + rune.push('•'); + } + + rune.push(c); + } + + assert_eq!( + Etching::MAX_SPACERS, + rune.parse::().unwrap().spacers + ); + } + + #[test] + fn supply() { + #[track_caller] + fn case(premine: Option, terms: Option, supply: Option) { + assert_eq!( + Etching { + premine, + terms, + ..default() + } + .supply(), + supply, + ); + } + + case(None, None, Some(0)); + case(Some(0), None, Some(0)); + case(Some(1), None, Some(1)); + case( + Some(1), + Some(Terms { + cap: None, + amount: None, + ..default() + }), + Some(1), + ); + + case( + None, + Some(Terms { + cap: None, + amount: None, + ..default() + }), + Some(0), + ); + + case( + Some(u128::MAX / 2 + 1), + Some(Terms { + cap: Some(u128::MAX / 2), + amount: Some(1), + ..default() + }), + Some(u128::MAX), + ); + + case( + Some(1000), + Some(Terms { + cap: Some(10), + amount: Some(100), + ..default() + }), + Some(2000), + ); + + case( + Some(u128::MAX), + Some(Terms { + cap: Some(1), + amount: Some(1), + ..default() + }), + None, + ); + + case( + Some(0), + Some(Terms { + cap: Some(1), + amount: Some(u128::MAX), + ..default() + }), + Some(u128::MAX), + ); + } +} diff --git a/crates/ordinals/src/flaw.rs b/crates/ordinals/src/flaw.rs new file mode 100644 index 0000000000..69a2be27f8 --- /dev/null +++ b/crates/ordinals/src/flaw.rs @@ -0,0 +1,57 @@ +use super::*; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Flaw { + EdictOutput, + EdictRuneId, + InvalidScript, + Opcode, + SupplyOverflow, + TrailingIntegers, + TruncatedField, + UnrecognizedEvenTag, + UnrecognizedFlag, + Varint, +} + +impl Flaw { + pub const ALL: [Self; 10] = [ + Self::EdictOutput, + Self::EdictRuneId, + Self::InvalidScript, + Self::Opcode, + Self::SupplyOverflow, + Self::TrailingIntegers, + Self::TruncatedField, + Self::UnrecognizedEvenTag, + Self::UnrecognizedFlag, + Self::Varint, + ]; + + pub fn flag(self) -> u32 { + 1 << (self as u32) + } +} + +impl Display for Flaw { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::EdictOutput => write!(f, "edict output greater than transaction output count"), + Self::EdictRuneId => write!(f, "invalid rune ID in edict"), + Self::InvalidScript => write!(f, "invalid script in OP_RETURN"), + Self::Opcode => write!(f, "non-pushdata opcode in OP_RETURN"), + Self::SupplyOverflow => write!(f, "supply overflows u128"), + Self::TrailingIntegers => write!(f, "trailing integers in body"), + Self::TruncatedField => write!(f, "field with missing value"), + Self::UnrecognizedEvenTag => write!(f, "unrecognized even tag"), + Self::UnrecognizedFlag => write!(f, "unrecognized field"), + Self::Varint => write!(f, "invalid varint"), + } + } +} + +impl From for u32 { + fn from(cenotaph: Flaw) -> Self { + cenotaph.flag() + } +} diff --git a/crates/ordinals/src/lib.rs b/crates/ordinals/src/lib.rs index 5f178ade34..7fb17fd154 100644 --- a/crates/ordinals/src/lib.rs +++ b/crates/ordinals/src/lib.rs @@ -1,15 +1,22 @@ -//! Types for interoperating with ordinals and inscriptions. +//! Types for interoperating with ordinals, inscriptions, and runes. +#![allow(clippy::large_enum_variant)] use { - bitcoin::constants::{COIN_VALUE, DIFFCHANGE_INTERVAL, SUBSIDY_HALVING_INTERVAL}, bitcoin::{ consensus::{Decodable, Encodable}, - OutPoint, + constants::{ + COIN_VALUE, DIFFCHANGE_INTERVAL, MAX_SCRIPT_ELEMENT_SIZE, SUBSIDY_HALVING_INTERVAL, + }, + opcodes, + script::{self, Instruction}, + Network, OutPoint, ScriptBuf, Transaction, }, derive_more::{Display, FromStr}, - serde::{Deserialize, Deserializer, Serialize, Serializer}, + serde::{Deserialize, Serialize}, + serde_with::{DeserializeFromStr, SerializeDisplay}, std::{ cmp, + collections::{HashMap, VecDeque}, fmt::{self, Display, Formatter}, io, num::ParseIntError, @@ -19,21 +26,36 @@ use { thiserror::Error, }; -pub const CYCLE_EPOCHS: u32 = 6; - pub use { - decimal_sat::DecimalSat, degree::Degree, epoch::Epoch, height::Height, rarity::Rarity, sat::Sat, - sat_point::SatPoint, + artifact::Artifact, cenotaph::Cenotaph, charm::Charm, decimal_sat::DecimalSat, degree::Degree, + edict::Edict, epoch::Epoch, etching::Etching, flaw::Flaw, height::Height, pile::Pile, + rarity::Rarity, rune::Rune, rune_id::RuneId, runestone::Runestone, sat::Sat, sat_point::SatPoint, + spaced_rune::SpacedRune, terms::Terms, }; -#[doc(hidden)] -pub use self::deserialize_from_str::DeserializeFromStr; +pub const CYCLE_EPOCHS: u32 = 6; + +fn default() -> T { + Default::default() +} +mod artifact; +mod cenotaph; +mod charm; mod decimal_sat; mod degree; -mod deserialize_from_str; +mod edict; mod epoch; +mod etching; +mod flaw; mod height; +mod pile; mod rarity; +mod rune; +mod rune_id; +mod runestone; mod sat; mod sat_point; +mod spaced_rune; +mod terms; +pub mod varint; diff --git a/src/runes/pile.rs b/crates/ordinals/src/pile.rs similarity index 85% rename from src/runes/pile.rs rename to crates/ordinals/src/pile.rs index fbf429bec5..d93cba20ba 100644 --- a/src/runes/pile.rs +++ b/crates/ordinals/src/pile.rs @@ -26,9 +26,7 @@ impl Display for Pile { write!(f, "{whole}.{fractional:0>width$}")?; } - if let Some(symbol) = self.symbol { - write!(f, "\u{00A0}{symbol}")?; - } + write!(f, "\u{A0}{}", self.symbol.unwrap_or('¤'))?; Ok(()) } @@ -47,7 +45,7 @@ mod tests { symbol: None, } .to_string(), - "0" + "0\u{A0}¤" ); assert_eq!( Pile { @@ -56,7 +54,7 @@ mod tests { symbol: None, } .to_string(), - "25" + "25\u{A0}¤" ); assert_eq!( Pile { @@ -65,7 +63,7 @@ mod tests { symbol: None, } .to_string(), - "0" + "0\u{A0}¤" ); assert_eq!( Pile { @@ -74,7 +72,7 @@ mod tests { symbol: None, } .to_string(), - "0.1" + "0.1\u{A0}¤" ); assert_eq!( Pile { @@ -83,7 +81,7 @@ mod tests { symbol: None, } .to_string(), - "0.01" + "0.01\u{A0}¤" ); assert_eq!( Pile { @@ -92,7 +90,7 @@ mod tests { symbol: None, } .to_string(), - "0.1" + "0.1\u{A0}¤" ); assert_eq!( Pile { @@ -101,7 +99,7 @@ mod tests { symbol: None, } .to_string(), - "1.1" + "1.1\u{A0}¤" ); assert_eq!( Pile { @@ -110,7 +108,7 @@ mod tests { symbol: None, } .to_string(), - "1" + "1\u{A0}¤" ); assert_eq!( Pile { @@ -119,7 +117,7 @@ mod tests { symbol: None, } .to_string(), - "1.01" + "1.01\u{A0}¤" ); assert_eq!( Pile { @@ -128,16 +126,16 @@ mod tests { symbol: None, } .to_string(), - "340282366920938463463.374607431768211455" + "340282366920938463463.374607431768211455\u{A0}¤" ); assert_eq!( Pile { amount: u128::MAX, - divisibility: MAX_DIVISIBILITY, + divisibility: 38, symbol: None, } .to_string(), - "3.40282366920938463463374607431768211455" + "3.40282366920938463463374607431768211455\u{A0}¤" ); assert_eq!( Pile { @@ -146,7 +144,7 @@ mod tests { symbol: Some('$'), } .to_string(), - "0\u{00A0}$" + "0\u{A0}$" ); } } diff --git a/crates/ordinals/src/rarity.rs b/crates/ordinals/src/rarity.rs index eb98e15dc4..a92fce72a9 100644 --- a/crates/ordinals/src/rarity.rs +++ b/crates/ordinals/src/rarity.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] +#[derive(Debug, PartialEq, PartialOrd, Copy, Clone, DeserializeFromStr, SerializeDisplay)] pub enum Rarity { Common, Uncommon, @@ -90,24 +90,6 @@ impl FromStr for Rarity { } } -impl Serialize for Rarity { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for Rarity { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/runes/rune.rs b/crates/ordinals/src/rune.rs similarity index 70% rename from src/runes/rune.rs rename to crates/ordinals/src/rune.rs index 04bbc3e00e..d3cd3c49d2 100644 --- a/src/runes/rune.rs +++ b/crates/ordinals/src/rune.rs @@ -1,9 +1,13 @@ use super::*; -#[derive(Default, Debug, PartialEq, Copy, Clone, PartialOrd, Ord, Eq)] +#[derive( + Default, Debug, PartialEq, Copy, Clone, PartialOrd, Ord, Eq, DeserializeFromStr, SerializeDisplay, +)] pub struct Rune(pub u128); impl Rune { + const RESERVED: u128 = 6402364363415443603228541259936211926; + const STEPS: &'static [u128] = &[ 0, 26, @@ -35,12 +39,27 @@ impl Rune { 166461473448801533683942072758341510102, ]; - pub(crate) fn minimum_at_height(chain: Chain, height: Height) -> Self { + pub fn n(self) -> u128 { + self.0 + } + + pub fn first_rune_height(network: Network) -> u32 { + SUBSIDY_HALVING_INTERVAL + * match network { + Network::Bitcoin => 4, + Network::Regtest => 0, + Network::Signet => 0, + Network::Testnet => 12, + _ => 0, + } + } + + pub fn minimum_at_height(chain: Network, height: Height) -> Self { let offset = height.0.saturating_add(1); const INTERVAL: u32 = SUBSIDY_HALVING_INTERVAL / 12; - let start = chain.first_rune_height(); + let start = Self::first_rune_height(chain); let end = start + SUBSIDY_HALVING_INTERVAL; @@ -65,30 +84,28 @@ impl Rune { Rune(start - ((start - end) * remainder / u128::from(INTERVAL))) } - pub(crate) fn is_reserved(self) -> bool { - self.0 >= RESERVED + pub fn is_reserved(self) -> bool { + self.0 >= Self::RESERVED } - pub(crate) fn reserved(n: u128) -> Self { - Rune(RESERVED.checked_add(n).unwrap()) + pub fn reserved(block: u64, tx: u32) -> Self { + Self( + Self::RESERVED + .checked_add(u128::from(block) << 32 | u128::from(tx)) + .unwrap(), + ) } -} -impl Serialize for Rune { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} + pub fn commitment(self) -> Vec { + let bytes = self.0.to_le_bytes(); + + let mut end = bytes.len(); + + while end > 0 && bytes[end - 1] == 0 { + end -= 1; + } -impl<'de> Deserialize<'de> for Rune { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) + bytes[..end].into() } } @@ -122,26 +139,41 @@ impl Display for Rune { impl FromStr for Rune { type Err = Error; - fn from_str(s: &str) -> crate::Result { + fn from_str(s: &str) -> Result { let mut x = 0u128; for (i, c) in s.chars().enumerate() { if i > 0 { x += 1; } - x = x.checked_mul(26).ok_or_else(|| anyhow!("out of range"))?; + x = x.checked_mul(26).ok_or(Error::Range)?; match c { 'A'..='Z' => { - x = x - .checked_add(c as u128 - 'A' as u128) - .ok_or_else(|| anyhow!("out of range"))?; + x = x.checked_add(c as u128 - 'A' as u128).ok_or(Error::Range)?; } - _ => bail!("invalid character in rune name: {c}"), + _ => return Err(Error::Character(c)), } } Ok(Rune(x)) } } +#[derive(Debug, PartialEq)] +pub enum Error { + Character(char), + Range, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Character(c) => write!(f, "invalid character `{c}`"), + Self::Range => write!(f, "name out of range"), + } + } +} + +impl std::error::Error for Error {} + #[cfg(test)] mod tests { use super::*; @@ -189,8 +221,12 @@ mod tests { } #[test] - fn from_str_out_of_range() { - "BCGDENLQRQWDSLRUGSNLBTMFIJAW".parse::().unwrap_err(); + fn from_str_error() { + assert_eq!( + "BCGDENLQRQWDSLRUGSNLBTMFIJAW".parse::().unwrap_err(), + Error::Range + ); + assert_eq!("x".parse::().unwrap_err(), Error::Character('x')); } #[test] @@ -201,7 +237,7 @@ mod tests { #[track_caller] fn case(height: u32, minimum: &str) { assert_eq!( - Rune::minimum_at_height(Chain::Mainnet, Height(height)).to_string(), + Rune::minimum_at_height(Network::Bitcoin, Height(height)).to_string(), minimum, ); } @@ -282,35 +318,35 @@ mod tests { #[test] fn minimum_at_height() { #[track_caller] - fn case(chain: Chain, height: u32, minimum: &str) { + fn case(network: Network, height: u32, minimum: &str) { assert_eq!( - Rune::minimum_at_height(chain, Height(height)).to_string(), + Rune::minimum_at_height(network, Height(height)).to_string(), minimum, ); } - case(Chain::Testnet, 0, "AAAAAAAAAAAAA"); + case(Network::Testnet, 0, "AAAAAAAAAAAAA"); case( - Chain::Testnet, + Network::Testnet, SUBSIDY_HALVING_INTERVAL * 12 - 1, "AAAAAAAAAAAAA", ); case( - Chain::Testnet, + Network::Testnet, SUBSIDY_HALVING_INTERVAL * 12, "ZZYZXBRKWXVA", ); case( - Chain::Testnet, + Network::Testnet, SUBSIDY_HALVING_INTERVAL * 12 + 1, "ZZXZUDIVTVQA", ); - case(Chain::Signet, 0, "ZZYZXBRKWXVA"); - case(Chain::Signet, 1, "ZZXZUDIVTVQA"); + case(Network::Signet, 0, "ZZYZXBRKWXVA"); + case(Network::Signet, 1, "ZZXZUDIVTVQA"); - case(Chain::Regtest, 0, "ZZYZXBRKWXVA"); - case(Chain::Regtest, 1, "ZZXZUDIVTVQA"); + case(Network::Regtest, 0, "ZZYZXBRKWXVA"); + case(Network::Regtest, 1, "ZZXZUDIVTVQA"); } #[test] @@ -324,12 +360,18 @@ mod tests { #[test] fn reserved() { assert_eq!( - RESERVED, + Rune::RESERVED, "AAAAAAAAAAAAAAAAAAAAAAAAAAA".parse::().unwrap().0, ); - assert_eq!(Rune::reserved(0), Rune(RESERVED)); - assert_eq!(Rune::reserved(1), Rune(RESERVED + 1)); + assert_eq!(Rune::reserved(0, 0), Rune(Rune::RESERVED)); + assert_eq!(Rune::reserved(0, 1), Rune(Rune::RESERVED + 1)); + assert_eq!(Rune::reserved(1, 0), Rune(Rune::RESERVED + (1 << 32))); + assert_eq!(Rune::reserved(1, 1), Rune(Rune::RESERVED + (1 << 32) + 1)); + assert_eq!( + Rune::reserved(u64::MAX, u32::MAX), + Rune(Rune::RESERVED + (u128::from(u64::MAX) << 32 | u128::from(u32::MAX))), + ); } #[test] @@ -358,4 +400,20 @@ mod tests { } } } + + #[test] + fn commitment() { + #[track_caller] + fn case(rune: u128, bytes: &[u8]) { + assert_eq!(Rune(rune).commitment(), bytes); + } + + case(0, &[]); + case(1, &[1]); + case(255, &[255]); + case(256, &[0, 1]); + case(65535, &[255, 255]); + case(65536, &[0, 0, 1]); + case(u128::MAX, &[255; 16]); + } } diff --git a/crates/ordinals/src/rune_id.rs b/crates/ordinals/src/rune_id.rs new file mode 100644 index 0000000000..0c82a0047a --- /dev/null +++ b/crates/ordinals/src/rune_id.rs @@ -0,0 +1,167 @@ +use super::*; + +#[derive( + Debug, + PartialEq, + Copy, + Clone, + Hash, + Eq, + Ord, + PartialOrd, + Default, + DeserializeFromStr, + SerializeDisplay, +)] +pub struct RuneId { + pub block: u64, + pub tx: u32, +} + +impl RuneId { + pub fn new(block: u64, tx: u32) -> Option { + let id = RuneId { block, tx }; + + if id.block == 0 && id.tx > 0 { + return None; + } + + Some(id) + } + + pub fn delta(self, next: RuneId) -> Option<(u128, u128)> { + let block = next.block.checked_sub(self.block)?; + + let tx = if block == 0 { + next.tx.checked_sub(self.tx)? + } else { + next.tx + }; + + Some((block.into(), tx.into())) + } + + pub fn next(self: RuneId, block: u128, tx: u128) -> Option { + RuneId::new( + self.block.checked_add(block.try_into().ok()?)?, + if block == 0 { + self.tx.checked_add(tx.try_into().ok()?)? + } else { + tx.try_into().ok()? + }, + ) + } +} + +impl Display for RuneId { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}:{}", self.block, self.tx) + } +} + +impl FromStr for RuneId { + type Err = Error; + + fn from_str(s: &str) -> Result { + let (height, index) = s.split_once(':').ok_or(Error::Separator)?; + + Ok(Self { + block: height.parse().map_err(Error::Block)?, + tx: index.parse().map_err(Error::Transaction)?, + }) + } +} + +#[derive(Debug, PartialEq)] +pub enum Error { + Separator, + Block(ParseIntError), + Transaction(ParseIntError), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Separator => write!(f, "missing separator"), + Self::Block(err) => write!(f, "invalid height: {err}"), + Self::Transaction(err) => write!(f, "invalid index: {err}"), + } + } +} + +impl std::error::Error for Error {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn delta() { + let mut expected = [ + RuneId { block: 4, tx: 2 }, + RuneId { block: 1, tx: 2 }, + RuneId { block: 1, tx: 1 }, + RuneId { block: 3, tx: 1 }, + RuneId { block: 2, tx: 0 }, + ]; + + expected.sort(); + + assert_eq!( + expected, + [ + RuneId { block: 1, tx: 1 }, + RuneId { block: 1, tx: 2 }, + RuneId { block: 2, tx: 0 }, + RuneId { block: 3, tx: 1 }, + RuneId { block: 4, tx: 2 }, + ] + ); + + let mut previous = RuneId::default(); + let mut deltas = Vec::new(); + for id in expected { + deltas.push(previous.delta(id).unwrap()); + previous = id; + } + + assert_eq!(deltas, [(1, 1), (0, 1), (1, 0), (1, 1), (1, 2)]); + + let mut previous = RuneId::default(); + let mut actual = Vec::new(); + for (block, tx) in deltas { + let next = previous.next(block, tx).unwrap(); + actual.push(next); + previous = next; + } + + assert_eq!(actual, expected); + } + + #[test] + fn display() { + assert_eq!(RuneId { block: 1, tx: 2 }.to_string(), "1:2"); + } + + #[test] + fn from_str() { + assert!(matches!("123".parse::(), Err(Error::Separator))); + assert!(matches!(":".parse::(), Err(Error::Block(_)))); + assert!(matches!("1:".parse::(), Err(Error::Transaction(_)))); + assert!(matches!(":2".parse::(), Err(Error::Block(_)))); + assert!(matches!("a:2".parse::(), Err(Error::Block(_)))); + assert!(matches!( + "1:a".parse::(), + Err(Error::Transaction(_)), + )); + assert_eq!("1:2".parse::().unwrap(), RuneId { block: 1, tx: 2 }); + } + + #[test] + fn serde() { + let rune_id = RuneId { block: 1, tx: 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/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs new file mode 100644 index 0000000000..4744dd875e --- /dev/null +++ b/crates/ordinals/src/runestone.rs @@ -0,0 +1,2097 @@ +use {super::*, flag::Flag, message::Message, tag::Tag}; + +mod flag; +mod message; +mod tag; + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Runestone { + pub edicts: Vec, + pub etching: Option, + pub mint: Option, + pub pointer: Option, +} + +#[derive(Debug, PartialEq)] +enum Payload { + Valid(Vec), + Invalid(Flaw), +} + +impl Runestone { + pub const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; + pub const COMMIT_INTERVAL: u16 = 6; + + pub fn decipher(transaction: &Transaction) -> Option { + let payload = match Runestone::payload(transaction) { + Some(Payload::Valid(payload)) => payload, + Some(Payload::Invalid(flaw)) => { + return Some(Artifact::Cenotaph(Cenotaph { + flaws: flaw.into(), + ..default() + })); + } + None => return None, + }; + + let Some(integers) = Runestone::integers(&payload) else { + return Some(Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Varint.into(), + ..default() + })); + }; + + let Message { + mut flaws, + edicts, + mut fields, + } = Message::from_integers(transaction, &integers); + + let mut flags = Tag::Flags + .take(&mut fields, |[flags]| Some(flags)) + .unwrap_or_default(); + + let etching = Flag::Etching.take(&mut flags).then(|| Etching { + divisibility: Tag::Divisibility.take(&mut fields, |[divisibility]| { + let divisibility = u8::try_from(divisibility).ok()?; + (divisibility <= Etching::MAX_DIVISIBILITY).then_some(divisibility) + }), + premine: Tag::Premine.take(&mut fields, |[premine]| Some(premine)), + rune: Tag::Rune.take(&mut fields, |[rune]| Some(Rune(rune))), + spacers: Tag::Spacers.take(&mut fields, |[spacers]| { + let spacers = u32::try_from(spacers).ok()?; + (spacers <= Etching::MAX_SPACERS).then_some(spacers) + }), + symbol: Tag::Symbol.take(&mut fields, |[symbol]| { + char::from_u32(u32::try_from(symbol).ok()?) + }), + terms: Flag::Terms.take(&mut flags).then(|| Terms { + cap: Tag::Cap.take(&mut fields, |[cap]| Some(cap)), + height: ( + Tag::HeightStart.take(&mut fields, |[start_height]| { + u64::try_from(start_height).ok() + }), + Tag::HeightEnd.take(&mut fields, |[start_height]| { + u64::try_from(start_height).ok() + }), + ), + amount: Tag::Amount.take(&mut fields, |[amount]| Some(amount)), + offset: ( + Tag::OffsetStart.take(&mut fields, |[start_offset]| { + u64::try_from(start_offset).ok() + }), + Tag::OffsetEnd.take(&mut fields, |[end_offset]| u64::try_from(end_offset).ok()), + ), + }), + }); + + let mint = Tag::Mint.take(&mut fields, |[block, tx]| { + RuneId::new(block.try_into().ok()?, tx.try_into().ok()?) + }); + + let pointer = Tag::Pointer.take(&mut fields, |[pointer]| { + let pointer = u32::try_from(pointer).ok()?; + (u64::from(pointer) < u64::try_from(transaction.output.len()).unwrap()).then_some(pointer) + }); + + if etching + .map(|etching| etching.supply().is_none()) + .unwrap_or_default() + { + flaws |= Flaw::SupplyOverflow.flag(); + } + + if flags != 0 { + flaws |= Flaw::UnrecognizedFlag.flag(); + } + + if fields.keys().any(|tag| tag % 2 == 0) { + flaws |= Flaw::UnrecognizedEvenTag.flag(); + } + + if flaws != 0 { + return Some(Artifact::Cenotaph(Cenotaph { + flaws, + mint, + etching: etching.and_then(|etching| etching.rune), + })); + } + + Some(Artifact::Runestone(Self { + edicts, + etching, + mint, + pointer, + })) + } + + pub fn encipher(&self) -> ScriptBuf { + let mut payload = Vec::new(); + + if let Some(etching) = self.etching { + let mut flags = 0; + Flag::Etching.set(&mut flags); + + if etching.terms.is_some() { + Flag::Terms.set(&mut flags); + } + + Tag::Flags.encode([flags], &mut payload); + + Tag::Rune.encode_option(etching.rune.map(|rune| rune.0), &mut payload); + Tag::Divisibility.encode_option(etching.divisibility, &mut payload); + Tag::Spacers.encode_option(etching.spacers, &mut payload); + Tag::Symbol.encode_option(etching.symbol, &mut payload); + Tag::Premine.encode_option(etching.premine, &mut payload); + + if let Some(terms) = etching.terms { + Tag::Amount.encode_option(terms.amount, &mut payload); + Tag::Cap.encode_option(terms.cap, &mut payload); + Tag::HeightStart.encode_option(terms.height.0, &mut payload); + Tag::HeightEnd.encode_option(terms.height.1, &mut payload); + Tag::OffsetStart.encode_option(terms.offset.0, &mut payload); + Tag::OffsetEnd.encode_option(terms.offset.1, &mut payload); + } + } + + if let Some(RuneId { block, tx }) = self.mint { + Tag::Mint.encode([block.into(), tx.into()], &mut payload); + } + + Tag::Pointer.encode_option(self.pointer, &mut payload); + + if !self.edicts.is_empty() { + varint::encode_to_vec(Tag::Body.into(), &mut payload); + + let mut edicts = self.edicts.clone(); + edicts.sort_by_key(|edict| edict.id); + + let mut previous = RuneId::default(); + for edict in edicts { + let (block, tx) = previous.delta(edict.id).unwrap(); + varint::encode_to_vec(block, &mut payload); + varint::encode_to_vec(tx, &mut payload); + varint::encode_to_vec(edict.amount, &mut payload); + varint::encode_to_vec(edict.output.into(), &mut payload); + previous = edict.id; + } + } + + let mut builder = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER); + + for chunk in payload.chunks(MAX_SCRIPT_ELEMENT_SIZE) { + let push: &script::PushBytes = chunk.try_into().unwrap(); + builder = builder.push_slice(push); + } + + builder.into_script() + } + + fn payload(transaction: &Transaction) -> Option { + // search transaction outputs for payload + for output in &transaction.output { + let mut instructions = output.script_pubkey.instructions(); + + // payload starts with OP_RETURN + if instructions.next() != Some(Ok(Instruction::Op(opcodes::all::OP_RETURN))) { + continue; + } + + // followed by the protocol identifier, ignoring errors, since OP_RETURN + // scripts may be invalid + if instructions.next() != Some(Ok(Instruction::Op(Runestone::MAGIC_NUMBER))) { + continue; + } + + // construct the payload by concatenating remaining data pushes + let mut payload = Vec::new(); + + for result in instructions { + match result { + Ok(Instruction::PushBytes(push)) => { + payload.extend_from_slice(push.as_bytes()); + } + Ok(Instruction::Op(_)) => { + return Some(Payload::Invalid(Flaw::Opcode)); + } + Err(_) => { + return Some(Payload::Invalid(Flaw::InvalidScript)); + } + } + } + + return Some(Payload::Valid(payload)); + } + + None + } + + fn integers(payload: &[u8]) -> Option> { + 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; + } + + Some(integers) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + bitcoin::{ + blockdata::locktime::absolute::LockTime, script::PushBytes, Sequence, TxIn, TxOut, Witness, + }, + pretty_assertions::assert_eq, + }; + + pub(crate) fn rune_id(tx: u32) -> RuneId { + RuneId { block: 1, tx } + } + + fn decipher(integers: &[u128]) -> Artifact { + let payload = payload(integers); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice(payload) + .into_script(), + value: 0, + }], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap() + } + + fn payload(integers: &[u128]) -> Vec { + let mut payload = Vec::new(); + + for integer in integers { + payload.extend(varint::encode(*integer)); + } + + payload + } + + #[test] + fn decipher_returns_none_if_first_opcode_is_malformed() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: ScriptBuf::from_bytes(vec![opcodes::all::OP_PUSHBYTES_4.to_u8()]), + value: 0, + }], + lock_time: LockTime::ZERO, + version: 2, + }), + None, + ); + } + + #[test] + fn deciphering_transaction_with_no_outputs_returns_none() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: Vec::new(), + lock_time: LockTime::ZERO, + version: 2, + }), + None, + ); + } + + #[test] + fn deciphering_transaction_with_non_op_return_output_returns_none() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new().push_slice([]).into_script(), + value: 0 + }], + lock_time: LockTime::ZERO, + version: 2, + }), + None, + ); + } + + #[test] + fn deciphering_transaction_with_bare_op_return_returns_none() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .into_script(), + value: 0 + }], + lock_time: LockTime::ZERO, + version: 2, + }), + None, + ); + } + + #[test] + fn deciphering_transaction_with_non_matching_op_return_returns_none() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"FOOO") + .into_script(), + value: 0 + }], + lock_time: LockTime::ZERO, + version: 2, + }), + None, + ); + } + + #[test] + fn deciphering_valid_runestone_with_invalid_script_postfix_returns_invalid_payload() { + let mut script_pubkey = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .into_script() + .into_bytes(); + + script_pubkey.push(opcodes::all::OP_PUSHBYTES_4.to_u8()); + + assert_eq!( + Runestone::payload(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: ScriptBuf::from_bytes(script_pubkey), + value: 0, + }], + lock_time: LockTime::ZERO, + version: 2, + }), + Some(Payload::Invalid(Flaw::InvalidScript)) + ); + } + + #[test] + fn deciphering_runestone_with_truncated_varint_succeeds() { + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice([128]) + .into_script(), + value: 0, + }], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(); + } + + #[test] + fn outputs_with_non_pushdata_opcodes_are_cenotaph() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![ + TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_opcode(opcodes::all::OP_VERIFY) + .push_slice([0]) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice([2, 0]) + .into_script(), + value: 0, + }, + TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice([0]) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) + .push_slice([3, 0]) + .into_script(), + value: 0, + }, + ], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Opcode.into(), + ..default() + }), + ); + } + + #[test] + fn pushnum_opcodes_in_runestone_produce_cenotaph() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_opcode(opcodes::all::OP_PUSHNUM_1) + .into_script(), + value: 0, + },], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Opcode.into(), + ..default() + }), + ); + } + + #[test] + fn deciphering_empty_runestone_is_successful() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .into_script(), + value: 0 + }], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Runestone(Runestone::default()), + ); + } + + #[test] + fn invalid_input_scripts_are_skipped_when_searching_for_runestone() { + let payload = payload(&[Tag::Mint.into(), 1, Tag::Mint.into(), 1]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + let script_pubkey = vec![ + opcodes::all::OP_RETURN.to_u8(), + opcodes::all::OP_PUSHBYTES_9.to_u8(), + Runestone::MAGIC_NUMBER.to_u8(), + opcodes::all::OP_PUSHBYTES_4.to_u8(), + ]; + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![ + TxOut { + script_pubkey: ScriptBuf::from_bytes(script_pubkey), + value: 0, + }, + TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice(payload) + .into_script(), + value: 0, + }, + ], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Runestone(Runestone { + mint: Some(RuneId::new(1, 1).unwrap()), + ..default() + }), + ); + } + + #[test] + fn deciphering_non_empty_runestone_is_successful() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 2, 0]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + ..default() + }), + ); + } + + #[test] + fn decipher_etching() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Body.into(), + 1, + 1, + 2, + 0 + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching::default()), + ..default() + }), + ); + } + + #[test] + fn decipher_etching_with_rune() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Rune.into(), + 4, + Tag::Body.into(), + 1, + 1, + 2, + 0 + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn terms_flag_without_etching_flag_produces_cenotaph() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Terms.mask(), + Tag::Body.into(), + 0, + 0, + 0, + 0 + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedFlag.into(), + ..default() + }), + ); + } + + #[test] + fn recognized_fields_without_flag_produces_cenotaph() { + #[track_caller] + fn case(integers: &[u128]) { + assert_eq!( + decipher(integers), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.into(), + ..default() + }), + ); + } + + case(&[Tag::Premine.into(), 0]); + case(&[Tag::Rune.into(), 0]); + case(&[Tag::Cap.into(), 0]); + case(&[Tag::Amount.into(), 0]); + case(&[Tag::OffsetStart.into(), 0]); + case(&[Tag::OffsetEnd.into(), 0]); + case(&[Tag::HeightStart.into(), 0]); + case(&[Tag::HeightEnd.into(), 0]); + + case(&[Tag::Flags.into(), Flag::Etching.into(), Tag::Cap.into(), 0]); + case(&[ + Tag::Flags.into(), + Flag::Etching.into(), + Tag::Amount.into(), + 0, + ]); + case(&[ + Tag::Flags.into(), + Flag::Etching.into(), + Tag::OffsetStart.into(), + 0, + ]); + case(&[ + Tag::Flags.into(), + Flag::Etching.into(), + Tag::OffsetEnd.into(), + 0, + ]); + case(&[ + Tag::Flags.into(), + Flag::Etching.into(), + Tag::HeightStart.into(), + 0, + ]); + case(&[ + Tag::Flags.into(), + Flag::Etching.into(), + Tag::HeightEnd.into(), + 0, + ]); + } + + #[test] + fn decipher_etching_with_term() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::OffsetEnd.into(), + 4, + Tag::Body.into(), + 1, + 1, + 2, + 0 + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + terms: Some(Terms { + offset: (None, Some(4)), + ..default() + }), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn decipher_etching_with_amount() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Amount.into(), + 4, + Tag::Body.into(), + 1, + 1, + 2, + 0 + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + terms: Some(Terms { + amount: Some(4), + ..default() + }), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn invalid_varint_produces_cenotaph() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice([128]) + .into_script(), + value: 0, + }], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::Varint.into(), + ..default() + }), + ); + } + + #[test] + fn duplicate_even_tags_produce_cenotaph() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Rune.into(), + 4, + Tag::Rune.into(), + 5, + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.into(), + etching: Some(Rune(4)), + ..default() + }), + ); + } + + #[test] + fn duplicate_odd_tags_are_ignored() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Divisibility.into(), + 4, + Tag::Divisibility.into(), + 5, + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: None, + divisibility: Some(4), + ..default() + }), + ..default() + }) + ); + } + + #[test] + fn unrecognized_odd_tag_is_ignored() { + assert_eq!( + decipher(&[Tag::Nop.into(), 100, Tag::Body.into(), 1, 1, 2, 0]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + ..default() + }), + ); + } + + #[test] + fn runestone_with_unrecognized_even_tag_is_cenotaph() { + assert_eq!( + decipher(&[Tag::Cenotaph.into(), 0, Tag::Body.into(), 1, 1, 2, 0]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn runestone_with_unrecognized_flag_is_cenotaph() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Cenotaph.mask(), + Tag::Body.into(), + 1, + 1, + 2, + 0 + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedFlag.flag(), + ..default() + }), + ); + } + + #[test] + fn runestone_with_edict_id_with_zero_block_and_nonzero_tx_is_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 0, 1, 2, 0]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.into(), + ..default() + }), + ); + } + + #[test] + fn runestone_with_overflowing_edict_id_delta_is_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 0, 0, 0, u64::MAX.into(), 0, 0, 0]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.into(), + ..default() + }), + ); + + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 0, 0, 0, u64::MAX.into(), 0, 0]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.into(), + ..default() + }), + ); + } + + #[test] + fn runestone_with_output_over_max_is_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 2, 2]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictOutput.into(), + ..default() + }), + ); + } + + #[test] + fn tag_with_no_value_is_cenotaph() { + assert_eq!( + decipher(&[Tag::Flags.into(), 1, Tag::Flags.into()]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::TruncatedField.flag(), + ..default() + }), + ); + } + + #[test] + fn trailing_integers_in_body_is_cenotaph() { + let mut integers = vec![Tag::Body.into(), 1, 1, 2, 0]; + + for i in 0..4 { + assert_eq!( + decipher(&integers), + if i == 0 { + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + ..default() + }) + } else { + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::TrailingIntegers.into(), + ..default() + }) + } + ); + + integers.push(0); + } + } + + #[test] + fn decipher_etching_with_divisibility() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Rune.into(), + 4, + Tag::Divisibility.into(), + 5, + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + divisibility: Some(5), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn divisibility_above_max_is_ignored() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Rune.into(), + 4, + Tag::Divisibility.into(), + (Etching::MAX_DIVISIBILITY + 1).into(), + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn symbol_above_max_is_ignored() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Symbol.into(), + u128::from(u32::from(char::MAX) + 1), + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching::default()), + ..default() + }), + ); + } + + #[test] + fn decipher_etching_with_symbol() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Rune.into(), + 4, + Tag::Symbol.into(), + 'a'.into(), + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + symbol: Some('a'), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn decipher_etching_with_all_etching_tags() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Rune.into(), + 4, + Tag::Divisibility.into(), + 1, + Tag::Spacers.into(), + 5, + Tag::Symbol.into(), + 'a'.into(), + Tag::OffsetEnd.into(), + 2, + Tag::Amount.into(), + 3, + Tag::Premine.into(), + 8, + Tag::Cap.into(), + 9, + Tag::Pointer.into(), + 0, + Tag::Mint.into(), + 1, + Tag::Mint.into(), + 1, + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + terms: Some(Terms { + cap: Some(9), + offset: (None, Some(2)), + amount: Some(3), + height: (None, None), + }), + premine: Some(8), + divisibility: Some(1), + symbol: Some('a'), + spacers: Some(5), + }), + pointer: Some(0), + mint: Some(RuneId::new(1, 1).unwrap()), + }), + ); + } + + #[test] + fn recognized_even_etching_fields_produce_cenotaph_if_etching_flag_is_not_set() { + assert_eq!( + decipher(&[Tag::Rune.into(), 4]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn decipher_etching_with_divisibility_and_symbol() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Rune.into(), + 4, + Tag::Divisibility.into(), + 1, + Tag::Symbol.into(), + 'a'.into(), + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(4)), + divisibility: Some(1), + symbol: Some('a'), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn tag_values_are_not_parsed_as_tags() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::Divisibility.into(), + Tag::Body.into(), + Tag::Body.into(), + 1, + 1, + 2, + 0, + ]), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + divisibility: Some(0), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn runestone_may_contain_multiple_edicts() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 2, 0, 0, 3, 5, 0]), + Artifact::Runestone(Runestone { + edicts: vec![ + Edict { + id: rune_id(1), + amount: 2, + output: 0, + }, + Edict { + id: rune_id(4), + amount: 5, + output: 0, + }, + ], + ..default() + }), + ); + } + + #[test] + fn runestones_with_invalid_rune_id_blocks_are_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 2, 0, u128::MAX, 1, 0, 0,]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.flag(), + ..default() + }), + ); + } + + #[test] + fn runestones_with_invalid_rune_id_txs_are_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 2, 0, 1, u128::MAX, 0, 0,]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictRuneId.flag(), + ..default() + }), + ); + } + + #[test] + fn payload_pushes_are_concatenated() { + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice::<&PushBytes>( + varint::encode(Tag::Flags.into()) + .as_slice() + .try_into() + .unwrap() + ) + .push_slice::<&PushBytes>( + varint::encode(Flag::Etching.mask()) + .as_slice() + .try_into() + .unwrap() + ) + .push_slice::<&PushBytes>( + varint::encode(Tag::Divisibility.into()) + .as_slice() + .try_into() + .unwrap() + ) + .push_slice::<&PushBytes>(varint::encode(5).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>( + varint::encode(Tag::Body.into()) + .as_slice() + .try_into() + .unwrap() + ) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(0).as_slice().try_into().unwrap()) + .into_script(), + value: 0 + }], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + etching: Some(Etching { + divisibility: Some(5), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn runestone_may_be_in_second_output() { + let payload = payload(&[0, 1, 1, 2, 0]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![ + TxOut { + script_pubkey: ScriptBuf::new(), + value: 0, + }, + TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice(payload) + .into_script(), + value: 0 + } + ], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + ..default() + }), + ); + } + + #[test] + fn runestone_may_be_after_non_matching_op_return() { + let payload = payload(&[0, 1, 1, 2, 0]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![ + TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"FOO") + .into_script(), + value: 0, + }, + TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_opcode(Runestone::MAGIC_NUMBER) + .push_slice(payload) + .into_script(), + value: 0 + } + ], + lock_time: LockTime::ZERO, + version: 2, + }) + .unwrap(), + Artifact::Runestone(Runestone { + edicts: vec![Edict { + id: rune_id(1), + amount: 2, + output: 0, + }], + ..default() + }) + ); + } + + #[test] + fn runestone_size() { + #[track_caller] + fn case(edicts: Vec, etching: Option, size: usize) { + assert_eq!( + Runestone { + edicts, + etching, + ..default() + } + .encipher() + .len(), + size + ); + } + + case(Vec::new(), None, 2); + + case( + Vec::new(), + Some(Etching { + rune: Some(Rune(0)), + ..default() + }), + 7, + ); + + case( + Vec::new(), + Some(Etching { + divisibility: Some(Etching::MAX_DIVISIBILITY), + rune: Some(Rune(0)), + ..default() + }), + 9, + ); + + case( + Vec::new(), + Some(Etching { + divisibility: Some(Etching::MAX_DIVISIBILITY), + terms: Some(Terms { + cap: Some(u32::MAX.into()), + amount: Some(u64::MAX.into()), + offset: (Some(u32::MAX.into()), Some(u32::MAX.into())), + height: (Some(u32::MAX.into()), Some(u32::MAX.into())), + }), + premine: Some(u64::MAX.into()), + rune: Some(Rune(u128::MAX)), + symbol: Some('\u{10FFFF}'), + spacers: Some(Etching::MAX_SPACERS), + }), + 89, + ); + + case( + Vec::new(), + Some(Etching { + rune: Some(Rune(u128::MAX)), + ..default() + }), + 25, + ); + + case( + vec![Edict { + amount: 0, + id: RuneId { block: 0, tx: 0 }, + output: 0, + }], + Some(Etching { + divisibility: Some(Etching::MAX_DIVISIBILITY), + rune: Some(Rune(u128::MAX)), + ..default() + }), + 32, + ); + + case( + vec![Edict { + amount: u128::MAX, + id: RuneId { block: 0, tx: 0 }, + output: 0, + }], + Some(Etching { + divisibility: Some(Etching::MAX_DIVISIBILITY), + rune: Some(Rune(u128::MAX)), + ..default() + }), + 50, + ); + + case( + vec![Edict { + amount: 0, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }], + None, + 14, + ); + + case( + vec![Edict { + amount: u128::MAX, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }], + None, + 32, + ); + + case( + vec![ + Edict { + amount: u128::MAX, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }, + Edict { + amount: u128::MAX, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }, + ], + None, + 54, + ); + + case( + vec![ + Edict { + amount: u128::MAX, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }, + Edict { + amount: u128::MAX, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }, + Edict { + amount: u128::MAX, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }, + ], + None, + 76, + ); + + case( + vec![ + Edict { + amount: u64::MAX.into(), + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }; + 4 + ], + None, + 62, + ); + + case( + vec![ + Edict { + amount: u64::MAX.into(), + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }; + 5 + ], + None, + 75, + ); + + case( + vec![ + Edict { + amount: u64::MAX.into(), + id: RuneId { + block: 0, + tx: u32::MAX, + }, + output: 0, + }; + 5 + ], + None, + 73, + ); + + case( + vec![ + Edict { + amount: 1_000_000_000_000_000_000, + id: RuneId { + block: 1_000_000, + tx: u32::MAX, + }, + output: 0, + }; + 5 + ], + None, + 70, + ); + } + + #[test] + fn etching_with_term_greater_than_maximum_is_still_an_etching() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask(), + Tag::OffsetEnd.into(), + u128::from(u64::MAX) + 1, + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.into(), + ..default() + }), + ); + } + + #[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::ZERO, + version: 2, + }; + + let Payload::Valid(payload) = Runestone::payload(&transaction).unwrap() else { + panic!("invalid payload") + }; + + 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::decipher(&transaction).unwrap(), + Artifact::Runestone(runestone), + ); + } + + case(Runestone::default(), &[]); + + case( + Runestone { + edicts: vec![ + Edict { + id: RuneId::new(2, 3).unwrap(), + amount: 1, + output: 0, + }, + Edict { + id: RuneId::new(5, 6).unwrap(), + amount: 4, + output: 1, + }, + ], + etching: Some(Etching { + divisibility: Some(7), + premine: Some(8), + rune: Some(Rune(9)), + spacers: Some(10), + symbol: Some('@'), + terms: Some(Terms { + cap: Some(11), + height: (Some(12), Some(13)), + amount: Some(14), + offset: (Some(15), Some(16)), + }), + }), + mint: Some(RuneId::new(17, 18).unwrap()), + pointer: Some(0), + }, + &[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Rune.into(), + 9, + Tag::Divisibility.into(), + 7, + Tag::Spacers.into(), + 10, + Tag::Symbol.into(), + '@'.into(), + Tag::Premine.into(), + 8, + Tag::Amount.into(), + 14, + Tag::Cap.into(), + 11, + Tag::HeightStart.into(), + 12, + Tag::HeightEnd.into(), + 13, + Tag::OffsetStart.into(), + 15, + Tag::OffsetEnd.into(), + 16, + Tag::Mint.into(), + 17, + Tag::Mint.into(), + 18, + Tag::Pointer.into(), + 0, + Tag::Body.into(), + 2, + 3, + 1, + 0, + 3, + 6, + 4, + 1, + ], + ); + + case( + Runestone { + etching: Some(Etching { + premine: None, + divisibility: None, + terms: None, + symbol: None, + rune: Some(Rune(3)), + spacers: None, + }), + ..default() + }, + &[Tag::Flags.into(), Flag::Etching.mask(), Tag::Rune.into(), 3], + ); + + case( + Runestone { + etching: Some(Etching { + premine: None, + divisibility: None, + terms: None, + symbol: None, + rune: None, + spacers: None, + }), + ..default() + }, + &[Tag::Flags.into(), Flag::Etching.mask()], + ); + } + + #[test] + fn runestone_payload_is_chunked() { + let script = Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 0, + output: 0 + }; + 129 + ], + ..default() + } + .encipher(); + + assert_eq!(script.instructions().count(), 3); + + let script = Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 0, + output: 0 + }; + 130 + ], + ..default() + } + .encipher(); + + assert_eq!(script.instructions().count(), 4); + } + + #[test] + fn edict_output_greater_than_32_max_produces_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 1, u128::from(u32::MAX) + 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::EdictOutput.flag(), + ..default() + }), + ); + } + + #[test] + fn partial_mint_produces_cenotaph() { + assert_eq!( + decipher(&[Tag::Mint.into(), 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn invalid_mint_produces_cenotaph() { + assert_eq!( + decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn invalid_deadline_produces_cenotaph() { + assert_eq!( + decipher(&[Tag::OffsetEnd.into(), u128::MAX]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn invalid_default_output_produces_cenotaph() { + assert_eq!( + decipher(&[Tag::Pointer.into(), 1]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + assert_eq!( + decipher(&[Tag::Pointer.into(), u128::MAX]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn invalid_divisibility_does_not_produce_cenotaph() { + assert_eq!( + decipher(&[Tag::Divisibility.into(), u128::MAX]), + Artifact::Runestone(default()), + ); + } + + #[test] + fn min_and_max_runes_are_not_cenotaphs() { + assert_eq!( + decipher(&[Tag::Flags.into(), Flag::Etching.into(), Tag::Rune.into(), 0]), + Artifact::Runestone(Runestone { + etching: Some(Etching { + rune: Some(Rune(0)), + ..default() + }), + ..default() + }), + ); + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.into(), + Tag::Rune.into(), + u128::MAX + ]), + Artifact::Runestone(Runestone { + etching: Some(Etching { + rune: Some(Rune(u128::MAX)), + ..default() + }), + ..default() + }), + ); + } + + #[test] + fn invalid_spacers_does_not_produce_cenotaph() { + assert_eq!( + decipher(&[Tag::Spacers.into(), u128::MAX]), + Artifact::Runestone(default()), + ); + } + + #[test] + fn invalid_symbol_does_not_produce_cenotaph() { + assert_eq!( + decipher(&[Tag::Symbol.into(), u128::MAX]), + Artifact::Runestone(default()), + ); + } + + #[test] + fn invalid_term_produces_cenotaph() { + assert_eq!( + decipher(&[Tag::OffsetEnd.into(), u128::MAX]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::UnrecognizedEvenTag.flag(), + ..default() + }), + ); + } + + #[test] + fn invalid_supply_produces_cenotaph() { + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Cap.into(), + 1, + Tag::Amount.into(), + u128::MAX + ]), + Artifact::Runestone(Runestone { + etching: Some(Etching { + terms: Some(Terms { + cap: Some(1), + amount: Some(u128::MAX), + height: (None, None), + offset: (None, None), + }), + ..default() + }), + ..default() + }), + ); + + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Cap.into(), + 2, + Tag::Amount.into(), + u128::MAX + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::SupplyOverflow.into(), + ..default() + }), + ); + + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Cap.into(), + 2, + Tag::Amount.into(), + u128::MAX / 2 + 1 + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::SupplyOverflow.into(), + ..default() + }), + ); + + assert_eq!( + decipher(&[ + Tag::Flags.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::Premine.into(), + 1, + Tag::Cap.into(), + 1, + Tag::Amount.into(), + u128::MAX + ]), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::SupplyOverflow.into(), + ..default() + }), + ); + } + + #[test] + fn invalid_scripts_in_op_returns_without_magic_number_are_ignored() { + assert_eq!( + Runestone::decipher(&Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![TxOut { + script_pubkey: ScriptBuf::from(vec![ + opcodes::all::OP_RETURN.to_u8(), + opcodes::all::OP_PUSHBYTES_4.to_u8(), + ]), + value: 0, + }], + }), + None + ); + + assert_eq!( + Runestone::decipher(&Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![ + TxOut { + script_pubkey: ScriptBuf::from(vec![ + opcodes::all::OP_RETURN.to_u8(), + opcodes::all::OP_PUSHBYTES_4.to_u8(), + ]), + value: 0, + }, + TxOut { + script_pubkey: Runestone::default().encipher(), + value: 0, + } + ], + }) + .unwrap(), + Artifact::Runestone(Runestone::default()), + ); + } + + #[test] + fn invalid_scripts_in_op_returns_with_magic_number_produce_cenotaph() { + assert_eq!( + Runestone::decipher(&Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::new(), + }], + output: vec![TxOut { + script_pubkey: ScriptBuf::from(vec![ + opcodes::all::OP_RETURN.to_u8(), + Runestone::MAGIC_NUMBER.to_u8(), + opcodes::all::OP_PUSHBYTES_4.to_u8(), + ]), + value: 0, + }], + }) + .unwrap(), + Artifact::Cenotaph(Cenotaph { + flaws: Flaw::InvalidScript.into(), + ..default() + }), + ); + } +} diff --git a/src/runes/flag.rs b/crates/ordinals/src/runestone/flag.rs similarity index 64% rename from src/runes/flag.rs rename to crates/ordinals/src/runestone/flag.rs index e0c9f93160..06cf463054 100644 --- a/src/runes/flag.rs +++ b/crates/ordinals/src/runestone/flag.rs @@ -1,8 +1,8 @@ pub(super) enum Flag { - Etch = 0, - Mint = 1, + Etching = 0, + Terms = 1, #[allow(unused)] - Burn = 127, + Cenotaph = 127, } impl Flag { @@ -22,31 +22,37 @@ impl Flag { } } +impl From for u128 { + fn from(flag: Flag) -> Self { + flag.mask() + } +} + #[cfg(test)] mod tests { use super::*; #[test] fn mask() { - assert_eq!(Flag::Etch.mask(), 0b1); - assert_eq!(Flag::Burn.mask(), 1 << 127); + assert_eq!(Flag::Etching.mask(), 0b1); + assert_eq!(Flag::Cenotaph.mask(), 1 << 127); } #[test] fn take() { let mut flags = 1; - assert!(Flag::Etch.take(&mut flags)); + assert!(Flag::Etching.take(&mut flags)); assert_eq!(flags, 0); let mut flags = 0; - assert!(!Flag::Etch.take(&mut flags)); + assert!(!Flag::Etching.take(&mut flags)); assert_eq!(flags, 0); } #[test] fn set() { let mut flags = 0; - Flag::Etch.set(&mut flags); + Flag::Etching.set(&mut flags); assert_eq!(flags, 1); } } diff --git a/crates/ordinals/src/runestone/message.rs b/crates/ordinals/src/runestone/message.rs new file mode 100644 index 0000000000..b833169fbc --- /dev/null +++ b/crates/ordinals/src/runestone/message.rs @@ -0,0 +1,56 @@ +use super::*; + +pub(super) struct Message { + pub(super) flaws: u32, + pub(super) edicts: Vec, + pub(super) fields: HashMap>, +} + +impl Message { + pub(super) fn from_integers(tx: &Transaction, payload: &[u128]) -> Self { + let mut edicts = Vec::new(); + let mut fields = HashMap::>::new(); + let mut flaws = 0; + + for i in (0..payload.len()).step_by(2) { + let tag = payload[i]; + + if Tag::Body == tag { + let mut id = RuneId::default(); + for chunk in payload[i + 1..].chunks(4) { + if chunk.len() != 4 { + flaws |= Flaw::TrailingIntegers.flag(); + break; + } + + let Some(next) = id.next(chunk[0], chunk[1]) else { + flaws |= Flaw::EdictRuneId.flag(); + break; + }; + + let Some(edict) = Edict::from_integers(tx, next, chunk[2], chunk[3]) else { + flaws |= Flaw::EdictOutput.flag(); + break; + }; + + id = next; + edicts.push(edict); + } + break; + } + + let Some(&value) = payload.get(i + 1) else { + flaws |= Flaw::TruncatedField.flag(); + break; + }; + + fields.entry(tag).or_default().push_back(value); + } + + Self { + flaws, + edicts, + fields, + } + } +} diff --git a/crates/ordinals/src/runestone/tag.rs b/crates/ordinals/src/runestone/tag.rs new file mode 100644 index 0000000000..c8afa6313c --- /dev/null +++ b/crates/ordinals/src/runestone/tag.rs @@ -0,0 +1,162 @@ +use super::*; + +#[derive(Copy, Clone, Debug)] +pub(super) enum Tag { + Body = 0, + Flags = 2, + Rune = 4, + Premine = 6, + Cap = 8, + Amount = 10, + HeightStart = 12, + HeightEnd = 14, + OffsetStart = 16, + OffsetEnd = 18, + Mint = 20, + Pointer = 22, + #[allow(unused)] + Cenotaph = 126, + + Divisibility = 1, + Spacers = 3, + Symbol = 5, + #[allow(unused)] + Nop = 127, +} + +impl Tag { + pub(super) fn take( + self, + fields: &mut HashMap>, + with: impl Fn([u128; N]) -> Option, + ) -> Option { + let field = fields.get_mut(&self.into())?; + + let mut values: [u128; N] = [0; N]; + + for (i, v) in values.iter_mut().enumerate() { + *v = *field.get(i)?; + } + + let value = with(values)?; + + field.drain(0..N); + + if field.is_empty() { + fields.remove(&self.into()).unwrap(); + } + + Some(value) + } + + pub(super) fn encode(self, values: [u128; N], payload: &mut Vec) { + for value in values { + varint::encode_to_vec(self.into(), payload); + varint::encode_to_vec(value, payload); + } + } + + pub(super) fn encode_option>(self, value: Option, payload: &mut Vec) { + if let Some(value) = value { + self.encode([value.into()], payload) + } + } +} + +impl From for u128 { + fn from(tag: Tag) -> Self { + tag as u128 + } +} + +impl PartialEq for Tag { + fn eq(&self, other: &u128) -> bool { + u128::from(*self) == *other + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_u128() { + assert_eq!(0u128, Tag::Body.into()); + assert_eq!(2u128, Tag::Flags.into()); + } + + #[test] + fn partial_eq() { + assert_eq!(Tag::Body, 0); + assert_eq!(Tag::Flags, 2); + } + + #[test] + fn take() { + let mut fields = vec![(2, vec![3].into_iter().collect())] + .into_iter() + .collect::>>(); + + assert_eq!(Tag::Flags.take(&mut fields, |[_]| None::), None); + + assert!(!fields.is_empty()); + + assert_eq!(Tag::Flags.take(&mut fields, |[flags]| Some(flags)), Some(3)); + + assert!(fields.is_empty()); + + assert_eq!(Tag::Flags.take(&mut fields, |[flags]| Some(flags)), None); + } + + #[test] + fn take_leaves_unconsumed_values() { + let mut fields = vec![(2, vec![1, 2, 3].into_iter().collect())] + .into_iter() + .collect::>>(); + + assert_eq!(fields[&2].len(), 3); + + assert_eq!(Tag::Flags.take(&mut fields, |[_]| None::), None); + + assert_eq!(fields[&2].len(), 3); + + assert_eq!( + Tag::Flags.take(&mut fields, |[a, b]| Some((a, b))), + Some((1, 2)) + ); + + assert_eq!(fields[&2].len(), 1); + + assert_eq!(Tag::Flags.take(&mut fields, |[a]| Some(a)), Some(3)); + + assert_eq!(fields.get(&2), None); + } + + #[test] + fn encode() { + let mut payload = Vec::new(); + + Tag::Flags.encode([3], &mut payload); + + assert_eq!(payload, [2, 3]); + + Tag::Rune.encode([5], &mut payload); + + assert_eq!(payload, [2, 3, 4, 5]); + + Tag::Rune.encode([5, 6], &mut payload); + + assert_eq!(payload, [2, 3, 4, 5, 4, 5, 4, 6]); + } + + #[test] + fn burn_and_nop_are_one_byte() { + let mut payload = Vec::new(); + Tag::Cenotaph.encode([0], &mut payload); + assert_eq!(payload.len(), 2); + + let mut payload = Vec::new(); + Tag::Nop.encode([0], &mut payload); + assert_eq!(payload.len(), 2); + } +} diff --git a/crates/ordinals/src/sat.rs b/crates/ordinals/src/sat.rs index 1665d2cd22..189bce8561 100644 --- a/crates/ordinals/src/sat.rs +++ b/crates/ordinals/src/sat.rs @@ -84,6 +84,29 @@ impl Sat { name.chars().rev().collect() } + pub fn charms(self) -> u16 { + let mut charms = 0; + + if self.nineball() { + Charm::Nineball.set(&mut charms); + } + + if self.coin() { + Charm::Coin.set(&mut charms); + } + + match self.rarity() { + Rarity::Common => {} + Rarity::Epic => Charm::Epic.set(&mut charms), + Rarity::Legendary => Charm::Legendary.set(&mut charms), + Rarity::Mythic => Charm::Mythic.set(&mut charms), + Rarity::Rare => Charm::Rare.set(&mut charms), + Rarity::Uncommon => Charm::Uncommon.set(&mut charms), + } + + charms + } + fn from_name(s: &str) -> Result { let mut x = 0; for c in s.chars() { diff --git a/crates/ordinals/src/sat_point.rs b/crates/ordinals/src/sat_point.rs index 2398a14edc..e08f337145 100644 --- a/crates/ordinals/src/sat_point.rs +++ b/crates/ordinals/src/sat_point.rs @@ -9,7 +9,19 @@ use {super::*, bitcoin::transaction::ParseOutPointError}; /// that of the second sat of the genesis block coinbase output is /// `000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f:0:1`, and /// so on and so on. -#[derive(Debug, PartialEq, Copy, Clone, Eq, PartialOrd, Ord, Default, Hash)] +#[derive( + Debug, + PartialEq, + Copy, + Clone, + Eq, + PartialOrd, + Ord, + Default, + Hash, + DeserializeFromStr, + SerializeDisplay, +)] pub struct SatPoint { pub outpoint: OutPoint, pub offset: u64, @@ -39,24 +51,6 @@ impl Decodable for SatPoint { } } -impl Serialize for SatPoint { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for SatPoint { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) - } -} - impl FromStr for SatPoint { type Err = Error; diff --git a/src/runes/spaced_rune.rs b/crates/ordinals/src/spaced_rune.rs similarity index 58% rename from src/runes/spaced_rune.rs rename to crates/ordinals/src/spaced_rune.rs index d1dedafe80..72c19125c8 100644 --- a/src/runes/spaced_rune.rs +++ b/crates/ordinals/src/spaced_rune.rs @@ -1,9 +1,17 @@ use super::*; -#[derive(Copy, Clone, Debug, PartialEq, Ord, PartialOrd, Eq)] +#[derive( + Copy, Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Default, DeserializeFromStr, SerializeDisplay, +)] pub struct SpacedRune { - pub(crate) rune: Rune, - pub(crate) spacers: u32, + pub rune: Rune, + pub spacers: u32, +} + +impl SpacedRune { + pub fn new(rune: Rune, spacers: u32) -> Self { + Self { rune, spacers } + } } impl FromStr for SpacedRune { @@ -17,22 +25,22 @@ impl FromStr for SpacedRune { match c { 'A'..='Z' => rune.push(c), '.' | '•' => { - let flag = 1 << rune.len().checked_sub(1).context("leading spacer")?; + let flag = 1 << rune.len().checked_sub(1).ok_or(Error::LeadingSpacer)?; if spacers & flag != 0 { - bail!("double spacer"); + return Err(Error::DoubleSpacer); } spacers |= flag; } - _ => bail!("invalid character"), + _ => return Err(Error::Character(c)), } } if 32 - spacers.leading_zeros() >= rune.len().try_into().unwrap() { - bail!("trailing spacer") + return Err(Error::TrailingSpacer); } Ok(SpacedRune { - rune: rune.parse()?, + rune: rune.parse().map_err(Error::Rune)?, spacers, }) } @@ -54,24 +62,29 @@ impl Display for SpacedRune { } } -impl Serialize for SpacedRune { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } +#[derive(Debug, PartialEq)] +pub enum Error { + LeadingSpacer, + TrailingSpacer, + DoubleSpacer, + Character(char), + Rune(rune::Error), } -impl<'de> Deserialize<'de> for SpacedRune { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Character(c) => write!(f, "invalid character `{c}`"), + Self::DoubleSpacer => write!(f, "double spacer"), + Self::LeadingSpacer => write!(f, "leading spacer"), + Self::TrailingSpacer => write!(f, "trailing spacer"), + Self::Rune(err) => write!(f, "{err}"), + } } } +impl std::error::Error for Error {} + #[cfg(test)] mod tests { use super::*; @@ -104,29 +117,30 @@ mod tests { } assert_eq!( - ".A".parse::().unwrap_err().to_string(), - "leading spacer", + ".A".parse::().unwrap_err(), + Error::LeadingSpacer, ); assert_eq!( - "A..B".parse::().unwrap_err().to_string(), - "double spacer", + "A..B".parse::().unwrap_err(), + Error::DoubleSpacer, ); assert_eq!( - "A.".parse::().unwrap_err().to_string(), - "trailing spacer", + "A.".parse::().unwrap_err(), + Error::TrailingSpacer, ); assert_eq!( - "Ax".parse::().unwrap_err().to_string(), - "invalid character", + "Ax".parse::().unwrap_err(), + Error::Character('x') ); case("A.B", "AB", 0b1); case("A.B.C", "ABC", 0b11); case("A•B", "AB", 0b1); case("A•B•C", "ABC", 0b11); + case("A•BC", "ABC", 0b1); } #[test] diff --git a/crates/ordinals/src/terms.rs b/crates/ordinals/src/terms.rs new file mode 100644 index 0000000000..9cd1e1a319 --- /dev/null +++ b/crates/ordinals/src/terms.rs @@ -0,0 +1,9 @@ +use super::*; + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] +pub struct Terms { + pub amount: Option, + pub cap: Option, + pub height: (Option, Option), + pub offset: (Option, Option), +} diff --git a/crates/ordinals/src/varint.rs b/crates/ordinals/src/varint.rs new file mode 100644 index 0000000000..cef2086a5e --- /dev/null +++ b/crates/ordinals/src/varint.rs @@ -0,0 +1,175 @@ +use super::*; + +pub fn encode_to_vec(mut n: u128, v: &mut Vec) { + while n >> 7 > 0 { + v.push(n.to_le_bytes()[0] | 0b1000_0000); + n >>= 7; + } + v.push(n.to_le_bytes()[0]); +} + +pub fn decode(buffer: &[u8]) -> Option<(u128, usize)> { + try_decode(buffer).ok() +} + +fn try_decode(buffer: &[u8]) -> Result<(u128, usize), Error> { + let mut n = 0u128; + + for (i, &byte) in buffer.iter().enumerate() { + if i > 18 { + return Err(Error::Overlong); + } + + let value = u128::from(byte) & 0b0111_1111; + + if i == 18 && value & 0b0111_1100 != 0 { + return Err(Error::Overflow); + } + + n |= value << (7 * i); + + if byte & 0b1000_0000 == 0 { + return Ok((n, i + 1)); + } + } + + Err(Error::Unterminated) +} + +pub fn encode(n: u128) -> Vec { + let mut v = Vec::new(); + encode_to_vec(n, &mut v); + v +} + +#[derive(PartialEq, Debug)] +enum Error { + Overlong, + Overflow, + Unterminated, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Overlong => write!(f, "too long"), + Self::Overflow => write!(f, "overflow"), + Self::Unterminated => write!(f, "unterminated"), + } + } +} + +impl std::error::Error for Error {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn zero_round_trips_successfully() { + let n = 0; + let encoded = encode(n); + let (decoded, length) = try_decode(&encoded).unwrap(); + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); + } + + #[test] + fn u128_max_round_trips_successfully() { + let n = u128::MAX; + let encoded = encode(n); + let (decoded, length) = try_decode(&encoded).unwrap(); + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); + } + + #[test] + fn powers_of_two_round_trip_successfully() { + for i in 0..128 { + let n = 1 << i; + let encoded = encode(n); + let (decoded, length) = try_decode(&encoded).unwrap(); + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); + } + } + + #[test] + fn alternating_bit_strings_round_trip_successfully() { + let mut n = 0; + + for i in 0..129 { + n = n << 1 | (i % 2); + let encoded = encode(n); + let (decoded, length) = try_decode(&encoded).unwrap(); + assert_eq!(decoded, n); + assert_eq!(length, encoded.len()); + } + } + + #[test] + fn varints_may_not_be_longer_than_19_bytes() { + const VALID: [u8; 19] = [ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0, + ]; + + const INVALID: [u8; 20] = [ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 128, 0, + ]; + + assert_eq!(try_decode(&VALID), Ok((0, 19))); + assert_eq!(try_decode(&INVALID), Err(Error::Overlong)); + } + + #[test] + fn varints_may_not_overflow_u128() { + assert_eq!( + try_decode(&[ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 64, + ]), + Err(Error::Overflow) + ); + assert_eq!( + try_decode(&[ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 32, + ]), + Err(Error::Overflow) + ); + assert_eq!( + try_decode(&[ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 16, + ]), + Err(Error::Overflow) + ); + assert_eq!( + try_decode(&[ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 8, + ]), + Err(Error::Overflow) + ); + assert_eq!( + try_decode(&[ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 4, + ]), + Err(Error::Overflow) + ); + assert_eq!( + try_decode(&[ + 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, + 2, + ]), + Ok((2u128.pow(127), 19)) + ); + } + + #[test] + fn varints_must_be_terminated() { + assert_eq!(try_decode(&[128]), Err(Error::Unterminated)); + } +} diff --git a/deploy/bitcoin.conf b/deploy/bitcoin.conf index 6f34a15194..b307bf5fe6 100644 --- a/deploy/bitcoin.conf +++ b/deploy/bitcoin.conf @@ -1,3 +1,5 @@ +datacarriersize=1000000 datadir=/var/lib/bitcoind maxmempool=1024 +mempoolfulrbf=1 txindex=1 diff --git a/deploy/ord.service b/deploy/ord.service index d43f14b810..883b54db59 100644 --- a/deploy/ord.service +++ b/deploy/ord.service @@ -12,7 +12,7 @@ ExecStart=/usr/local/bin/ord \ --bitcoin-data-dir /var/lib/bitcoind \ --chain ${CHAIN} \ --config-dir /var/lib/ord \ - --data-dir /var/lib/ord \ + --datadir /var/lib/ord \ --index-runes \ --index-sats \ server \ diff --git a/docs/po/de.po b/docs/po/de.po index 44fcb7ee00..1665df9082 100644 --- a/docs/po/de.po +++ b/docs/po/de.po @@ -5111,11 +5111,11 @@ msgstr "" #: src\guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index run` or give it a specific filename and path " +"`ord --datadir index run` or give it a specific filename and path " "with `ord --index index run`." msgstr "" "Sie können natürlich auch den Speicherort des Dataverzeichnisses selbst " -"festlegen, indem Sie `ord --data-dir index run` verwenden oder ihm " +"festlegen, indem Sie `ord --datadir index run` verwenden oder ihm " "einen bestimmten Dateinamen und path mit `ord --index index run` " "zuweisen." diff --git a/docs/po/es.po b/docs/po/es.po index b08e7c1872..79b2199fc1 100644 --- a/docs/po/es.po +++ b/docs/po/es.po @@ -5062,11 +5062,11 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" "También tienes la opción de determinar la ubicación del directorio de datos " -"utilizando el comando `ord --data-dir index update` o asignarle un nombre " +"utilizando el comando `ord --datadir index update` o asignarle un nombre " "de archivo y ruta específicos utilizando el comando `ord --index " "index update`." diff --git a/docs/po/fil.po b/docs/po/fil.po index 96b41c8586..3901098be2 100644 --- a/docs/po/fil.po +++ b/docs/po/fil.po @@ -3388,7 +3388,7 @@ msgid "" "content, the higher the fee that the inscription transaction must pay." msgstr "" "Gayundin, ang mga inscription ay kasama sa mga transaksyon, kaya kung mas " -"maraming nilalaman, mas mataas ang bayad para sa transaksyon sa incription." +"maraming nilalaman, mas mataas ang bayad para sa transaksyon sa inscription." #: src/guides/inscriptions.md:204 msgid "" @@ -5078,11 +5078,11 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" "Siyempre maaari mo ring itakda ang lokasyon ng direktoryo ng data sa iyong " -"sarili gamit ang `ord --data-dir index update` o bigyan ito ng " +"sarili gamit ang `ord --datadir index update` o bigyan ito ng " "partikular na filename at path na may `ord --index index update`." #: src/bounties.md:1 diff --git a/docs/po/fr.po b/docs/po/fr.po index 73f45a1a06..f4b71d221d 100644 --- a/docs/po/fr.po +++ b/docs/po/fr.po @@ -5192,10 +5192,10 @@ msgstr "" #: src\guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" -"Vous pouvez bien sûr aussi définir `ord --data-dir index update` ou " +"Vous pouvez bien sûr aussi définir `ord --datadir index update` ou " "lui donner un nom de fichier et un chemin d’accès spécifiques avec `ord --" "index index update`." diff --git a/docs/po/hi.po b/docs/po/hi.po index d79a1db059..da73433841 100644 --- a/docs/po/hi.po +++ b/docs/po/hi.po @@ -4863,10 +4863,10 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" -"आप निस्‍संदेह `ord --data-dir index update` के साथ डेटा निर्देशिका का स्थान स्वयं भी सेट " +"आप निस्‍संदेह `ord --datadir index update` के साथ डेटा निर्देशिका का स्थान स्वयं भी सेट " "कर सकते हैं या इसे `ord --index index update` के साथ एक विशिष्ट फ़ाइल नाम और " "पथ भी दे सकते हैं।" diff --git a/docs/po/it.po b/docs/po/it.po index 4c42b9c909..bfecbbb7b1 100644 --- a/docs/po/it.po +++ b/docs/po/it.po @@ -6062,11 +6062,11 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" "Naturalmente potete anche impostare la posizione della directory dei dati " -"con `ord --data-dir index update` o assegnare un nome di file e un percorso " +"con `ord --datadir index update` o assegnare un nome di file e un percorso " "specifici con `ord --index index update`." #: src/bounties.md:1 diff --git a/docs/po/ja.po b/docs/po/ja.po index 3ff53a1196..4385552f4c 100644 --- a/docs/po/ja.po +++ b/docs/po/ja.po @@ -4775,10 +4775,10 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" -"もちろん自分でデータの目録の位置を設置することができます,`ord --data-dir " +"もちろん自分でデータの目録の位置を設置することができます,`ord --datadir " " index update` または指定された特定のファイル名とパスに‘ord --index " "で索引運行します’。" diff --git a/docs/po/ko.po b/docs/po/ko.po index b57e95c68d..e9ebe8903c 100644 --- a/docs/po/ko.po +++ b/docs/po/ko.po @@ -4794,10 +4794,10 @@ msgstr "" #: /workspaces/ord_ko/docs/src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" -"물론 `ord --data-dir index update`로 데이터 디렉터리의 위치를 직접 설정" +"물론 `ord --datadir index update`로 데이터 디렉터리의 위치를 직접 설정" "하거나 `ord --index index update`로 특정 파일 이름과 경로를 지정" "할 수도 있다." diff --git a/docs/po/pt.po b/docs/po/pt.po index 7353e02070..46eec0d143 100644 --- a/docs/po/pt.po +++ b/docs/po/pt.po @@ -5006,10 +5006,10 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" "You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " +"`ord --datadir index update` or give it a specific filename and path " "with `ord --index index update`." msgstr "" -"É claro que você também pode definir a localização do diretório de dados com `ord --data-dir index update` " +"É claro que você também pode definir a localização do diretório de dados com `ord --datadir index update` " "ou fornecer um nome de arquivo e caminho específicos com `ord --index index update`." #: src/bounties.md:1 diff --git a/docs/po/ru.po b/docs/po/ru.po index 6f91e6bd85..15a7e5228a 100644 --- a/docs/po/ru.po +++ b/docs/po/ru.po @@ -4024,10 +4024,10 @@ msgstr "" #: src/guides/reindexing.md:29 msgid "" -"You can of course also set the location of the data directory yourself with `ord --data-dir index run` or give it a specific filename and path with `ord " +"You can of course also set the location of the data directory yourself with `ord --datadir index run` or give it a specific filename and path with `ord " "--index index run`." msgstr "" -"Конечно, можно также самостоятельно задать расположение директории данных командой `ord --data-dir index run` или указать ему конкретное имя файла и путь " +"Конечно, можно также самостоятельно задать расположение директории данных командой `ord --datadir index run` или указать ему конкретное имя файла и путь " "к нему командой `ord --index index run`." #: src/bounties.md:1 diff --git a/docs/po/zh.po b/docs/po/zh.po index e0871b9c07..ff5388eaf5 100644 --- a/docs/po/zh.po +++ b/docs/po/zh.po @@ -23,58 +23,62 @@ msgstr "概述" msgid "Digital Artifacts" msgstr "数字文物" -#: src/SUMMARY.md:7 src/SUMMARY.md:17 src/overview.md:221 src/inscriptions.md:1 +#: src/SUMMARY.md:7 src/overview.md:221 src/inscriptions.md:1 msgid "Inscriptions" msgstr "铭文" -#: src/SUMMARY.md:8 src/inscriptions/metadata.md:1 +#: src/SUMMARY.md:8 src/inscriptions/delegate.md:1 +msgid "Delegate" +msgstr "委托" + +#: src/SUMMARY.md:9 src/inscriptions/metadata.md:1 msgid "Metadata" msgstr "元数据" -#: src/SUMMARY.md:9 src/inscriptions/provenance.md:1 +#: src/SUMMARY.md:10 src/inscriptions/pointer.md:1 +msgid "Pointer" +msgstr "指针" + +#: src/SUMMARY.md:11 src/inscriptions/provenance.md:1 msgid "Provenance" msgstr "溯源" -#: src/SUMMARY.md:10 src/inscriptions/recursion.md:1 +#: src/SUMMARY.md:12 src/inscriptions/recursion.md:1 msgid "Recursion" msgstr "递归" -#: src/SUMMARY.md:11 src/inscriptions/pointer.md:1 -msgid "Pointer" -msgstr "指针" +#: src/SUMMARY.md:13 src/inscriptions/rendering.md:1 +msgid "Rendering" +msgstr "渲染" -#: src/SUMMARY.md:12 +#: src/SUMMARY.md:14 msgid "FAQ" msgstr "常见问题" -#: src/SUMMARY.md:13 +#: src/SUMMARY.md:15 msgid "Contributing" msgstr "贡献" -#: src/SUMMARY.md:14 src/donate.md:1 +#: src/SUMMARY.md:16 src/donate.md:1 msgid "Donate" msgstr "捐赠" -#: src/SUMMARY.md:15 +#: src/SUMMARY.md:17 msgid "Guides" msgstr "指引" -#: src/SUMMARY.md:16 +#: src/SUMMARY.md:18 msgid "Explorer" msgstr "浏览器" -#: src/SUMMARY.md:18 src/guides/batch-inscribing.md:1 +#: src/SUMMARY.md:19 src/guides/wallet.md:1 +msgid "Wallet" +msgstr "麻雀钱包" + +#: src/SUMMARY.md:20 src/guides/batch-inscribing.md:1 msgid "Batch Inscribing" msgstr "批量铸造" -#: src/SUMMARY.md:19 src/guides/sat-hunting.md:1 -msgid "Sat Hunting" -msgstr "猎聪" - -#: src/SUMMARY.md:20 src/guides/teleburning.md:1 -msgid "Teleburning" -msgstr "燃烧传送" - #: src/SUMMARY.md:21 src/guides/collecting.md:1 msgid "Collecting" msgstr "收藏" @@ -83,35 +87,47 @@ msgstr "收藏" msgid "Sparrow Wallet" msgstr "麻雀钱包" -#: src/SUMMARY.md:23 src/guides/testing.md:1 -msgid "Testing" -msgstr "调试" - -#: src/SUMMARY.md:24 src/guides/moderation.md:1 +#: src/SUMMARY.md:23 src/guides/moderation.md:1 msgid "Moderation" msgstr "调节" -#: src/SUMMARY.md:25 src/guides/reindexing.md:1 +#: src/SUMMARY.md:24 src/guides/reindexing.md:1 msgid "Reindexing" msgstr "重新索引" -#: src/SUMMARY.md:26 +#: src/SUMMARY.md:25 src/guides/sat-hunting.md:1 +msgid "Sat Hunting" +msgstr "猎聪" + +#: src/SUMMARY.md:26 src/guides/settings.md:1 +msgid "Settings" +msgstr "调试" + +#: src/SUMMARY.md:27 src/guides/teleburning.md:1 +msgid "Teleburning" +msgstr "燃烧传送" + +#: src/SUMMARY.md:28 src/guides/testing.md:1 +msgid "Testing" +msgstr "调试" + +#: src/SUMMARY.md:29 msgid "Bounties" msgstr "赏金" -#: src/SUMMARY.md:27 +#: src/SUMMARY.md:30 msgid "Bounty 0: 100,000 sats Claimed!" msgstr "任务 0: 100,000 sats 完成!" -#: src/SUMMARY.md:28 +#: src/SUMMARY.md:31 msgid "Bounty 1: 200,000 sats Claimed!" msgstr "任务 1: 200,000 sats 完成!" -#: src/SUMMARY.md:29 +#: src/SUMMARY.md:32 msgid "Bounty 2: 300,000 sats Claimed!" msgstr "任务 2: 300,000 sats 完成!" -#: src/SUMMARY.md:30 +#: src/SUMMARY.md:33 msgid "Bounty 3: 400,000 sats" msgstr "任务 3: 400,000 sats" @@ -180,10 +196,10 @@ msgstr "有关铭文的更多详细信息,请参阅[铭文](inscriptions.md)." #: src/introduction.md:31 msgid "" "When you're ready to get your hands dirty, a good place to start is with " -"[inscriptions](guides/inscriptions.md), a curious species of digital " -"artifact enabled by ordinal theory." +"[inscriptions](guides/wallet.md), a curious species of digital artifact " +"enabled by ordinal theory." msgstr "" -"当您准备好亲自动手时,一个好的起点是[铭文](guides/inscriptions.md)这是一种由" +"当您准备好亲自动手时,一个好的起点是[铭文](inscriptions.md)这是一种由" "序数理论支持的独特的数字文物。" #: src/introduction.md:35 @@ -227,8 +243,7 @@ msgid "" "[Ordinal Theory Explained: Satoshi Serial Numbers and NFTs on Bitcoin]" "(https://www.youtube.com/watch?v=rSS0O2KQpsI)" msgstr "" -"[解释序数理论: 聪的序列号和比特币上的NFT](https://www.youtube.com/watch?" -"v=rSS0O2KQpsI)" +"[解释序数理论: 聪的序列号和比特币上的NFT](https://www.youtube.com/watch?v=rSS0O2KQpsI)" #: src/introduction.md:50 msgid "" @@ -237,14 +252,6 @@ msgid "" msgstr "" "[CaseyRodarmor的序数理论工作坊 ](https://www.youtube.com/watch?v=MC_haVa6N3I)" -#: src/introduction.md:51 -msgid "" -"[Ordinal Art: Mint Your own NFTs on Bitcoin w/ @rodarmor](https://www." -"youtube.com/watch?v=j5V33kV3iqo)" -msgstr "" -"[序数艺术:在比特币上铸造你自己的NFT w/ @rodarmor](https://www.youtube.com/" -"watch?v=j5V33kV3iqo)" - #: src/overview.md:1 msgid "Ordinal Theory Overview" msgstr "序数理论概述" @@ -665,7 +672,6 @@ msgid "`common`: 1.9 quadrillion" msgstr "`普通`: 1千900万亿" #: src/overview.md:186 -#, fuzzy msgid "`uncommon`: 808,262" msgstr "`非普通`: 808,262" @@ -1032,7 +1038,6 @@ msgid "" msgstr "首先字符串`ord`被推送,以消除铭文与信封其他用途的歧义。" #: src/inscriptions.md:56 -#, fuzzy msgid "" "`OP_PUSH 1` indicates that the next push contains the content type, and " "`OP_PUSH 0`indicates that subsequent data pushes contain the content itself. " @@ -1047,9 +1052,10 @@ msgstr "" #: src/inscriptions.md:62 msgid "" "The inscription content is contained within the input of a reveal " -"transaction, and the inscription is made on the first sat of its input. This " -"sat can then be tracked using the familiar rules of ordinal theory, allowing " -"it to be transferred, bought, sold, lost to fees, and recovered." +"transaction, and the inscription is made on the first sat of its input if it " +"has no pointer field. This sat can then be tracked using the familiar rules " +"of ordinal theory, allowing it to be transferred, bought, sold, lost to " +"fees, and recovered." msgstr "" "铭文内容包含在reveal交易的输入中,并且铭文是铭刻在其第一个输出的第一个聪" "(Satoshi)上。我们可以使用熟悉的序数理论规则来跟踪这个聪 sat,允许它被转移、" @@ -1080,19 +1086,55 @@ msgstr "" "值。" #: src/inscriptions.md:79 +msgid "Currently, there are six defined fields:" +msgstr "" + +#: src/inscriptions.md:81 msgid "" -"Currently, the only defined field is `content-type`, with a tag of `1`, " -"whose value is the MIME type of the body." +"`content_type`, with a tag of `1`, whose value is the MIME type of the body." msgstr "" "目前,唯一定义的字段是‘content-type’,标签为‘1’,其值是正文的 MIME 类型。" #: src/inscriptions.md:82 msgid "" +"`pointer`, with a tag of `2`, see [pointer docs](inscriptions/pointer.md)." +msgstr "" + +#: src/inscriptions.md:83 +msgid "" +"`parent`, with a tag of `3`, see [provenance](inscriptions/provenance.md)." +msgstr "" + +#: src/inscriptions.md:84 +msgid "" +"`metadata`, with a tag of `5`, see [metadata](inscriptions/metadata.md)." +msgstr "" + +#: src/inscriptions.md:85 +msgid "" +"`metaprotocol`, with a tag of `7`, whose value is the metaprotocol " +"identifier." +msgstr "" + +#: src/inscriptions.md:86 +msgid "" +"`content_encoding`, with a tag of `9`, whose value is the encoding of the " +"body." +msgstr "" +"目前,唯一定义的字段是‘content-type’,标签为‘1’,其值是正文的 MIME 类型。" + +#: src/inscriptions.md:87 +msgid "" +"`delegate`, with a tag of `11`, see [delegate](inscriptions/delegate.md)." +msgstr "" + +#: src/inscriptions.md:89 +msgid "" "The beginning of the body and end of fields is indicated with an empty data " "push." msgstr "正文的开头和字段的结尾用'空数据'指示推送。" -#: src/inscriptions.md:85 +#: src/inscriptions.md:92 msgid "" "Unrecognized tags are interpreted differently depending on whether they are " "even or odd, following the \"it's okay to be odd\" rule used by the " @@ -1101,7 +1143,7 @@ msgstr "" "无法识别的标签的解释不同,取决于它们是否是偶数或奇数,遵循闪电网络\"可以是奇" "数\"的规则。" -#: src/inscriptions.md:89 +#: src/inscriptions.md:96 msgid "" "Even tags are used for fields which may affect creation, initial assignment, " "or transfer of an inscription. Thus, inscriptions with unrecognized even " @@ -1110,7 +1152,7 @@ msgstr "" "甚至标签也用于可能影响创建、初始分配的字段,或铭文的转移。因此,即使无法识别" "的铭文,字段也必须显示为\"未绑定\",即没有位置。" -#: src/inscriptions.md:93 +#: src/inscriptions.md:100 msgid "" "Odd tags are used for fields which do not affect creation, initial " "assignment, or transfer, such as additional metadata, and thus are safe to " @@ -1119,11 +1161,11 @@ msgstr "" "奇数标签用于不影响创建、初始的字段,分配或转移,例如附加元数据,因此是选择忽略" "是安全的。" -#: src/inscriptions.md:96 +#: src/inscriptions.md:103 msgid "Inscription IDs" msgstr "铭文身份ID" -#: src/inscriptions.md:99 +#: src/inscriptions.md:106 msgid "" "The inscriptions are contained within the inputs of a reveal transaction. In " "order to uniquely identify them they are assigned an ID of the form:" @@ -1131,11 +1173,11 @@ msgstr "" "铭文包含在揭示交易的输入中。为了唯一地识别他们,他们被分配了一个以下形式的 " "ID:" -#: src/inscriptions.md:102 +#: src/inscriptions.md:109 msgid "`521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0`" msgstr "" -#: src/inscriptions.md:104 +#: src/inscriptions.md:111 msgid "" "The part in front of the `i` is the transaction ID (`txid`) of the reveal " "transaction. The number after the `i` defines the index (starting at 0) of " @@ -1144,7 +1186,7 @@ msgstr "" " `i` 的前面部分是交易ID (`txid`),在`i`之后的数字定义了新的铭文在交易总被铭刻" "的索引的位置 (从 0 开始)" -#: src/inscriptions.md:108 +#: src/inscriptions.md:115 msgid "" "Inscriptions can either be located in different inputs, within the same " "input or a combination of both. In any case the ordering is clear, since a " @@ -1154,59 +1196,90 @@ msgstr "" "铭文可以位于同一输入中的不同输入中,可以是同一个输入或两者的组合。在任何情况" "下,顺序都是明确的,因为解析器将连续检查输入并查找所有铭文`信封`" -#: src/inscriptions.md:112 +#: src/inscriptions.md:119 msgid "Input" -msgstr "" +msgstr "输入" -#: src/inscriptions.md:112 +#: src/inscriptions.md:119 msgid "Inscription Count" -msgstr "" +msgstr "铭文数量" -#: src/inscriptions.md:112 +#: src/inscriptions.md:119 msgid "Indices" -msgstr "" +msgstr "指数" -#: src/inscriptions.md:114 src/inscriptions.md:117 +#: src/inscriptions.md:121 src/inscriptions.md:124 msgid "0" msgstr "" -#: src/inscriptions.md:114 src/inscriptions.md:116 +#: src/inscriptions.md:121 src/inscriptions.md:123 msgid "2" msgstr "" -#: src/inscriptions.md:114 +#: src/inscriptions.md:121 msgid "i0, i1" msgstr "" -#: src/inscriptions.md:115 src/inscriptions.md:118 +#: src/inscriptions.md:122 src/inscriptions.md:125 msgid "1" msgstr "" -#: src/inscriptions.md:115 +#: src/inscriptions.md:122 msgid "i2" msgstr "" -#: src/inscriptions.md:116 src/inscriptions.md:117 +#: src/inscriptions.md:123 src/inscriptions.md:124 msgid "3" msgstr "" -#: src/inscriptions.md:116 +#: src/inscriptions.md:123 msgid "i3, i4, i5" msgstr "" -#: src/inscriptions.md:118 +#: src/inscriptions.md:125 msgid "4" msgstr "" -#: src/inscriptions.md:118 +#: src/inscriptions.md:125 msgid "i6" msgstr "" -#: src/inscriptions.md:120 +#: src/inscriptions.md:127 +msgid "Inscription Numbers" +msgstr "铭文" + +#: src/inscriptions.md:130 +msgid "" +"Inscriptions are assigned inscription numbers starting at zero, first by the " +"order reveal transactions appear in blocks, and the order that reveal " +"envelopes appear in those transactions." +msgstr "" +"铭文被分配的铭文编号从零开始,首先按照揭示交易在区块中出现的顺序,以及揭示信封在这些交易中出现的顺序。" + +#: src/inscriptions.md:134 +msgid "" +"Due to a historical bug in `ord` which cannot be fixed without changing a " +"great many inscription numbers, inscriptions which are revealed and then " +"immediately spent to fees are numbered as if they appear last in the block " +"in which they are revealed." +msgstr "" +"由于在`ord`中的一个过往的错误,如果不改变大量的铭文编号就无法修复 " +"因此,那些被揭示出来然后立即用于支付费用的铭文,其编号就好像它们是在被揭示出来的区块中最后出现的一样。" + +#: src/inscriptions.md:139 +msgid "" +"Inscriptions which are cursed are numbered starting at negative one, " +"counting down. Cursed inscriptions on and after the jubilee at block 824544 " +"are vindicated, and are assigned positive inscription numbers." +msgstr "" +"被诅咒的铭文从负一开始编号,依次递减。在区块824544及之后的朱比利(Jubilee)事件中," +"被诅咒的铭文得到了宽恕,并被分配了正数的铭文编号。" + +#: src/inscriptions.md:143 msgid "Sandboxing" msgstr "沙盒化" -#: src/inscriptions.md:123 +#: src/inscriptions.md:146 msgid "" "HTML and SVG inscriptions are sandboxed in order to prevent references to " "off-chain content, thus keeping inscriptions immutable and self-contained." @@ -1214,7 +1287,7 @@ msgstr "" "HTML 和 SVG 铭文被沙箱化,以防止引用链下内容,从而保持铭文的不可变性和独立" "性。" -#: src/inscriptions.md:126 +#: src/inscriptions.md:149 msgid "" "This is accomplished by loading HTML and SVG inscriptions inside `iframes` " "with the `sandbox` attribute, as well as serving inscription content with " @@ -1223,6 +1296,116 @@ msgstr "" "这是通过在“iframes”中加载 HTML 和 SVG 铭文来完成的`sandbox` 属性,以及提供铭" "文内容Content-Security-Policy”标头。" +#: src/inscriptions.md:153 +msgid "Reinscriptions" +msgstr "再刻铭文" + +#: src/inscriptions.md:156 +msgid "" +"Previously inscribed sats can be reinscribed with the `--reinscribe` command " +"if the inscription is present in the wallet. This will only append an " +"inscription to a sat, not change the initial inscription." +msgstr "" + +#: src/inscriptions.md:160 +msgid "" +"Reinscribe with satpoint: `ord wallet inscribe --fee-rate --" +"reinscribe --file --satpoint `" +msgstr "" +"如果铭文存在于钱包中,之前铭刻的sats可以使用--reinscribe命令进行重新铭刻。" +"这将只会在一个sat上追加一个铭文,而不会改变最初的铭文。" + +#: src/inscriptions.md:163 +msgid "" +"Reinscribe on a sat (requires sat index): `ord --index-sats wallet inscribe " +"--fee-rate --reinscribe --file --sat `" +msgstr "" +"在一个聪上再刻录铭文 (需要聪索引): `ord --index-sats wallet inscribe " +"--fee-rate --reinscribe --file --sat `" + +#: src/inscriptions/delegate.md:4 +msgid "" +"Inscriptions may nominate a delegate inscription. Requests for the content " +"of an inscription with a delegate will instead return the content and " +"content type of the delegate. This can be used to cheaply create copies of " +"an inscription." +msgstr "" +"铭文可以指定一个代理铭文。对带有代理的铭文内容的请求,将返回代理的内容和内容类型。" +"这可以用来低成本地创建铭文的副本。" + +#: src/inscriptions/delegate.md:8 src/inscriptions/provenance.md:14 +msgid "Specification" +msgstr "规范" + +#: src/inscriptions/delegate.md:10 +msgid "To create an inscription I with delegate inscription D:" +msgstr "为父系铭文P创建一个子铭文C:" + +#: src/inscriptions/delegate.md:12 +msgid "" +"Create an inscription D. Note that inscription D does not have to exist when " +"making inscription I. It may be inscribed later. Before inscription D is " +"inscribed, requests for the content of inscription I will return a 404." +msgstr "" +"创建一个铭文D。请注意,在创建铭文I时,铭文D不必已经存在。它可以稍后被铭刻。" +"在铭文D被铭刻之前,对铭文I内容的请求将返回404错误" + +#: src/inscriptions/delegate.md:15 +msgid "" +"Include tag `11`, i.e. `OP_PUSH 11`, in I, with the value of the serialized " +"binary inscription ID of D, serialized as the 32-byte `TXID`, followed by " +"the four-byte little-endian `INDEX`, with trailing zeroes omitted." +msgstr "" +"在C中包含标签`3`,即`OP_PUSH 3`,其值为P的序列化二进制铭文ID序列化为32字节的" +"`TXID`,后跟四字节的小端`INDEX`,不含末尾的零。" + +#: src/inscriptions/delegate.md:19 src/inscriptions/provenance.md:24 +msgid "" +"_NB_ The bytes of a bitcoin transaction ID are reversed in their text " +"representation, so the serialized transaction ID will be in the opposite " +"order." +msgstr "" +"_请注意_,比特币交易ID的字节在文本中的表现形式是反向的,所以序列化的交易ID会" +"以相反的顺序呈现。" + +#: src/inscriptions/delegate.md:22 src/inscriptions/metadata.md:30 +#: src/inscriptions/provenance.md:27 src/guides/reindexing.md:15 +#: src/guides/teleburning.md:23 src/guides/testing.md:62 +msgid "Example" +msgstr "示例" + +#: src/inscriptions/delegate.md:24 +msgid "" +"An example of an inscription which delegates to " +"`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi0`:" +msgstr "" +"子铭文的一个示例 " +"`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi0`:" + +#: src/inscriptions/delegate.md:27 +msgid "" +"```\n" +"OP_FALSE\n" +"OP_IF\n" +" OP_PUSH \"ord\"\n" +" OP_PUSH 11\n" +" OP_PUSH " +"0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100\n" +"OP_ENDIF\n" +"```" +msgstr "" + +#: src/inscriptions/delegate.md:36 +msgid "Note that the value of tag `11` is decimal, not hex." +msgstr "请注意,标签`11`的值是十进制的,而不是十六进制的。" + +#: src/inscriptions/delegate.md:38 +msgid "" +"The delegate field value uses the same encoding as the parent field. See " +"[provenance](provenance.md) for more examples of inscription ID encodings;" +msgstr "" +"代理字段的值使用与父字段相同的编码方式。有关铭文ID编码的更多示例,请参见[provenance](provenance.md) " + #: src/inscriptions/metadata.md:4 msgid "" "Inscriptions may include [CBOR](https://cbor.io/) metadata, stored as data " @@ -1230,10 +1413,9 @@ msgid "" "metadata longer than 520 bytes must be split into multiple tag `5` fields, " "which will then be concatenated before decoding." msgstr "" -"铭文可能包含[CBOR](https://cbor.io/) 元数据, 将以数据推送的形式储存在" -"带有标签 `5`的字段中. 由于数据推送的限制为520 字节 " -"因此超过520字节的元数据必须拆分到多个标签为 `5` 的字段中, " -"然后在解码前进行连接。" +"铭文可能包含[CBOR](https://cbor.io/) 元数据, 将以数据推送的形式储存在带有标" +"签 `5`的字段中. 由于数据推送的限制为520 字节 因此超过520字节的元数据必须拆分" +"到多个标签为 `5` 的字段中, 然后在解码前进行连接。" #: src/inscriptions/metadata.md:9 msgid "" @@ -1241,8 +1423,8 @@ msgid "" "with its inscription. Inscribers are encouraged to consider how metadata " "will be displayed, and make metadata concise and attractive." msgstr "" -"元数据是人类可读的数据,并且所有元数据都将与其铭文一起展示给用户" -"建议铭文铸造者考虑元数据展示的方式,使元数据简洁且吸引人。" +"元数据是人类可读的数据,并且所有元数据都将与其铭文一起展示给用户建议铭文铸造" +"者考虑元数据展示的方式,使元数据简洁且吸引人。" #: src/inscriptions/metadata.md:13 msgid "Metadata is rendered to HTML for display as follows:" @@ -1262,22 +1444,21 @@ msgstr "字节字符串将呈现为大写十六进制。" msgid "" "Arrays are rendered as `
    ` tags, with every element wrapped in `
  • ` " "tags." -msgstr "" -"数组将以 `
      ` 标签的形式呈现,每个元素都会被`
    • `标签包裹。 " +msgstr "数组将以 `
        ` 标签的形式呈现,每个元素都会被`
      • `标签包裹。 " #: src/inscriptions/metadata.md:20 msgid "" "Maps are rendered as `
        ` tags, with every key wrapped in `
        ` tags, and " "every value wrapped in `
        ` tags." msgstr "" -"映射将以 `
        ` 标签的形式呈现,每一个键被 `
        ` 标签包裹,每一个值被 `
        ` 标签包裹。" +"映射将以 `
        ` 标签的形式呈现,每一个键被 `
        ` 标签包裹,每一个值被 " +"`
        ` 标签包裹。" #: src/inscriptions/metadata.md:22 msgid "" "Tags are rendered as the tag , enclosed in a `` tag, followed by the " "value." -msgstr "" -"标签将以 `` 标签包裹的标签的形式呈现,紧接着是值。 " +msgstr "标签将以 `` 标签包裹的标签的形式呈现,紧接着是值。 " #: src/inscriptions/metadata.md:25 msgid "" @@ -1286,15 +1467,9 @@ msgid "" "bignums, and encoding such as indefinite values, may fail to display " "correctly or at all. Contributions to `ord` to remedy this are welcome." msgstr "" -"CBOR是一个包含许多不同数据类型和多种表达相同数据方式的复杂规格。" -"一些特殊的数据类型,如标签、浮点数和大数字,以及某些编码方式,如不定值,可能无法正确或完全显示。" -"欢迎为ord做出贡献来改善这个问题。" - -#: src/inscriptions/metadata.md:30 src/inscriptions/provenance.md:27 -#: src/guides/teleburning.md:23 src/guides/testing.md:18 -#: src/guides/reindexing.md:15 -msgid "Example" -msgstr "示例" +"CBOR是一个包含许多不同数据类型和多种表达相同数据方式的复杂规格。一些特殊的数" +"据类型,如标签、浮点数和大数字,以及某些编码方式,如不定值,可能无法正确或完" +"全显示。欢迎为ord做出贡献来改善这个问题。" #: src/inscriptions/metadata.md:33 msgid "" @@ -1302,15 +1477,14 @@ msgid "" "JSON. Keep in mind that this is _only_ for these examples, and JSON metadata " "will _not_ be displayed correctly." msgstr "" -"由于CBOR不属于人类可读的,在这些示例中,它将用JSON格式来表示。但请注意,这只适用于这些示例,JSON元数据将无法正确显示。" +"由于CBOR不属于人类可读的,在这些示例中,它将用JSON格式来表示。但请注意,这只" +"适用于这些示例,JSON元数据将无法正确显示。" #: src/inscriptions/metadata.md:37 msgid "" "The metadata `{\"foo\":\"bar\",\"baz\":[null,true,false,0]}` would be " "included in an inscription as:" -msgstr "" -"铭文中包含的元数据 `{\"foo\":\"bar\",\"baz\":[null,true,false,0]}` " - +msgstr "铭文中包含的元数据 `{\"foo\":\"bar\",\"baz\":[null,true,false,0]}` " #: src/inscriptions/metadata.md:39 msgid "" @@ -1376,8 +1550,103 @@ msgid "" "Which would then be concatenated into `{\"very\":\"long\",\"metadata\":" "\"is\",\"finally\":\"done\"}`." msgstr "" -"然后,可以被连接成 `{\"very\":\"long\",\"metadata\":" -"\"is\",\"finally\":\"done\"}`." +"然后,可以被连接成 `{\"very\":\"long\",\"metadata\":\"is\",\"finally\":" +"\"done\"}`." + +#: src/inscriptions/pointer.md:4 +msgid "" +"In order to make an inscription on a sat other than the first of its input, " +"a zero-based integer, called the \"pointer\", can be provided with tag `2`, " +"causing the inscription to be made on the sat at the given position in the " +"outputs. If the pointer is equal to or greater than the number of total sats " +"in the outputs of the inscribe transaction, it is ignored, and the " +"inscription is made as usual. The value of the pointer field is a little " +"endian integer, with trailing zeroes ignored." +msgstr "" +"为了在输入的第一个以外的sat上进行铭刻,可以提供一个以0为基础的整数,称作 \"指" +"针\",并配以标签 `2`, 这将导致铭文被做在给定位置的输出的sat上。 如果指针等于或" +"大于铭文交易输出中的总sat数,那么它将被忽略, 而铭文将像往常一样被铭刻。指针" +"字段的值是一个小端整数,尾随零将被忽略。 " + +#: src/inscriptions/pointer.md:12 +msgid "" +"An even tag is used, so that old versions of `ord` consider the inscription " +"to be unbound, instead of assigning it, incorrectly, to the first sat." +msgstr "" +"使用了偶数标签,所以旧版本的 `ord` 会把铭文视为无约束,而不是错误地将其分配到" +"第一个sat。" + +#: src/inscriptions/pointer.md:15 +msgid "" +"This can be used to create multiple inscriptions in a single transaction on " +"different sats, when otherwise they would be made on the same sat." +msgstr "" +"这可以用于一次性在不同的sat上创建多个铭文,否则它们将被制成在同一个sat上" + +#: src/inscriptions/pointer.md:18 src/inscriptions/recursion.md:62 +msgid "Examples" +msgstr "示例" + +#: src/inscriptions/pointer.md:21 +msgid "An inscription with pointer 255:" +msgstr "一个带有255指针的铭文" + +#: src/inscriptions/pointer.md:23 +msgid "" +"```\n" +"OP_FALSE\n" +"OP_IF\n" +" OP_PUSH \"ord\"\n" +" OP_PUSH 1\n" +" OP_PUSH \"text/plain;charset=utf-8\"\n" +" OP_PUSH 2\n" +" OP_PUSH 0xff\n" +" OP_PUSH 0\n" +" OP_PUSH \"Hello, world!\"\n" +"OP_ENDIF\n" +"```" +msgstr "" + +#: src/inscriptions/pointer.md:36 +msgid "An inscription with pointer 256:" +msgstr "一个带有256指针的铭文" + +#: src/inscriptions/pointer.md:38 +msgid "" +"```\n" +"OP_FALSE\n" +"OP_IF\n" +" OP_PUSH \"ord\"\n" +" OP_PUSH 1\n" +" OP_PUSH \"text/plain;charset=utf-8\"\n" +" OP_PUSH 2\n" +" OP_PUSH 0x0001\n" +" OP_PUSH 0\n" +" OP_PUSH \"Hello, world!\"\n" +"OP_ENDIF\n" +"```" +msgstr "" + +#: src/inscriptions/pointer.md:51 +msgid "" +"An inscription with pointer 256, with trailing zeroes, which are ignored:" +msgstr "带有指针256的铭文,尾随零被忽略:" + +#: src/inscriptions/pointer.md:53 +msgid "" +"```\n" +"OP_FALSE\n" +"OP_IF\n" +" OP_PUSH \"ord\"\n" +" OP_PUSH 1\n" +" OP_PUSH \"text/plain;charset=utf-8\"\n" +" OP_PUSH 2\n" +" OP_PUSH 0x000100\n" +" OP_PUSH 0\n" +" OP_PUSH \"Hello, world!\"\n" +"OP_ENDIF\n" +"```" +msgstr "" #: src/inscriptions/provenance.md:4 msgid "" @@ -1387,8 +1656,8 @@ msgid "" "collections, with the children of a parent inscription being members of the " "same collection." msgstr "" -"铭文的所有者可以创建子铭文,在链上信任地建立这些子铭文的源头," -"证明它们是由父铭文的所有者创建的。这可以用于收藏品,父铭文的子铭文属于同一收藏系列。" +"铭文的所有者可以创建子铭文,在链上信任地建立这些子铭文的源头,证明它们是由父" +"铭文的所有者创建的。这可以用于收藏品,父铭文的子铭文属于同一收藏系列。" #: src/inscriptions/provenance.md:9 msgid "" @@ -1397,12 +1666,9 @@ msgid "" "sub inscriptions representing collections that they create, with the " "children of those sub inscriptions being items in those collections." msgstr "" -"子铭文自己也可以有子铭文,从而形成复杂的层级结构。例如,一位艺术家可能创建一个代表自己的铭文," -"子铭文代表他们创建的合辑,而那些子铭文的子项就是合辑中的项目。" - -#: src/inscriptions/provenance.md:14 -msgid "Specification" -msgstr "规范" +"子铭文自己也可以有子铭文,从而形成复杂的层级结构。例如,一位艺术家可能创建一" +"个代表自己的铭文,子铭文代表他们创建的合辑,而那些子铭文的子项就是合辑中的项" +"目。" #: src/inscriptions/provenance.md:16 msgid "To create a child inscription C with parent inscription P:" @@ -1425,15 +1691,6 @@ msgstr "" "在C中包含标签`3`,即`OP_PUSH 3`,其值为P的序列化二进制铭文ID序列化为32字节的" "`TXID`,后跟四字节的小端`INDEX`,不含末尾的零。" -#: src/inscriptions/provenance.md:24 -msgid "" -"_NB_ The bytes of a bitcoin transaction ID are reversed in their text " -"representation, so the serialized transaction ID will be in the opposite " -"order." -msgstr "" -"_请注意_,比特币交易ID的字节在文本中的表现形式是反向的," -"所以序列化的交易ID会以相反的顺序呈现。" - #: src/inscriptions/provenance.md:29 msgid "" "An example of a child inscription of " @@ -1533,42 +1790,69 @@ msgid "" "A collection can be closed by burning the collection's parent inscription, " "which guarantees that no more items in the collection can be issued." msgstr "" -"通过销毁集合的父铭文,可以关闭一个集合,这保证了该集合中不能再发行更多的项目。 " +"通过销毁集合的父铭文,可以关闭一个集合,这保证了该集合中不能再发行更多的项" +"目。 " #: src/inscriptions/recursion.md:4 msgid "" "An important exception to [sandboxing](../inscriptions.md#sandboxing) is " -"recursion: access to `ord`'s `/content` endpoint is permitted, allowing " -"inscriptions to access the content of other inscriptions by requesting `/" -"content/`." +"recursion. Recursive endpoints are whitelisted endpoints that allow access " +"to on-chain data, including the content of other inscriptions." msgstr "" "[沙盒化](../inscriptions.md#sandboxing)的一个重要例外是递归:访问“ord”的“/" "content”允许端点,允许铭文访问其他端点的内容通过请求 `/content/" "` 来获取铭文。" -#: src/inscriptions/recursion.md:9 -msgid "This has a number of interesting use-cases:" -msgstr "这有许多有趣的用例:" +#: src/inscriptions/recursion.md:8 +msgid "" +"Since changes to recursive endpoints might break inscriptions that rely on " +"them, recursive endpoints have backwards-compatibility guarantees not shared " +"by `ord server`'s other endpoints. In particular:" +msgstr "" +"由于对递归端点的更改可能会破坏依赖它们的铭文,递归端点具有向后兼容性保证," +"这是`ord server`的其他端点所不具备的。具体来说:" -#: src/inscriptions/recursion.md:11 -msgid "Remixing the content of existing inscriptions." -msgstr "重新混合现有铭文的内容。" +#: src/inscriptions/recursion.md:12 +msgid "Recursive endpoints will not be removed" +msgstr "递归端点将不会被移除。" #: src/inscriptions/recursion.md:13 msgid "" -"Publishing snippets of code, images, audio, or stylesheets as shared public " -"resources." -msgstr "将代码、图像、音频或样式表片段发布为公共的共享资源。" +"Object fields returned by recursive endpoints will not be renamed or change " +"types" +msgstr "" -#: src/inscriptions/recursion.md:16 +#: src/inscriptions/recursion.md:15 msgid "" -"Generative art collections where an algorithm is inscribed as JavaScript, " -"and instantiated from multiple inscriptions with unique seeds." +"However, additional object fields may be added or reordered, so inscriptions " +"must handle additional, unexpected fields, and must not expect fields to be " +"returned in a specific order." msgstr "" -"生成艺术收藏,其中算法使用JavaScript刻写,并从具有独特种子的多个铭文中实例" -"化。" +"递归端点返回的对象字段将不会被重命名或更改类型。" #: src/inscriptions/recursion.md:19 +msgid "Recursion has a number of interesting use-cases:" +msgstr "这有许多有趣的用例:" + +#: src/inscriptions/recursion.md:21 +msgid "Remixing the content of existing inscriptions." +msgstr "重新混合现有铭文的内容。" + +#: src/inscriptions/recursion.md:23 +msgid "" +"Publishing snippets of code, images, audio, or stylesheets as shared public " +"resources." +msgstr "将代码、图像、音频或样式表片段发布为公共的共享资源。" + +#: src/inscriptions/recursion.md:26 +msgid "" +"Generative art collections where an algorithm is inscribed as JavaScript, " +"and instantiated from multiple inscriptions with unique seeds." +msgstr "" +"生成艺术收藏,其中算法使用JavaScript刻写,并从具有独特种子的多个铭文中实例" +"化。" + +#: src/inscriptions/recursion.md:29 msgid "" "Generative profile picture collections where accessories and attributes are " "inscribed as individual images, or in a shared texture atlas, and then " @@ -1577,125 +1861,355 @@ msgstr "" "生成个人资料图片集,其中包含配件和属性刻录为单独的图像,或刻录在共享纹理图集" "中,然后组合,拼贴风格,在多个铭文中以独特的组合。" -#: src/inscriptions/recursion.md:23 -msgid "A few other endpoints that inscriptions may access are the following:" -msgstr "铭文可以访问的其他几个端点如下:" +#: src/inscriptions/recursion.md:33 +msgid "The recursive endpoints are:" +msgstr "递归端点是" + +#: src/inscriptions/recursion.md:35 +msgid "" +"`/content/`: the content of the inscription with " +"``" +msgstr "" +"`/content/`: 铭文的内容 " +"``" + +#: src/inscriptions/recursion.md:36 +msgid "`/r/blockhash/`: block hash at given block height." +msgstr "`/blockhash/`:给定块高度的块哈希。" + +#: src/inscriptions/recursion.md:37 +msgid "`/r/blockhash`: latest block hash." +msgstr "`/blockhash`:最新的块哈希。" + +#: src/inscriptions/recursion.md:38 +msgid "`/r/blockheight`: latest block height." +msgstr "`/blockheight`:最新区块高度。" + +#: src/inscriptions/recursion.md:39 +msgid "" +"`/r/blockinfo/`: block info. `` may be a block height or block " +"hash." +msgstr "" +"`/r/blockinfo/`: 区块信息. `` 可能是区块高度或者区块哈希" + +#: src/inscriptions/recursion.md:40 +msgid "`/r/blocktime`: UNIX time stamp of latest block." +msgstr "`/blocktime`:最新块的 UNIX 时间戳。" + +#: src/inscriptions/recursion.md:41 +msgid "`/r/children/`: the first 100 child inscription ids." +msgstr "`/r/children/`: 前100个子铭文的ID." + +#: src/inscriptions/recursion.md:42 +msgid "" +"`/r/children//`: the set of 100 child inscription ids " +"on ``." +msgstr "" +"`/r/children//`: 100个子铭文ID的合集 " +"on ``." + +#: src/inscriptions/recursion.md:43 +msgid "`/r/inscription/:inscription_id`: information about an inscription" +msgstr "`/r/inscription/:inscription_id`: 关于一个铭文的信息" + +#: src/inscriptions/recursion.md:44 +msgid "" +"`/r/metadata/`: JSON string containing the hex-encoded CBOR " +"metadata." +msgstr "" +"`/r/metadata/`: 包含十六进制编码的 CBOR 元数据 的 JSON 字符串" + +#: src/inscriptions/recursion.md:45 +msgid "`/r/sat/`: the first 100 inscription ids on a sat." +msgstr "`/r/sat/`: 在一个Sats上的头100个铭文ID. + +#: src/inscriptions/recursion.md:46 +msgid "" +"`/r/sat//`: the set of 100 inscription ids on ``." +msgstr "" +"`/r/sat//`: 在 ``上的100个铭文ID合集." + +#: src/inscriptions/recursion.md:47 +msgid "" +"`/r/sat//at/`: the inscription id at `` of all " +"inscriptions on a sat. `` may be a negative number to index from the " +"back. `0` being the first and `-1` being the most recent for example." +msgstr "" +"`/r/sat//at/`: 所有`` 处在一个聪上的铭文ID " +"``可能是从索引往后的负数" +"比如`0` 是第一个而 `-1` 是最近的." + +#: src/inscriptions/recursion.md:49 +msgid "" +"Note: `` only allows the actual number of a sat no other sat " +"notations like degree, percentile or decimal. We may expand to allow those " +"in the future." +msgstr "" +"注意: `` 仅允许使用sat的实际数字,不允许使用度数、" +"百分位数或小数等其他sat表示法。我们可能会在将来考虑支持这些表示法。" + +#: src/inscriptions/recursion.md:53 +msgid "" +"Responses from the above recursive endpoints are JSON. For backwards " +"compatibility additional endpoints are supported, some of which return plain-" +"text responses." +msgstr "" +"来自上述递归端点的响应是 JSON。为了向后兼容,支持其他端点,其中一些返回纯文本响应。 " -#: src/inscriptions/recursion.md:25 +#: src/inscriptions/recursion.md:57 msgid "`/blockheight`: latest block height." msgstr "`/blockheight`:最新区块高度。" -#: src/inscriptions/recursion.md:26 +#: src/inscriptions/recursion.md:58 msgid "`/blockhash`: latest block hash." msgstr "`/blockhash`:最新的块哈希。" -#: src/inscriptions/recursion.md:27 +#: src/inscriptions/recursion.md:59 msgid "`/blockhash/`: block hash at given block height." msgstr "`/blockhash/`:给定块高度的块哈希。" -#: src/inscriptions/recursion.md:28 +#: src/inscriptions/recursion.md:60 msgid "`/blocktime`: UNIX time stamp of latest block." msgstr "`/blocktime`:最新块的 UNIX 时间戳。" -#: src/inscriptions/pointer.md:4 +#: src/inscriptions/recursion.md:65 +msgid "`/r/blockhash/0`:" +msgstr "" + +#: src/inscriptions/recursion.md:67 msgid "" -"In order to make an inscription on a sat other than the first of its input, " -"a zero-based integer, called the \"pointer\", can be provided with tag `2`, " -"causing the inscription to be made on the sat at the given position in the " -"outputs. If the pointer is equal to or greater than the number of total sats " -"in the outputs of the inscribe transaction, it is ignored, and the " -"inscription is made as usual. The value of the pointer field is a little " -"endian integer, with trailing zeroes ignored." +"```json\n" +"\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"\n" +"```" msgstr "" -"为了在输入的第一个以外的sat上进行铭刻," -"可以提供一个以0为基础的整数,称作 \"指针\",并配以标签 `2`, " -"这将导致铭文被做在给定位置的输出的sat上。 " -"如果指针等于或大于铭文交易输出中的总sat数,那么它将被忽略, " -"而铭文将像往常一样被铭刻。指针字段的值是一个小端整数,尾随零将被忽略。 " -#: src/inscriptions/pointer.md:12 +#: src/inscriptions/recursion.md:71 +msgid "`/r/blockheight`:" +msgstr "" + +#: src/inscriptions/recursion.md:73 msgid "" -"An even tag is used, so that old versions of `ord` consider the inscription " -"to be unbound, instead of assigning it, incorrectly, to the first sat." +"```json\n" +"777000\n" +"```" msgstr "" -"使用了偶数标签,所以旧版本的 `ord` 会把铭文视为无约束,而不是错误地将其分配到第一个sat。" -#: src/inscriptions/pointer.md:15 +#: src/inscriptions/recursion.md:77 +msgid "`/r/blockinfo/0`:" +msgstr "" + +#: src/inscriptions/recursion.md:79 msgid "" -"This can be used to create multiple inscriptions in a single transaction on " -"different sats, when otherwise they would be made on the same sat." +"```json\n" +"{\n" +" \"average_fee\": 0,\n" +" \"average_fee_rate\": 0,\n" +" \"bits\": 486604799,\n" +" \"chainwork\": " +"\"0000000000000000000000000000000000000000000000000000000100010001\",\n" +" \"confirmations\": 0,\n" +" \"difficulty\": 0.0,\n" +" \"hash\": " +"\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\",\n" +" \"height\": 0,\n" +" \"max_fee\": 0,\n" +" \"max_fee_rate\": 0,\n" +" \"max_tx_size\": 0,\n" +" \"median_fee\": 0,\n" +" \"median_time\": 1231006505,\n" +" \"merkle_root\": " +"\"0000000000000000000000000000000000000000000000000000000000000000\",\n" +" \"min_fee\": 0,\n" +" \"min_fee_rate\": 0,\n" +" \"next_block\": null,\n" +" \"nonce\": 0,\n" +" \"previous_block\": null,\n" +" \"subsidy\": 5000000000,\n" +" \"target\": " +"\"00000000ffff0000000000000000000000000000000000000000000000000000\",\n" +" \"timestamp\": 1231006505,\n" +" \"total_fee\": 0,\n" +" \"total_size\": 0,\n" +" \"total_weight\": 0,\n" +" \"transaction_count\": 1,\n" +" \"version\": 1\n" +"}\n" +"```" msgstr "" -"这可以用于一次性在不同的sat上创建多个铭文,否则它们将被制成在同一个sat上" -#: src/inscriptions/pointer.md:18 -#, fuzzy -msgid "Examples" -msgstr "示例" +#: src/inscriptions/recursion.md:111 +msgid "`/r/blocktime`:" +msgstr "" -#: src/inscriptions/pointer.md:21 -#, fuzzy -msgid "An inscription with pointer 255:" -msgstr "一个带有255指针的铭文" +#: src/inscriptions/recursion.md:113 +msgid "" +"```json\n" +"1700770905\n" +"```" +msgstr "" -#: src/inscriptions/pointer.md:23 +#: src/inscriptions/recursion.md:117 src/inscriptions/recursion.md:178 msgid "" -"```\n" -"OP_FALSE\n" -"OP_IF\n" -" OP_PUSH \"ord\"\n" -" OP_PUSH 1\n" -" OP_PUSH \"text/plain;charset=utf-8\"\n" -" OP_PUSH 2\n" -" OP_PUSH 0xff\n" -" OP_PUSH 0\n" -" OP_PUSH \"Hello, world!\"\n" -"OP_ENDIF\n" +"`/r/" +"children/60bcf821240064a9c55225c4f01711b0ebbcab39aa3fafeefe4299ab158536fai0/49`:" +msgstr "" + +#: src/inscriptions/recursion.md:119 src/inscriptions/recursion.md:180 +msgid "" +"```json\n" +"{\n" +" \"ids\":[\n" +" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4900\",\n" +" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4901\",\n" +" ...\n" +" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4935\",\n" +" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4936\"\n" +" ],\n" +" \"more\":false,\n" +" \"page\":49\n" +"}\n" "```" msgstr "" -#: src/inscriptions/pointer.md:36 -#, fuzzy -msgid "An inscription with pointer 256:" -msgstr "一个带有256指针的铭文" +#: src/inscriptions/recursion.md:133 +msgid "" +"`r/" +"inscription/3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0`" +msgstr "" -#: src/inscriptions/pointer.md:38 +#: src/inscriptions/recursion.md:135 msgid "" -"```\n" -"OP_FALSE\n" -"OP_IF\n" -" OP_PUSH \"ord\"\n" -" OP_PUSH 1\n" -" OP_PUSH \"text/plain;charset=utf-8\"\n" -" OP_PUSH 2\n" -" OP_PUSH 0x0001\n" -" OP_PUSH 0\n" -" OP_PUSH \"Hello, world!\"\n" -"OP_ENDIF\n" +"```json\n" +"{\n" +" \"charms\": [],\n" +" \"content_type\": \"image/png\",\n" +" \"content_length\": 144037,\n" +" \"fee\": 36352,\n" +" \"height\": 209,\n" +" \"id\": " +"\"3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0\",\n" +" \"number\": 2,\n" +" \"output\": " +"\"3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36:0\",\n" +" \"sat\": null,\n" +" \"satpoint\": " +"\"3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36:0:0\",\n" +" \"timestamp\": 1708312562,\n" +" \"value\": 10000\n" +"}\n" "```" msgstr "" -#: src/inscriptions/pointer.md:51 +#: src/inscriptions/recursion.md:152 msgid "" -"An inscription with pointer 256, with trailing zeroes, which are ignored:" +"`/r/" +"metadata/35b66389b44535861c44b2b18ed602997ee11db9a30d384ae89630c9fc6f011fi3`:" msgstr "" -"带有指针256的铭文,尾随零被忽略:" -#: src/inscriptions/pointer.md:53 +#: src/inscriptions/recursion.md:154 msgid "" -"```\n" -"OP_FALSE\n" -"OP_IF\n" -" OP_PUSH \"ord\"\n" -" OP_PUSH 1\n" -" OP_PUSH \"text/plain;charset=utf-8\"\n" -" OP_PUSH 2\n" -" OP_PUSH 0x000100\n" -" OP_PUSH 0\n" -" OP_PUSH \"Hello, world!\"\n" -"OP_ENDIF\n" +"```json\n" +"\"a2657469746c65664d656d6f727966617574686f726e79656c6c6f775f6f72645f626f74\"\n" +"```" +msgstr "" + +#: src/inscriptions/recursion.md:158 +msgid "`/r/sat/1023795949035695`:" +msgstr "" + +#: src/inscriptions/recursion.md:160 +msgid "" +"```json\n" +"{\n" +" \"ids\":[\n" +" \"17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0\"\n" +" ],\n" +" \"more\":false,\n" +" \"page\":0\n" +"}\n" +"```" +msgstr "" + +#: src/inscriptions/recursion.md:170 +msgid "`/r/sat/1023795949035695/at/-1`:" +msgstr "" + +#: src/inscriptions/recursion.md:172 +msgid "" +"```json\n" +"{\n" +" \"id\":" +"\"17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0\"\n" +"}\n" "```" msgstr "" +#: src/inscriptions/rendering.md:4 +msgid "Aspect Ratio" +msgstr "纵横比" + +#: src/inscriptions/rendering.md:7 +msgid "" +"Inscriptions should be rendered with a square aspect ratio. Non-square " +"aspect ratio inscriptions should not be cropped, and should instead be " +"centered and resized to fit within their container." +msgstr "" +"铭文应以正方形的纵横比进行渲染。非正方形纵横比的铭文不应被裁剪,而应该居中并调整大小以适应其容器。" + +#: src/inscriptions/rendering.md:11 +msgid "Maximum Size" +msgstr "最大尺寸" + +#: src/inscriptions/rendering.md:14 +msgid "" +"The `ord` explorer, used by [ordinals.com](https://ordinals.com/), displays " +"inscription previews with a maximum size of 576 by 576 pixels, making it a " +"reasonable choice when choosing a maximum display size." +msgstr "" +"由[ordinals.com](https://ordinals.com/)使用的`ord` 浏览器," +"展示的铭文预览的最大尺寸为576乘以576像素,这使得它在选择最大显示尺寸时是一个合理的选择。" + +#: src/inscriptions/rendering.md:18 +msgid "Image Rendering" +msgstr "图片渲染" + +#: src/inscriptions/rendering.md:21 +msgid "" +"The CSS `image-rendering` property controls how images are resampled when " +"upscaled and downscaled." +msgstr "" +"CSS中的`image-rendering` 属性控制了在图片放大和缩小时如何重新采样图片。" + +#: src/inscriptions/rendering.md:24 +msgid "" +"When downscaling image inscriptions, `image-rendering: auto`, should be " +"used. This is desirable even when downscaling pixel art." +msgstr "" +"在缩小图片铭文时,应使用`image-rendering: auto`,即使在缩小像素艺术图片时,这也是可取的。" + +#: src/inscriptions/rendering.md:27 +msgid "" +"When upscaling image inscriptions other than AVIF, `image-rendering: " +"pixelated` should be used. This is desirable when upscaling pixel art, since " +"it preserves the sharp edges of pixels. It is undesirable when upscaling non-" +"pixel art, but should still be used for visual compatibility with the `ord` " +"explorer." +msgstr "" +"在放大非AVIF格式的图片铭文时,应使用`image-rendering: pixelated`。" +"这在放大像素艺术图片时是可取的,因为它保留了像素的锐利边缘。" +"虽然在放大非像素艺术图片时这可能不太理想,但为了与ord浏览器的视觉兼容性,仍应使用此设置。" + +#: src/inscriptions/rendering.md:32 +msgid "" +"When upscaling AVIF and JPEG XL inscriptions, `image-rendering: auto` should " +"be used. This allows inscribers to opt-in to non-pixelated upscaling for non-" +"pixel art inscriptions. Until such time as JPEG XL is widely supported by " +"browsers, it is not a recommended image format." +msgstr "" +"在放大AVIF和JPEG XL格式的图片铭文时,应使用`image-rendering: auto`。" +"这允许铭文者选择非像素化的放大方式,适用于非像素艺术的铭文。" +"直到JPEG XL格式被浏览器广泛支持之前,它并不是一个推荐的图片格式。" + #: src/faq.md:1 msgid "Ordinal Theory FAQ" msgstr "序数理论常见问题" @@ -2712,134 +3226,129 @@ msgstr "序数浏览器" #: src/guides/explorer.md:4 msgid "" -"The `ord` binary includes a block explorer. We host a instance of the block " -"explorer on mainnet at [ordinals.com](https://ordinals.com), and on signet " -"at [signet.ordinals.com](https://signet.ordinals.com)." +"The `ord` binary includes a block explorer. We host an instance of the block " +"explorer on mainnet at [ordinals.com](https://ordinals.com), on signet at " +"[signet.ordinals.com](https://signet.ordinals.com), and on testnet at " +"[testnet.ordinals.com](https://testnet.ordinals.com). As of version 0.16.0 " +"the wallet needs `ord server` running in the background. This is analogous " +"to how `bitcoin-cli` needs `bitcoind` running in the background." msgstr "" "`ord` 文件包含一个区块浏览器。我们的主网区块链器部署在 [ordinals.com]" "(https://ordinals.com), signet部署在[signet.ordinals.com](https://signet." "ordinals.com)." -#: src/guides/explorer.md:8 +#: src/guides/explorer.md:11 msgid "Running The Explorer" msgstr "运行浏览器" -#: src/guides/explorer.md:9 +#: src/guides/explorer.md:12 msgid "The server can be run locally with:" msgstr "服务器可以使用本地运行:" -#: src/guides/explorer.md:11 +#: src/guides/explorer.md:14 msgid "`ord server`" msgstr "" -#: src/guides/explorer.md:13 +#: src/guides/explorer.md:16 msgid "To specify a port add the `--http-port` flag:" msgstr "指定端口使用`--http-port`标记" -#: src/guides/explorer.md:15 +#: src/guides/explorer.md:18 msgid "`ord server --http-port 8080`" msgstr "" -#: src/guides/explorer.md:17 -msgid "" -"To enable the JSON-API endpoints add the `--enable-json-api` or `-j` flag " -"(see [here](#json-api) for more info):" -msgstr "" -"要启动JSON-API 端点 添加 `--enable-json-api` 或者 `-j` 标志 " -"(更多信息参考 [这里](#json-api) :" - #: src/guides/explorer.md:20 -msgid "`ord --enable-json-api server`" +msgid "" +"The JSON-API endpoints are enabled by default, to disable them add the `--" +"disable-json-api` flag (see [here](#json-api) for more info):" msgstr "" +"要启动JSON-API 端点 添加 `--enable-json-api` 或者 `-j` 标志 (更多信息参考 [这" +"里](#json-api) :" -#: src/guides/explorer.md:22 -msgid "To test how your inscriptions will look you can run:" -msgstr "测试你的铭文你可以运行:" - -#: src/guides/explorer.md:24 -msgid "`ord preview ...`" +#: src/guides/explorer.md:23 +msgid "`ord server --disable-json-api`" msgstr "" -#: src/guides/explorer.md:26 +#: src/guides/explorer.md:25 msgid "Search" msgstr "搜索" -#: src/guides/explorer.md:29 +#: src/guides/explorer.md:28 msgid "The search box accepts a variety of object representations." msgstr "搜索框可以使用各种对象:" -#: src/guides/explorer.md:31 +#: src/guides/explorer.md:30 msgid "Blocks" msgstr "区块" -#: src/guides/explorer.md:33 +#: src/guides/explorer.md:32 msgid "Blocks can be searched by hash, for example, the genesis block:" msgstr "区块可以通过哈希来查找,例如创世区块:" -#: src/guides/explorer.md:35 +#: src/guides/explorer.md:34 msgid "" "[000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f](https://" "ordinals.com/" "search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f)" msgstr "" -#: src/guides/explorer.md:37 +#: src/guides/explorer.md:36 msgid "Transactions" msgstr "交易" -#: src/guides/explorer.md:39 +#: src/guides/explorer.md:38 msgid "" "Transactions can be searched by hash, for example, the genesis block " "coinbase transaction:" msgstr "可以通过哈希查找交易,例如创世区块的coinbase交易:" -#: src/guides/explorer.md:42 +#: src/guides/explorer.md:41 msgid "" "[4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b](https://" "ordinals.com/" "search/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b)" msgstr "" -#: src/guides/explorer.md:44 +#: src/guides/explorer.md:43 msgid "Outputs" msgstr "输出" -#: src/guides/explorer.md:46 +#: src/guides/explorer.md:45 msgid "" -"Transaction outputs can searched by outpoint, for example, the only output " -"of the genesis block coinbase transaction:" +"Transaction outputs can be searched by outpoint, for example, the only " +"output of the genesis block coinbase transaction:" msgstr "可以通过outpoint搜索交易输出,例如创世块coinbase交易的唯一输出:" -#: src/guides/explorer.md:49 +#: src/guides/explorer.md:48 msgid "" "[4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0](https://" "ordinals.com/" "search/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0)" msgstr "" -#: src/guides/explorer.md:51 +#: src/guides/explorer.md:50 msgid "Sats" msgstr "聪" -#: src/guides/explorer.md:53 +#: src/guides/explorer.md:52 msgid "" "Sats can be searched by integer, their position within the entire bitcoin " "supply:" msgstr "聪 可以按整数搜索,它们在整个比特币供应中的位置:" -#: src/guides/explorer.md:56 +#: src/guides/explorer.md:55 msgid "[2099994106992659](https://ordinals.com/search/2099994106992659)" msgstr "" -#: src/guides/explorer.md:58 +#: src/guides/explorer.md:57 msgid "By decimal, their block and offset within that block:" msgstr "按十进制,它们的块和该块内的偏移量:" -#: src/guides/explorer.md:60 +#: src/guides/explorer.md:59 msgid "[481824.0](https://ordinals.com/search/481824.0)" msgstr "" -#: src/guides/explorer.md:62 +#: src/guides/explorer.md:61 msgid "" "By degree, their cycle, blocks since the last halving, blocks since the last " "difficulty adjustment, and offset within their block:" @@ -2847,96 +3356,93 @@ msgstr "" "按度数,他们的周期,自上次减半以来的区块,自上次难度调整以来的区块,以及区块" "内的偏移量:" -#: src/guides/explorer.md:65 +#: src/guides/explorer.md:64 msgid "[1°0′0″0‴](https://ordinals.com/search/1°0′0″0‴)" msgstr "" -#: src/guides/explorer.md:67 +#: src/guides/explorer.md:66 msgid "" "By name, their base 26 representation using the letters \"a\" through \"z\":" msgstr "按照名称,它们使用字母\"a\"到\"z\"的 26个字母组合表示:" -#: src/guides/explorer.md:69 +#: src/guides/explorer.md:68 msgid "[ahistorical](https://ordinals.com/search/ahistorical)" msgstr "" -#: src/guides/explorer.md:71 +#: src/guides/explorer.md:70 msgid "" "Or by percentile, the percentage of bitcoin's supply that has been or will " "have been issued when they are mined:" msgstr "或者按百分位数,在开采时已经或将要发行的比特币供应量的百分比:" -#: src/guides/explorer.md:74 +#: src/guides/explorer.md:73 msgid "[100%](https://ordinals.com/search/100%)" msgstr "" -#: src/guides/explorer.md:76 +#: src/guides/explorer.md:75 msgid "JSON-API" msgstr "" -#: src/guides/explorer.md:79 +#: src/guides/explorer.md:78 msgid "" -"You can run `ord` with the `--enable-json-api` flag to access endpoints that " -"return JSON instead of HTML if you set the HTTP `Accept: application/json` " -"header. The structure of theses objects closely follows what is shown in the " -"HTML. These endpoints are:" +"By default the `ord server` gives access to endpoints that return JSON " +"instead of HTML if you set the HTTP `Accept: application/json` header. The " +"structure of these objects closely follows what is shown in the HTML. These " +"endpoints are:" msgstr "" -"你可以运行 `ord` 和 `--enable-json-api` 标签访问返回JSON而非HTML的端点," -"只需要设置HTTP的header `Accept: application/json` " -"这些对象的结构紧贴HTML所展示的内容。这些端点包括: " +"你可以运行 `ord` 和 `--enable-json-api` 标签访问返回JSON而非HTML的端点,只需" +"要设置HTTP的header `Accept: application/json` 这些对象的结构紧贴HTML所展示的" +"内容。这些端点包括: " -#: src/guides/explorer.md:84 +#: src/guides/explorer.md:83 msgid "`/inscription/`" msgstr "" -#: src/guides/explorer.md:85 -#, fuzzy +#: src/guides/explorer.md:84 msgid "`/inscriptions`" msgstr "" -#: src/guides/explorer.md:86 +#: src/guides/explorer.md:85 msgid "`/inscriptions/block/`" msgstr "" -#: src/guides/explorer.md:87 +#: src/guides/explorer.md:86 msgid "`/inscriptions/block//`" msgstr "" -#: src/guides/explorer.md:88 -#, fuzzy +#: src/guides/explorer.md:87 msgid "`/inscriptions/`" msgstr "" -#: src/guides/explorer.md:89 +#: src/guides/explorer.md:88 msgid "`/inscriptions//`" msgstr "" -#: src/guides/explorer.md:90 src/guides/explorer.md:91 +#: src/guides/explorer.md:89 src/guides/explorer.md:90 msgid "`/output/`" msgstr "" -#: src/guides/explorer.md:92 +#: src/guides/explorer.md:91 msgid "`/sat/`" msgstr "" -#: src/guides/explorer.md:94 +#: src/guides/explorer.md:93 msgid "To get a list of the latest 100 inscriptions you would do:" msgstr "你可以运行以下命令来得到最近的100个铭文的清单" -#: src/guides/explorer.md:96 +#: src/guides/explorer.md:95 msgid "" "```\n" "curl -s -H \"Accept: application/json\" 'http://0.0.0.0:80/inscriptions'\n" "```" msgstr "" -#: src/guides/explorer.md:100 +#: src/guides/explorer.md:99 msgid "" "To see information about a UTXO, which includes inscriptions inside it, do:" -msgstr "" -"要看到一个UTXO包含的铭文信息,运行:" +msgstr "要看到一个UTXO包含的铭文信息,运行:" -#: src/guides/explorer.md:102 +#: src/guides/explorer.md:101 msgid "" "```\n" "curl -s -H \"Accept: application/json\" 'http://0.0.0.0:80/output/" @@ -2944,11 +3450,11 @@ msgid "" "```" msgstr "" -#: src/guides/explorer.md:106 +#: src/guides/explorer.md:105 msgid "Which returns:" msgstr "返回" -#: src/guides/explorer.md:108 +#: src/guides/explorer.md:107 msgid "" "```\n" "{\n" @@ -2967,11 +3473,7 @@ msgid "" "```" msgstr "" -#: src/guides/inscriptions.md:1 -msgid "Ordinal Inscription Guide" -msgstr "铭文指引" - -#: src/guides/inscriptions.md:4 +#: src/guides/wallet.md:4 msgid "" "Individual sats can be inscribed with arbitrary content, creating Bitcoin-" "native digital artifacts that can be held in a Bitcoin wallet and " @@ -2981,7 +3483,7 @@ msgstr "" "单个 聪 可以刻有任意内容,创建可以保存在比特币钱包中并使用比特币交易传输的比" "特币原生数字人工制品。铭文与比特币本身一样持久、不变、安全和去中心化。" -#: src/guides/inscriptions.md:9 +#: src/guides/wallet.md:9 msgid "" "Working with inscriptions requires a Bitcoin full node, to give you a view " "of the current state of the Bitcoin blockchain, and a wallet that can create " @@ -2991,7 +3493,7 @@ msgstr "" "使用铭文需要一个比特币完整节点,让您了解比特币区块链的当前状态,以及一个可以" "创建铭文并在构建交易以将铭文发送到另一个钱包时执行 聪 控制的钱包。" -#: src/guides/inscriptions.md:14 +#: src/guides/wallet.md:14 msgid "" "Bitcoin Core provides both a Bitcoin full node and wallet. However, the " "Bitcoin Core wallet cannot create inscriptions and does not perform sat " @@ -3000,7 +3502,7 @@ msgstr "" "Bitcoin Core 提供比特币全节点和钱包。 但是,Bitcoin Core 钱包不能创建铭文,不" "执行 聪 控制。" -#: src/guides/inscriptions.md:17 +#: src/guides/wallet.md:17 msgid "" "This requires [`ord`](https://github.com/ordinals/ord), the ordinal utility. " "`ord` doesn't implement its own wallet, so `ord wallet` subcommands interact " @@ -3009,48 +3511,47 @@ msgstr "" "这需要[`ord`](https://github.com/ordinals/ord),序数实用程序。 `ord` 没有自己" "的钱包,因此 `ord wallet`子命令与 Bitcoin Core 钱包交互。" -#: src/guides/inscriptions.md:21 +#: src/guides/wallet.md:21 msgid "This guide covers:" msgstr "本指南涵盖:" -#: src/guides/inscriptions.md:23 src/guides/inscriptions.md:40 +#: src/guides/wallet.md:23 src/guides/wallet.md:40 msgid "Installing Bitcoin Core" msgstr "安装 Bitcoin Core" -#: src/guides/inscriptions.md:24 +#: src/guides/wallet.md:24 msgid "Syncing the Bitcoin blockchain" msgstr "同步比特币区块链" -#: src/guides/inscriptions.md:25 +#: src/guides/wallet.md:25 msgid "Creating a Bitcoin Core wallet" msgstr "创建 Bitcoin Core 钱包" -#: src/guides/inscriptions.md:26 +#: src/guides/wallet.md:26 msgid "Using `ord wallet receive` to receive sats" msgstr "使用 `ord wallet receive`收取聪" -#: src/guides/inscriptions.md:27 +#: src/guides/wallet.md:27 msgid "Creating inscriptions with `ord wallet inscribe`" msgstr "使用`ord wallet inscribe`创建铭文" -#: src/guides/inscriptions.md:28 +#: src/guides/wallet.md:28 msgid "Sending inscriptions with `ord wallet send`" msgstr "使用 `ord wallet send`发送铭文" -#: src/guides/inscriptions.md:29 +#: src/guides/wallet.md:29 msgid "Receiving inscriptions with `ord wallet receive`" msgstr "使用`ord wallet receive`收取铭文" -#: src/guides/inscriptions.md:30 -#, fuzzy +#: src/guides/wallet.md:30 msgid "Batch inscribing with `ord wallet inscribe --batch`" msgstr "使用`ord wallet inscribe`创建铭文" -#: src/guides/inscriptions.md:32 +#: src/guides/wallet.md:32 msgid "Getting Help" msgstr "寻求帮助" -#: src/guides/inscriptions.md:35 +#: src/guides/wallet.md:35 msgid "" "If you get stuck, try asking for help on the [Ordinals Discord Server]" "(https://discord.com/invite/87cjuz4FYg), or checking GitHub for relevant " @@ -3061,7 +3562,7 @@ msgstr "" "invite/87cjuz4FYg),或者检查Github上的相关内容[问题](https://github.com/" "ordinals/ord/issues) 和[讨论](https://github.com/ordinals/ord/discussions)." -#: src/guides/inscriptions.md:43 +#: src/guides/wallet.md:43 msgid "" "Bitcoin Core is available from [bitcoincore.org](https://bitcoincore.org/) " "on the [download page](https://bitcoincore.org/en/download/)." @@ -3069,12 +3570,11 @@ msgstr "" "Bitcoin Core 可以在 [bitcoincore.org](https://bitcoincore.org/) 上的[下载页" "面](https://bitcoincore.org/en/download/)." -#: src/guides/inscriptions.md:46 +#: src/guides/wallet.md:46 msgid "Making inscriptions requires Bitcoin Core 24 or newer." msgstr "制作铭文需要Bitcoin Core 24 或者更新版本。" -#: src/guides/inscriptions.md:48 -#, fuzzy +#: src/guides/wallet.md:48 msgid "" "This guide does not cover installing Bitcoin Core in detail. Once Bitcoin " "Core is installed, you should be able to run `bitcoind -version` " @@ -3083,68 +3583,67 @@ msgstr "" "本指南不包括如何详细安装 Bitcoin Core;当你成功安装Bitcoin Core以后,你应该可" "以在命令行使用 `bitcoind -version`命令。" -#: src/guides/inscriptions.md:52 +#: src/guides/wallet.md:52 msgid "Configuring Bitcoin Core" msgstr "配置 Bitcoin Core" -#: src/guides/inscriptions.md:55 -#, fuzzy +#: src/guides/wallet.md:55 msgid "`ord` requires Bitcoin Core's transaction index and rest interface." msgstr "`ord` 需要Bitcoin Core 的交易索引" -#: src/guides/inscriptions.md:57 +#: src/guides/wallet.md:57 msgid "" "To configure your Bitcoin Core node to maintain a transaction index, add the " "following to your `bitcoin.conf`:" msgstr "" "配置你的Bitcoin Core阶段去维护一个交易索引,需要在`bitcoin.conf`里面添加:" -#: src/guides/inscriptions.md:60 src/guides/sat-hunting.md:30 +#: src/guides/wallet.md:60 src/guides/sat-hunting.md:30 msgid "" "```\n" "txindex=1\n" "```" msgstr "" -#: src/guides/inscriptions.md:64 +#: src/guides/wallet.md:64 msgid "Or, run `bitcoind` with `-txindex`:" msgstr "或者, 运行 `bitcoind` 和 `-txindex`:" -#: src/guides/inscriptions.md:66 src/guides/inscriptions.md:78 +#: src/guides/wallet.md:66 src/guides/wallet.md:78 src/guides/wallet.md:169 msgid "" "```\n" "bitcoind -txindex\n" "```" msgstr "" -#: src/guides/inscriptions.md:70 +#: src/guides/wallet.md:70 msgid "" "Details on creating or modifying your `bitcoin.conf` file can be found [here]" "(https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md)." msgstr "" -"关于创建或者修改你的 `bitcoin.conf`文件,可以参考 [这里]" -"(https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md)." +"关于创建或者修改你的 `bitcoin.conf`文件,可以参考 [这里](https://github.com/" +"bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md)." -#: src/guides/inscriptions.md:73 +#: src/guides/wallet.md:73 msgid "Syncing the Bitcoin Blockchain" msgstr "比特币区块同步" -#: src/guides/inscriptions.md:76 +#: src/guides/wallet.md:76 msgid "To sync the chain, run:" msgstr "区块同步,运行:" -#: src/guides/inscriptions.md:82 +#: src/guides/wallet.md:82 msgid "…and leave it running until `getblockcount`:" msgstr "…直到运行 `getblockcount`:" -#: src/guides/inscriptions.md:84 +#: src/guides/wallet.md:84 msgid "" "```\n" "bitcoin-cli getblockcount\n" "```" msgstr "" -#: src/guides/inscriptions.md:88 +#: src/guides/wallet.md:88 msgid "" "agrees with the block count on a block explorer like [the mempool.space " "block explorer](https://mempool.space/). `ord` interacts with `bitcoind`, so " @@ -3155,7 +3654,7 @@ msgstr "" "对区块进行记述. `ord`同`bitcoind`进行交互, 所以你在使用`ord`时候需要让" "`bitcoind` 在后台运行。" -#: src/guides/inscriptions.md:92 +#: src/guides/wallet.md:92 msgid "" "The blockchain takes about 600GB of disk space. If you have an external " "drive you want to store blocks on, use the configuration option " @@ -3163,40 +3662,40 @@ msgid "" "`datadir` option because the cookie file will still be in the default " "location for `bitcoin-cli` and `ord` to find." msgstr "" -"T区块链占用约600GB的磁盘空间。如果你有一个外接硬盘来存储区块,可以使用配置选项" -"`blocksdir=`. 这比使用`datadir` 选项更简单, " -"`bitcoin-cli` 和 `ord` 可以在默认的位置找到cookie文件" +"T区块链占用约600GB的磁盘空间。如果你有一个外接硬盘来存储区块,可以使用配置选" +"项`blocksdir=`. 这比使用`datadir` 选项更简单, `bitcoin-" +"cli` 和 `ord` 可以在默认的位置找到cookie文件" -#: src/guides/inscriptions.md:98 src/guides/collecting/sparrow-wallet.md:173 +#: src/guides/wallet.md:98 src/guides/collecting/sparrow-wallet.md:173 msgid "Troubleshooting" msgstr "故障排除" -#: src/guides/inscriptions.md:101 +#: src/guides/wallet.md:101 msgid "" "Make sure you can access `bitcoind` with `bitcoin-cli -getinfo` and that it " "is fully synced." msgstr "" "确保你可以通过 `bitcoin-cli -getinfo` 来访问`bitcoind` ,并且它已经完全同步 " -#: src/guides/inscriptions.md:104 +#: src/guides/wallet.md:104 msgid "" "If `bitcoin-cli -getinfo` returns `Could not connect to the server`, " "`bitcoind` is not running." msgstr "" -"假如 `bitcoin-cli -getinfo` 返回的是 `Could not connect to the server`, " -"这可能是`bitcoind` 没有运行" +"假如 `bitcoin-cli -getinfo` 返回的是 `Could not connect to the server`, 这可" +"能是`bitcoind` 没有运行" -#: src/guides/inscriptions.md:107 +#: src/guides/wallet.md:107 msgid "" "Make sure `rpcuser`, `rpcpassword`, or `rpcauth` are _NOT_ set in your " "`bitcoin.conf` file. `ord` requires using cookie authentication. Make sure " "there is a file `.cookie` in your bitcoin data directory." msgstr "" -"确保 `rpcuser`, `rpcpassword`, 或者 `rpcauth` _没有_ 在你的 " -"`bitcoin.conf` 文件里进行设置。 `ord` 需要使用 cookie 认证。因此需要确保 " -"你的bitcoin data的文件夹里有 `.cookie`文件。" +"确保 `rpcuser`, `rpcpassword`, 或者 `rpcauth` _没有_ 在你的 `bitcoin.conf` 文" +"件里进行设置。 `ord` 需要使用 cookie 认证。因此需要确保 你的bitcoin data的文" +"件夹里有 `.cookie`文件。" -#: src/guides/inscriptions.md:111 +#: src/guides/wallet.md:111 msgid "" "If `bitcoin-cli -getinfo` returns `Could not locate RPC credentials`, then " "you must specify the cookie file location. If you are using a custom data " @@ -3205,31 +3704,32 @@ msgid "" "cookie -getinfo`. When running `ord` you must specify the cookie file " "location with `--cookie-file=/.cookie`." msgstr "" -"如果 `bitcoin-cli -getinfo` 返回`Could not locate RPC credentials`, 那么 " -"你必须指定 cookie 文件的位置。如果你正在使用自定义的数据目录 (指定 `datadir` 的选项)," -"那么你必须指定cookie文件的位置. `bitcoin-cli -rpccookiefile=/ " -"cookie -getinfo`.当你运行 `ord` 命令时,你必须指定 cookie 文件的位置 " -"`--cookie-file=/.cookie`." +"如果 `bitcoin-cli -getinfo` 返回`Could not locate RPC credentials`, 那么 你必" +"须指定 cookie 文件的位置。如果你正在使用自定义的数据目录 (指定 `datadir` 的选" +"项),那么你必须指定cookie文件的位置. `bitcoin-cli -" +"rpccookiefile=/ cookie -getinfo`.当你运行 `ord` 命令" +"时,你必须指定 cookie 文件的位置 `--cookie-file=/." +"cookie`." - -#: src/guides/inscriptions.md:119 +#: src/guides/wallet.md:119 msgid "" "Make sure you do _NOT_ have `disablewallet=1` in your `bitcoin.conf` file. " "If `bitcoin-cli listwallets` returns `Method not found` then the wallet is " "disabled and you won't be able to use `ord`." msgstr "" -"确保你在`bitcoin.conf` 文件中 _没有_ 配置 `disablewallet=1` " -"如果 `bitcoin-cli listwallets` 返回 `Method not found` 那么钱包就会被禁用" -"你将要无法使用 `ord`." +"确保你在`bitcoin.conf` 文件中 _没有_ 配置 `disablewallet=1` 如果 `bitcoin-" +"cli listwallets` 返回 `Method not found` 那么钱包就会被禁用你将要无法使用 " +"`ord`." -#: src/guides/inscriptions.md:123 +#: src/guides/wallet.md:123 msgid "" "Make sure `txindex=1` is set. Run `bitcoin-cli getindexinfo` and it should " "return something like" msgstr "" -"确保设置 `txindex=1` 。运行 `bitcoin-cli getindexinfo` 将会返回一些这样的结果 " +"确保设置 `txindex=1` 。运行 `bitcoin-cli getindexinfo` 将会返回一些这样的结" +"果 " -#: src/guides/inscriptions.md:125 +#: src/guides/wallet.md:125 msgid "" "```json\n" "{\n" @@ -3241,29 +3741,29 @@ msgid "" "```" msgstr "" -#: src/guides/inscriptions.md:133 +#: src/guides/wallet.md:133 msgid "" "If it only returns `{}`, `txindex` is not set. If it returns `\"synced\": " "false`, `bitcoind` is still creating the `txindex`. Wait until `\"synced\": " "true` before using `ord`." msgstr "" -"假如仅仅返回 `{}`, `txindex` 没有被设置。如果返回 `\"synced\": " -"false`, `bitcoind` 仍然在创建 `txindex`。那就需要等到`\"synced\": " -"true` ,`ord`命令方可以使用." +"假如仅仅返回 `{}`, `txindex` 没有被设置。如果返回 `\"synced\": false`, " +"`bitcoind` 仍然在创建 `txindex`。那就需要等到`\"synced\": true` ,`ord`命令方" +"可以使用." -#: src/guides/inscriptions.md:137 +#: src/guides/wallet.md:137 msgid "" "If you have `maxuploadtarget` set it can interfere with fetching blocks for " "`ord` index. Either remove it or set `whitebind=127.0.0.1:8333`." msgstr "" -"如果你设置了`maxuploadtarget` ,他将干扰 `ord` 的索引获取区块, " -"你可以选择移除或者设置`whitebind=127.0.0.1:8333`." +"如果你设置了`maxuploadtarget` ,他将干扰 `ord` 的索引获取区块, 你可以选择移" +"除或者设置`whitebind=127.0.0.1:8333`." -#: src/guides/inscriptions.md:140 +#: src/guides/wallet.md:140 msgid "Installing `ord`" msgstr "安装 `ord`" -#: src/guides/inscriptions.md:143 +#: src/guides/wallet.md:143 msgid "" "The `ord` utility is written in Rust and can be built from [source](https://" "github.com/ordinals/ord). Pre-built binaries are available on the [releases " @@ -3273,11 +3773,11 @@ msgstr "" "装. 预制文件可以从[版本发布页](https://github.com/ordinals/ord/releases)下" "载。" -#: src/guides/inscriptions.md:147 +#: src/guides/wallet.md:147 msgid "You can install the latest pre-built binary from the command line with:" msgstr "你也可以在命令行中使用下面命令来安装最新的文件:" -#: src/guides/inscriptions.md:149 +#: src/guides/wallet.md:149 msgid "" "```sh\n" "curl --proto '=https' --tlsv1.2 -fsLS https://ordinals.com/install.sh | bash " @@ -3285,108 +3785,296 @@ msgid "" "```" msgstr "" -#: src/guides/inscriptions.md:153 +#: src/guides/wallet.md:153 msgid "Once `ord` is installed, you should be able to run:" msgstr "当 `ord` 成功安装以后,你可以运行 :" -#: src/guides/inscriptions.md:155 +#: src/guides/wallet.md:155 msgid "" "```\n" "ord --version\n" "```" msgstr "" -#: src/guides/inscriptions.md:159 +#: src/guides/wallet.md:159 msgid "Which prints out `ord`'s version number." msgstr "这会返回 `ord`的版本信息." -#: src/guides/inscriptions.md:161 -msgid "Creating a Bitcoin Core Wallet" +#: src/guides/wallet.md:161 +msgid "Creating a Wallet" msgstr "创建一个Bitcoin Core钱包" -#: src/guides/inscriptions.md:164 +#: src/guides/wallet.md:164 +msgid "" +"`ord` uses `bitcoind` to manage private keys, sign transactions, and " +"broadcast transactions to the Bitcoin network. Additionally the `ord wallet` " +"requires [`ord server`](explorer.md) running in the background. Make sure " +"these programs are running:" +msgstr "" +"`ord` 使用Bitcoin Core来管理私钥,签署交易以及向比特币网络广播交易。" +"此外,`ord` 钱包需要在后台运行[`ord server`](explorer.md),请确保这些程序运行" + +#: src/guides/wallet.md:173 msgid "" -"`ord` uses Bitcoin Core to manage private keys, sign transactions, and " -"broadcast transactions to the Bitcoin network." -msgstr "`ord` 使用Bitcoin Core来管理私钥,签署交易以及向比特币网络广播交易。" +"```\n" +"ord server\n" +"```" +msgstr "" -#: src/guides/inscriptions.md:167 -msgid "To create a Bitcoin Core wallet named `ord` for use with `ord`, run:" +#: src/guides/wallet.md:177 +msgid "" +"To create a wallet named `ord`, the default, for use with `ord wallet`, run:" msgstr "创建一个名为`ord` 的Bitcoin Core 钱包,运行:" -#: src/guides/inscriptions.md:169 +#: src/guides/wallet.md:179 msgid "" "```\n" "ord wallet create\n" "```" msgstr "" -#: src/guides/inscriptions.md:173 -msgid "Receiving Sats" -msgstr "接收聪" +#: src/guides/wallet.md:183 +msgid "This will print out your seed phrase mnemonic, store it somewhere safe." +msgstr "这将打印出您的助记词,并将其存储在安全的地方。" -#: src/guides/inscriptions.md:176 +#: src/guides/wallet.md:185 msgid "" -"Inscriptions are made on individual sats, using normal Bitcoin transactions " -"that pay fees in sats, so your wallet will need some sats." +"```\n" +"{\n" +" \"mnemonic\": \"dignity buddy actor toast talk crisp city annual tourist " +"orient similar federal\",\n" +" \"passphrase\": \"\"\n" +"}\n" +"```" msgstr "" -"铭文是在单个聪上制作的,使用聪来支付费用的普通比特币交易,因此你的钱包将需要" -"一些 聪(比特币)。" -#: src/guides/inscriptions.md:179 -msgid "Get a new address from your `ord` wallet by running:" -msgstr "为你的 `ord` 钱包创建一个新地址,运行:" +#: src/guides/wallet.md:192 +msgid "" +"If you want to specify a different name or use an `ord server` running on a " +"non-default URL you can set these options:" +msgstr "" +"如果你想指定不同的名称或者在非默认的URL上运行 `ord server`" +"你可以设置以下选项:" -#: src/guides/inscriptions.md:181 src/guides/inscriptions.md:269 -#: src/guides/inscriptions.md:297 +#: src/guides/wallet.md:195 msgid "" "```\n" -"ord wallet receive\n" +"ord wallet --name foo --server-url http://127.0.0.1:8080 create\n" "```" msgstr "" -#: src/guides/inscriptions.md:185 -msgid "And send it some funds." -msgstr "向上面地址发送一些资金。" - -#: src/guides/inscriptions.md:187 -msgid "You can see pending transactions with:" -msgstr "你可以使用以下命令看到交易情况:" +#: src/guides/wallet.md:199 +msgid "To see all available wallet options you can run:" +msgstr "查看所有可用的钱包选项,你可以运行" -#: src/guides/inscriptions.md:189 src/guides/inscriptions.md:281 -#: src/guides/inscriptions.md:308 +#: src/guides/wallet.md:201 msgid "" "```\n" -"ord wallet transactions\n" +"ord wallet help\n" "```" msgstr "" -#: src/guides/inscriptions.md:193 -msgid "" -"Once the transaction confirms, you should be able to see the transactions " -"outputs with `ord wallet outputs`." -msgstr "一旦交易确认,你应该可以使用 `ord wallet outputs`看到交易的输出;" - -#: src/guides/inscriptions.md:196 -msgid "Creating Inscription Content" -msgstr "创建铭文内容" +#: src/guides/wallet.md:205 +msgid "Restoring and Dumping Wallet" +msgstr "恢复和转存钱包" -#: src/guides/inscriptions.md:199 +#: src/guides/wallet.md:208 msgid "" -"Sats can be inscribed with any kind of content, but the `ord` wallet only " -"supports content types that can be displayed by the `ord` block explorer." +"The `ord` wallet uses descriptors, so you can export the output descriptors " +"and import them into another descriptor-based wallet. To export the wallet " +"descriptors, which include your private keys:" msgstr "" -"聪上可以刻录任何类型的内容,但`ord`钱包只支持`ord`区块浏览器可以显示的内容类" -"型。" +"`ord`钱包使用描述符descriptors,你可以导出输出描述符并将它们导入另外一个基于描述符的钱包" +"导出钱包描述符,其中包含你的私钥:" -#: src/guides/inscriptions.md:202 +#: src/guides/wallet.md:212 msgid "" -"Additionally, inscriptions are included in transactions, so the larger the " -"content, the higher the fee that the inscription transaction must pay." +"```\n" +"$ ord wallet dump\n" +"==========================================\n" +"= THIS STRING CONTAINS YOUR PRIVATE KEYS =\n" +"= DO NOT SHARE WITH ANYONE =\n" +"==========================================\n" +"{\n" +" \"wallet_name\": \"ord\",\n" +" \"descriptors\": [\n" +" {\n" +" \"desc\": " +"\"tr([551ac972/86'/1'/0']tprv8h4xBhrfZwX9o1XtUMmz92yNiGRYjF9B1vkvQ858aN1UQcACZNqN9nFzj3vrYPa4jdPMfw4ooMuNBfR4gcYm7LmhKZNTaF4etbN29Tj7UcH/0/" +"*)#uxn94yt5\",\n" +" \"timestamp\": 1296688602,\n" +" \"active\": true,\n" +" \"internal\": false,\n" +" \"range\": [\n" +" 0,\n" +" 999\n" +" ],\n" +" \"next\": 0\n" +" },\n" +" {\n" +" \"desc\": " +"\"tr([551ac972/86'/1'/0']tprv8h4xBhrfZwX9o1XtUMmz92yNiGRYjF9B1vkvQ858aN1UQcACZNqN9nFzj3vrYPa4jdPMfw4ooMuNBfR4gcYm7LmhKZNTaF4etbN29Tj7UcH/1/" +"*)#djkyg3mv\",\n" +" \"timestamp\": 1296688602,\n" +" \"active\": true,\n" +" \"internal\": true,\n" +" \"range\": [\n" +" 0,\n" +" 999\n" +" ],\n" +" \"next\": 0\n" +" }\n" +" ]\n" +"}\n" +"```" +msgstr "" +"```\n" +"$ ord wallet dump\n" +"==========================================\n" +"= 这个字节包含你的私钥信息 =\n" +"= 不要和任何人分享 =\n" +"==========================================\n" +"{\n" +" \"wallet_name\": \"ord\",\n" +" \"descriptors\": [\n" +" {\n" +" \"desc\": " +"\"tr([551ac972/86'/1'/0']tprv8h4xBhrfZwX9o1XtUMmz92yNiGRYjF9B1vkvQ858aN1UQcACZNqN9nFzj3vrYPa4jdPMfw4ooMuNBfR4gcYm7LmhKZNTaF4etbN29Tj7UcH/0/" +"*)#uxn94yt5\",\n" +" \"timestamp\": 1296688602,\n" +" \"active\": true,\n" +" \"internal\": false,\n" +" \"range\": [\n" +" 0,\n" +" 999\n" +" ],\n" +" \"next\": 0\n" +" },\n" +" {\n" +" \"desc\": " +"\"tr([551ac972/86'/1'/0']tprv8h4xBhrfZwX9o1XtUMmz92yNiGRYjF9B1vkvQ858aN1UQcACZNqN9nFzj3vrYPa4jdPMfw4ooMuNBfR4gcYm7LmhKZNTaF4etbN29Tj7UcH/1/" +"*)#djkyg3mv\",\n" +" \"timestamp\": 1296688602,\n" +" \"active\": true,\n" +" \"internal\": true,\n" +" \"range\": [\n" +" 0,\n" +" 999\n" +" ],\n" +" \"next\": 0\n" +" }\n" +" ]\n" +"}\n" +"```" + +#: src/guides/wallet.md:247 +msgid "An `ord` wallet can be restored from a mnemonic:" +msgstr "`ord` 钱包可以从助记词恢复:" + +#: src/guides/wallet.md:249 +msgid "" +"```\n" +"ord wallet restore --from mnemonic\n" +"```" +msgstr "" + +#: src/guides/wallet.md:253 +msgid "Type your mnemonic and press return." +msgstr "输入你的助记词并按回车" + +#: src/guides/wallet.md:255 +msgid "To restore from a descriptor in `descriptor.json`:" +msgstr "从`descriptor.json`恢复描述符:" + +#: src/guides/wallet.md:257 +msgid "" +"```\n" +"cat descriptor.json | ord wallet restore --from descriptor\n" +"```" +msgstr "" + +#: src/guides/wallet.md:261 +msgid "To restore from a descriptor in the clipboard:" +msgstr "要从剪贴板中的描述符恢复:" + +#: src/guides/wallet.md:263 +msgid "" +"```\n" +"ord wallet restore --from descriptor\n" +"```" +msgstr "" + +#: src/guides/wallet.md:267 +msgid "" +"Paste the descriptor into the terminal and press CTRL-D on unix and CTRL-Z " +"on Windows." +msgstr "" +"将描述符粘贴到终端中,UNIX里按CTRL-D 或 Windows里按 CTRL-Z " + +#: src/guides/wallet.md:270 +msgid "Receiving Sats" +msgstr "接收聪" + +#: src/guides/wallet.md:273 +msgid "" +"Inscriptions are made on individual sats, using normal Bitcoin transactions " +"that pay fees in sats, so your wallet will need some sats." +msgstr "" +"铭文是在单个聪上制作的,使用聪来支付费用的普通比特币交易,因此你的钱包将需要" +"一些 聪(比特币)。" + +#: src/guides/wallet.md:276 +msgid "Get a new address from your `ord` wallet by running:" +msgstr "为你的 `ord` 钱包创建一个新地址,运行:" + +#: src/guides/wallet.md:278 src/guides/wallet.md:366 src/guides/wallet.md:394 +msgid "" +"```\n" +"ord wallet receive\n" +"```" +msgstr "" + +#: src/guides/wallet.md:282 +msgid "And send it some funds." +msgstr "向上面地址发送一些资金。" + +#: src/guides/wallet.md:284 +msgid "You can see pending transactions with:" +msgstr "你可以使用以下命令看到交易情况:" + +#: src/guides/wallet.md:286 src/guides/wallet.md:378 src/guides/wallet.md:405 +msgid "" +"```\n" +"ord wallet transactions\n" +"```" +msgstr "" + +#: src/guides/wallet.md:290 +msgid "" +"Once the transaction confirms, you should be able to see the transactions " +"outputs with `ord wallet outputs`." +msgstr "一旦交易确认,你应该可以使用 `ord wallet outputs`看到交易的输出;" + +#: src/guides/wallet.md:293 +msgid "Creating Inscription Content" +msgstr "创建铭文内容" + +#: src/guides/wallet.md:296 +msgid "" +"Sats can be inscribed with any kind of content, but the `ord` wallet only " +"supports content types that can be displayed by the `ord` block explorer." +msgstr "" +"聪上可以刻录任何类型的内容,但`ord`钱包只支持`ord`区块浏览器可以显示的内容类" +"型。" + +#: src/guides/wallet.md:299 +msgid "" +"Additionally, inscriptions are included in transactions, so the larger the " +"content, the higher the fee that the inscription transaction must pay." msgstr "" "另外,铭文是包含在交易中的,所以内容越大,铭文交易需要支付的费用就越高。" -#: src/guides/inscriptions.md:205 +#: src/guides/wallet.md:302 msgid "" "Inscription content is included in transaction witnesses, which receive the " "witness discount. To calculate the approximate fee that an inscribe " @@ -3396,7 +4084,7 @@ msgstr "" "铭文内容包含在交易见证中,获得见证折扣。要计算写入交易将支付的大概费用,请将" "内容大小除以四,然后乘以费率。" -#: src/guides/inscriptions.md:209 +#: src/guides/wallet.md:306 msgid "" "Inscription transactions must be less than 400,000 weight units, or they " "will not be relayed by Bitcoin Core. One byte of inscription content costs " @@ -3408,22 +4096,22 @@ msgstr "" "节的铭文内容需要一个权重计量单位。 由于铭文交易不只是铭文内容,铭文内容限制在" "400,000权重计量单位以内。390,000 个权重计量单位应该是安全的。" -#: src/guides/inscriptions.md:215 +#: src/guides/wallet.md:312 msgid "Creating Inscriptions" msgstr "创建铭文" -#: src/guides/inscriptions.md:218 +#: src/guides/wallet.md:315 msgid "To create an inscription with the contents of `FILE`, run:" msgstr "以`FILE`的内容创建一个铭文,需要运行:" -#: src/guides/inscriptions.md:220 +#: src/guides/wallet.md:317 msgid "" "```\n" -"ord wallet inscribe --fee-rate FEE_RATE FILE\n" +"ord wallet inscribe --fee-rate FEE_RATE --file FILE\n" "```" msgstr "" -#: src/guides/inscriptions.md:224 +#: src/guides/wallet.md:321 msgid "" "Ord will output two transactions IDs, one for the commit transaction, and " "one for the reveal transaction, and the inscription ID. Inscription IDs are " @@ -3435,7 +4123,7 @@ msgstr "" "的格式为`TXIDiN`,其中`TXID` 是揭示交易的交易 ID,`N` 是揭示交易中铭文的索" "引。" -#: src/guides/inscriptions.md:229 +#: src/guides/wallet.md:326 msgid "" "The commit transaction commits to a tapscript containing the content of the " "inscription, and the reveal transaction spends from that tapscript, " @@ -3445,7 +4133,7 @@ msgstr "" "Commit交易提交到包含铭文内容的 tapscript,reveal交易则从该 tapscript 中花费," "显示链上的内容并将它们铭刻在reveal交易的第一个输出的第一个 sat 上。" -#: src/guides/inscriptions.md:234 +#: src/guides/wallet.md:331 msgid "" "Wait for the reveal transaction to be mined. You can check the status of the " "commit and reveal transactions using [the mempool.space block explorer]" @@ -3454,108 +4142,109 @@ msgstr "" "在等待reveal交易被记录的同时,你可以使用[the mempool.space block explorer]" "(https://mempool.space/)来检查交易的状态。" -#: src/guides/inscriptions.md:238 +#: src/guides/wallet.md:335 msgid "" "Once the reveal transaction has been mined, the inscription ID should be " "printed when you run:" msgstr "一旦reveal交易完成记账,你可以使用以下命令查询铭文ID:" -#: src/guides/inscriptions.md:241 src/guides/inscriptions.md:288 -#: src/guides/inscriptions.md:314 +#: src/guides/wallet.md:338 src/guides/wallet.md:385 src/guides/wallet.md:411 msgid "" "```\n" "ord wallet inscriptions\n" "```" msgstr "" -#: src/guides/inscriptions.md:245 -#, fuzzy +#: src/guides/wallet.md:342 msgid "Parent-Child Inscriptions" -msgstr "创建铭文" +msgstr "父-子铭文" -#: src/guides/inscriptions.md:248 +#: src/guides/wallet.md:345 msgid "" "Parent-child inscriptions enable what is colloquially known as collections, " "see [provenance](../inscriptions/provenance.md) for more information." msgstr "" +"父子铭文使得人们通常所说的收藏成为可能,有关更多信息," +"请参见[provenance](../inscriptions/provenance.md)。" -#: src/guides/inscriptions.md:251 +#: src/guides/wallet.md:348 msgid "" "To make an inscription a child of another, the parent inscription has to be " "inscribed and present in the wallet. To choose a parent run `ord wallet " "inscriptions` and copy the inscription id (``)." msgstr "" +"要使一个铭文成为另一个铭文的子项,父铭文必须已经被铭刻并且存在于钱包中。" +"要选择一个父铭文,请运行`ord wallet inscriptions`并复制铭文ID(``)。" -#: src/guides/inscriptions.md:255 -#, fuzzy +#: src/guides/wallet.md:352 msgid "Now inscribe the child inscription and specify the parent like so:" msgstr "为父系铭文P创建一个子铭文C:" -#: src/guides/inscriptions.md:257 +#: src/guides/wallet.md:354 msgid "" "```\n" -"ord wallet inscribe --fee-rate FEE_RATE --parent " -"CHILD_FILE\n" +"ord wallet inscribe --fee-rate FEE_RATE --parent --" +"file CHILD_FILE\n" "```" msgstr "" -#: src/guides/inscriptions.md:261 +#: src/guides/wallet.md:358 msgid "" "This relationship cannot be added retroactively, the parent has to be " "present at inception of the child." msgstr "" +"这种父子关系不能事后添加,父铭文必须在子铭文创建之初就存在。" -#: src/guides/inscriptions.md:264 +#: src/guides/wallet.md:361 msgid "Sending Inscriptions" msgstr "发送铭文" -#: src/guides/inscriptions.md:267 +#: src/guides/wallet.md:364 msgid "Ask the recipient to generate a new address by running:" msgstr "铭文接收方使用一下命令生成地址" -#: src/guides/inscriptions.md:273 +#: src/guides/wallet.md:370 msgid "Send the inscription by running:" msgstr "使用命令格式发送铭文:" -#: src/guides/inscriptions.md:275 +#: src/guides/wallet.md:372 msgid "" "```\n" "ord wallet send --fee-rate
        \n" "```" msgstr "" -#: src/guides/inscriptions.md:279 src/guides/inscriptions.md:307 +#: src/guides/wallet.md:376 src/guides/wallet.md:404 msgid "See the pending transaction with:" msgstr "检查未完成交易情况:" -#: src/guides/inscriptions.md:285 +#: src/guides/wallet.md:382 msgid "" "Once the send transaction confirms, the recipient can confirm receipt by " "running:" msgstr "一旦交易确认,接收方可以使用一下命令查看接收到的铭文" -#: src/guides/inscriptions.md:292 +#: src/guides/wallet.md:389 msgid "Receiving Inscriptions" msgstr "接收铭文" -#: src/guides/inscriptions.md:295 +#: src/guides/wallet.md:392 msgid "Generate a new receive address using:" msgstr "使用以下命令生成一个新的接收地址" -#: src/guides/inscriptions.md:301 +#: src/guides/wallet.md:398 msgid "The sender can transfer the inscription to your address using:" msgstr "发送方使用命令发送铭文到你的地址" -#: src/guides/inscriptions.md:303 +#: src/guides/wallet.md:400 msgid "" "```\n" -"ord wallet send ADDRESS INSCRIPTION_ID\n" +"ord wallet send --fee-rate ADDRESS INSCRIPTION_ID\n" "```" msgstr "" -#: src/guides/inscriptions.md:312 -msgid "" -"Once the send transaction confirms, you can can confirm receipt by running:" +#: src/guides/wallet.md:409 +msgid "Once the send transaction confirms, you can confirm receipt by running:" msgstr "一旦交易确认,你可以使用以下命令确认收到" #: src/guides/batch-inscribing.md:4 @@ -3566,15 +4255,15 @@ msgid "" "same parent, since the parent can passed into a reveal transaction that " "creates multiple children." msgstr "" -"可以使用[指针字段](./../inscriptions/pointer.md)来批量创建多个铭文. " -"这在创建需要共享同一父系的合集或者其他情况下就特别有用,因为父犀铭文可以传递到创建多个子铭文的揭示交易中。" +"可以使用[指针字段](./../inscriptions/pointer.md)来批量创建多个铭文. 这在创建" +"需要共享同一父系的合集或者其他情况下就特别有用,因为父犀铭文可以传递到创建多" +"个子铭文的揭示交易中。" #: src/guides/batch-inscribing.md:10 msgid "" "To create a batch inscription using a batchfile in `batch.yaml`, run the " "following command:" -msgstr "" -"创建批量铭文,使用批处理文件`batch.yaml`, 运行" +msgstr "创建批量铭文,使用批处理文件`batch.yaml`, 运行" #: src/guides/batch-inscribing.md:13 msgid "" @@ -3592,1242 +4281,1691 @@ msgid "" "```yaml\n" "# example batch file\n" "\n" -"# there are two modes:\n" -"# - `separate-outputs`: place all inscriptions in separate postage-sized " -"outputs\n" -"# - `shared-output`: place inscriptions in a single output separated by " -"postage\n" +"# inscription modes:\n" +"# - `same-sat`: inscribe on the same sat\n" +"# - `satpoints`: inscribe on the first sat of specified satpoint's output\n" +"# - `separate-outputs`: inscribe on separate postage-sized outputs\n" +"# - `shared-output`: inscribe on a single output separated by postage\n" "mode: separate-outputs\n" "\n" "# parent inscription:\n" "parent: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0\n" "\n" +"# postage for each inscription:\n" +"postage: 12345\n" +"\n" +"# allow reinscribing\n" +"reinscribe: true\n" +"\n" +"# sat to inscribe on, can only be used with `same-sat`:\n" +"# sat: 5000000000\n" +"\n" "# inscriptions to inscribe\n" -"#\n" -"# each inscription has the following fields:\n" -"#\n" -"# `inscription`: path to inscription contents\n" -"# `metadata`: inscription metadata (optional)\n" -"# `metaprotocol`: inscription metaprotocol (optional)\n" "inscriptions:\n" -" - file: mango.avif\n" -" metadata:\n" -" title: Delicious Mangos\n" -" description: >\n" -" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam " +" # path to inscription content\n" +"- file: mango.avif\n" +" # inscription to delegate content to (optional)\n" +" delegate: " +"6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0\n" +" # destination (optional, if no destination is specified a new wallet " +"change address will be used)\n" +" destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n" +" # inscription metadata (optional)\n" +" metadata:\n" +" title: Delicious Mangos\n" +" description: >\n" +" Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam " "semper,\n" -" ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel " +" ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel " "blandit\n" -" metus est et odio. Nullam venenatis, urna et molestie vestibulum, " +" metus est et odio. Nullam venenatis, urna et molestie vestibulum, " "orci\n" -" mi efficitur risus, eu malesuada diam lorem sed velit. Nam " -"fermentum\n" -" dolor et luctus euismod.\n" +" mi efficitur risus, eu malesuada diam lorem sed velit. Nam fermentum\n" +" dolor et luctus euismod.\n" +" # inscription metaprotocol (optional)\n" +" metaprotocol: DOPEPROTOCOL-42069\n" "\n" -" - file: token.json\n" -" metaprotocol: brc-20\n" +"- file: token.json\n" "\n" -" - file: tulip.png\n" -" metadata:\n" -" author: Satoshi Nakamoto\n" +"- file: tulip.png\n" +" destination: " +"bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6\n" +" metadata:\n" +" author: Satoshi Nakamoto\n" "```" msgstr "" -#: src/guides/sat-hunting.md:4 +#: src/guides/collecting.md:4 msgid "" -"_This guide is out of date. Since it was written, the `ord` binary was " -"changed to only build the full satoshi index when the `--index-sats` flag is " -"supplied. Additionally, `ord` now has a built-in wallet that wraps a Bitcoin " -"Core wallet. See `ord wallet --help`._" +"Currently, [ord](https://github.com/ordinals/ord/) is the only wallet " +"supporting sat-control and sat-selection, which are required to safely store " +"and send rare sats and inscriptions, hereafter ordinals." msgstr "" -"_本指南已过时。自编写以来,“ord”安装文件已更改仅当提供“--index-sats”标志时才" -"构建完整的聪索引。此外,“ord”现在有一个内置钱包,其中包含比特币核心钱包。请参" -"阅`ord wallet --help`。_" +"目前,[ord](https://github.com/ordinals/ord/) 是唯一支持 sat-control 和 sat-" +"selection 的钱包,这是安全存储和发送稀有 sats 和铭文(以下简称序数)所必需" +"的。" -#: src/guides/sat-hunting.md:9 +#: src/guides/collecting.md:8 msgid "" -"Ordinal hunting is difficult but rewarding. The feeling of owning a wallet " -"full of UTXOs, redolent with the scent of rare and exotic sats, is beyond " -"compare." +"The recommended way to send, receive, and store ordinals is with `ord`, but " +"if you are careful, it is possible to safely store, and in some cases send, " +"ordinals with other wallets." msgstr "" +"发送、接收和存储序号的推荐方法是使用 `ord`,但如果你小心,可以安全地存储,在" +"某些情况下,使用其他钱包发送序号。" -#: src/guides/sat-hunting.md:12 +#: src/guides/collecting.md:12 msgid "" -"Ordinals are numbers for satoshis. Every satoshi has an ordinal number and " -"every ordinal number has a satoshi." -msgstr "" - -#: src/guides/sat-hunting.md:15 -msgid "Preparation" -msgstr "" - -#: src/guides/sat-hunting.md:18 -msgid "There are a few things you'll need before you start." +"As a general note, receiving ordinals in an unsupported wallet is not " +"dangerous. Ordinals can be sent to any bitcoin address, and are safe as long " +"as the UTXO that contains them is not spent. However, if that wallet is then " +"used to send bitcoin, it may select the UTXO containing the ordinal as an " +"input, and send the inscription or spend it to fees." msgstr "" +"作为一般说明,在不受支持的钱包中接收序号并不危险。 序号可以发送到任何比特币地" +"址,只要包含它们的 UTXO 没有被花费,它就是安全的。 但是,如果该钱包随后用于发" +"送比特币,它可能会选择包含序号的 UTXO 作为输入,并发送铭文或将其用于费用。" -#: src/guides/sat-hunting.md:20 +#: src/guides/collecting.md:18 msgid "" -"First, you'll need a synced Bitcoin Core node with a transaction index. To " -"turn on transaction indexing, pass `-txindex` on the command-line:" +"A [guide](./collecting/sparrow-wallet.md) to creating an `ord`\\-compatible " +"wallet with [Sparrow Wallet](https://sparrowwallet.com/), is available in " +"this handbook." msgstr "" +"本手册提供了使用[Sparrow Wallet](https://sparrowwallet.com/)创建与 `ord`兼容" +"的钱包的[指南](./collecting/sparrow-wallet.md) 。" -#: src/guides/sat-hunting.md:23 +#: src/guides/collecting.md:21 msgid "" -"```sh\n" -"bitcoind -txindex\n" -"```" +"Please note that if you follow this guide, you should not use the wallet you " +"create to send BTC, unless you perform manual coin-selection to avoid " +"sending ordinals." msgstr "" +"请注意,如果您遵循本指南,则不应使用您创建的钱包发送 BTC,除非您执行手动硬币" +"选择以避免发送序号。" -#: src/guides/sat-hunting.md:27 -msgid "" -"Or put the following in your [Bitcoin configuration file](https://github.com/" -"bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md#configuration-file-path):" -msgstr "" +#: src/guides/collecting/sparrow-wallet.md:1 +msgid "Collecting Inscriptions and Ordinals with Sparrow Wallet" +msgstr "使用麻雀Sparrow钱包收藏铭文" -#: src/guides/sat-hunting.md:34 +#: src/guides/collecting/sparrow-wallet.md:4 msgid "" -"Launch it and wait for it to catch up to the chain tip, at which point the " -"following command should print out the current block height:" +"Users who cannot or have not yet set up the [ord](https://github.com/" +"ordinals/ord) wallet can receive inscriptions and ordinals with alternative " +"bitcoin wallets, as long as they are _very_ careful about how they spend " +"from that wallet." msgstr "" +"那些无法活着尚未设置[ord](https://github.com/ordinals/ord) 钱包的用户可以使用" +"其他比特币钱包接收铭文和序数,只要他们在使用该钱包时非常小心。" -#: src/guides/sat-hunting.md:37 +#: src/guides/collecting/sparrow-wallet.md:6 msgid "" -"```sh\n" -"bitcoin-cli getblockcount\n" -"```" +"This guide gives some basic steps on how to create a wallet with [Sparrow " +"Wallet](https://sparrowwallet.com/) which is compatible with `ord` and can " +"be later imported into `ord`" msgstr "" +"本指南提供了一些基本步骤,说明如何使用 [Sparrow Wallet](https://" +"sparrowwallet.com/) 创建一个与`ord`兼容的钱包,稍后可以将其导入到`ord`" -#: src/guides/sat-hunting.md:41 -msgid "Second, you'll need a synced `ord` index." -msgstr "" +#: src/guides/collecting/sparrow-wallet.md:8 +msgid "⚠️⚠️ Warning!! ⚠️⚠️" +msgstr "⚠️⚠️ 警告!! ⚠️⚠️" -#: src/guides/sat-hunting.md:43 -msgid "Get a copy of `ord` from [the repo](https://github.com/ordinals/ord/)." +#: src/guides/collecting/sparrow-wallet.md:9 +msgid "" +"As a general rule if you take this approach, you should use this wallet with " +"the Sparrow software as a receive-only wallet." msgstr "" +"一般来说,如果你选择这种方法,你应该将这个钱包作为接收款项的钱包,使用Sparrow" +"软件。" -#: src/guides/sat-hunting.md:45 +#: src/guides/collecting/sparrow-wallet.md:11 msgid "" -"Run `RUST_LOG=info ord index`. It should connect to your bitcoin core node " -"and start indexing." +"Do not spend any satoshis from this wallet unless you are sure you know what " +"you are doing. You could very easily inadvertently lose access to your " +"ordinals and inscriptions if you don't heed this warning." msgstr "" +"除非你确定知道自己在做什么,否则不要从这个钱包中花费任何比特币。如果你不注意" +"这个警告,你可能会很容易无意间失去对序数和铭文的访问权限。" -#: src/guides/sat-hunting.md:48 -msgid "Wait for it to finish indexing." -msgstr "" +#: src/guides/collecting/sparrow-wallet.md:13 +msgid "Wallet Setup & Receiving" +msgstr "钱包设置和接收" -#: src/guides/sat-hunting.md:50 -msgid "Third, you'll need a wallet with UTXOs that you want to search." +#: src/guides/collecting/sparrow-wallet.md:15 +msgid "" +"Download the Sparrow Wallet from the [releases page](https://sparrowwallet." +"com/download/) for your particular operating system." msgstr "" +"根据你的操作系统从 [发布页面](https://sparrowwallet.com/download/) 下载" +"Sparrow钱包。" -#: src/guides/sat-hunting.md:52 -msgid "Searching for Rare Ordinals" -msgstr "" +#: src/guides/collecting/sparrow-wallet.md:17 +msgid "Select `File -> New Wallet` and create a new wallet called `ord`." +msgstr "选择 `File -> New Wallet`并创建一个名为`ord`的新钱包。" -#: src/guides/sat-hunting.md:55 -msgid "Searching for Rare Ordinals in a Bitcoin Core Wallet" +#: src/guides/collecting/sparrow-wallet.md:19 +msgid "![](images/wallet_setup_01.png)" msgstr "" -#: src/guides/sat-hunting.md:57 +#: src/guides/collecting/sparrow-wallet.md:21 msgid "" -"The `ord wallet` command is just a wrapper around Bitcoin Core's RPC API, so " -"searching for rare ordinals in a Bitcoin Core wallet is Easy. Assuming your " -"wallet is named `foo`:" +"Change the `Script Type` to `Taproot (P2TR)` and select the `New or Imported " +"Software Wallet` option." msgstr "" +"将`Script Type`更改为`Taproot (P2TR)`,然后选择`New or Imported Software " +"Wallet`选项。" -#: src/guides/sat-hunting.md:61 -msgid "Load your wallet:" +#: src/guides/collecting/sparrow-wallet.md:23 +msgid "![](images/wallet_setup_02.png)" msgstr "" -#: src/guides/sat-hunting.md:63 +#: src/guides/collecting/sparrow-wallet.md:25 msgid "" -"```sh\n" -"bitcoin-cli loadwallet foo\n" -"```" -msgstr "" +"Select `Use 12 Words` and then click `Generate New`. Leave the passphrase " +"blank." +msgstr "选择`Use 12 Words`,然后点击 `Generate New`。密码短语留空。" -#: src/guides/sat-hunting.md:67 -msgid "Display any rare ordinals wallet `foo`'s UTXOs:" +#: src/guides/collecting/sparrow-wallet.md:27 +msgid "![](images/wallet_setup_03.png)" msgstr "" -#: src/guides/sat-hunting.md:69 src/guides/sat-hunting.md:132 -#: src/guides/sat-hunting.md:233 +#: src/guides/collecting/sparrow-wallet.md:29 msgid "" -"```sh\n" -"ord wallet sats\n" -"```" +"A new 12 word BIP39 seed phrase will be generated for you. Write this down " +"somewhere safe as this is your backup to get access to your wallet. NEVER " +"share or show this seed phrase to anyone else." msgstr "" +"将为你生成一个新的12词BIP39种子短语。将此短语写在安全的地方,这是获取钱包访问" +"权限的备份。切勿与他人分享或显示这个种子短语。" -#: src/guides/sat-hunting.md:73 -msgid "Searching for Rare Ordinals in a Non-Bitcoin Core Wallet" +#: src/guides/collecting/sparrow-wallet.md:31 +msgid "Once you have written down the seed phrase click `Confirm Backup`." +msgstr "一旦你把种子短语写下来,点击 `Confirm Backup`." + +#: src/guides/collecting/sparrow-wallet.md:33 +msgid "![](images/wallet_setup_04.png)" msgstr "" -#: src/guides/sat-hunting.md:75 +#: src/guides/collecting/sparrow-wallet.md:35 msgid "" -"The `ord wallet` command is just a wrapper around Bitcoin Core's RPC API, so " -"to search for rare ordinals in a non-Bitcoin Core wallet, you'll need to " -"import your wallet's descriptors into Bitcoin Core." +"Re-enter the seed phrase which you wrote down, and then click `Create " +"Keystore`." +msgstr "重新输入你记下的种子短语,然后点击 `Create Keystore`." + +#: src/guides/collecting/sparrow-wallet.md:37 +msgid "![](images/wallet_setup_05.png)" msgstr "" -#: src/guides/sat-hunting.md:79 -msgid "" -"[Descriptors](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors." -"md) describe the ways that wallets generate private keys and public keys." +#: src/guides/collecting/sparrow-wallet.md:39 +msgid "Click `Import Keystore`." +msgstr "点击 `Import Keystore`." + +#: src/guides/collecting/sparrow-wallet.md:41 +msgid "![](images/wallet_setup_06.png)" msgstr "" -#: src/guides/sat-hunting.md:82 -msgid "" -"You should only import descriptors into Bitcoin Core for your wallet's " -"public keys, not its private keys." +#: src/guides/collecting/sparrow-wallet.md:43 +msgid "Click `Apply`. Add a password for the wallet if you want to." +msgstr "点击 `Apply`。如果你想的话,可以为钱包添加一个密码。" + +#: src/guides/collecting/sparrow-wallet.md:45 +msgid "![](images/wallet_setup_07.png)" msgstr "" -#: src/guides/sat-hunting.md:85 +#: src/guides/collecting/sparrow-wallet.md:47 msgid "" -"If your wallet's public key descriptor is compromised, an attacker will be " -"able to see your wallet's addresses, but your funds will be safe." +"You now have a wallet which is compatible with `ord`, and can be imported " +"into `ord` using the BIP39 Seed Phrase. To receive ordinals or inscriptions, " +"click on the `Receive` tab and copy a new address." msgstr "" +"你现在有了一个兼容`ord`的钱包,可以使用BIP39种子短语导入到 `ord`。要接收序数" +"或铭文,点击 `Receive`选项卡并复制一个新地址。" -#: src/guides/sat-hunting.md:88 +#: src/guides/collecting/sparrow-wallet.md:49 msgid "" -"If your wallet's private key descriptor is compromised, an attacker can " -"drain your wallet of funds." -msgstr "" +"Each time you want to receive you should use a brand-new address, and not re-" +"use existing addresses." +msgstr "每次你想接收时,都应该使用一个全新的地址,而不是重复使用现有的地址。" -#: src/guides/sat-hunting.md:91 +#: src/guides/collecting/sparrow-wallet.md:51 msgid "" -"Get the wallet descriptor from the wallet whose UTXOs you want to search for " -"rare ordinals. It will look something like this:" +"Note that bitcoin is different to some other blockchain wallets, in that " +"this wallet can generate an unlimited number of new addresses. You can " +"generate a new address by clicking on the `Get Next Address` button. You can " +"see all of your addresses in the `Addresses` tab of the app." msgstr "" +"注意,比特币与一些其他区块链钱包不同,这个钱包可以生成无限数量的新地址。你可" +"以通过点击获取下一个地址按钮生成新地址。你可以在应用程序的`Addresses`选项卡中" +"看到所有的地址。" -#: src/guides/sat-hunting.md:94 +#: src/guides/collecting/sparrow-wallet.md:53 msgid "" -"```\n" -"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" -"*)#csvefu29\n" -"```" -msgstr "" +"You can add a label to each address, so you can keep track of what it was " +"used for." +msgstr "你可以给每个地址添加一个标签,这样你就可以跟踪它的用途。" -#: src/guides/sat-hunting.md:98 -msgid "Create a watch-only wallet named `foo-watch-only`:" +#: src/guides/collecting/sparrow-wallet.md:55 +msgid "![](images/wallet_setup_08.png)" msgstr "" -#: src/guides/sat-hunting.md:100 +#: src/guides/collecting/sparrow-wallet.md:57 +msgid "Validating / Viewing Received Inscriptions" +msgstr "验证/查看收到的铭文" + +#: src/guides/collecting/sparrow-wallet.md:59 msgid "" -"```sh\n" -"bitcoin-cli createwallet foo-watch-only true true\n" -"```" +"Once you have received an inscription you will see a new transaction in the " +"`Transactions` tab of Sparrow, as well as a new UTXO in the `UTXOs` tab." msgstr "" +"一旦你收到一条铭文,你将在 Sparrow 的 `Transactions` 选项卡中看到一个新的交" +"易,以及在`UTXOs`选项卡中看到一个新的 UTXO。" -#: src/guides/sat-hunting.md:104 -msgid "Feel free to give it a better name than `foo-watch-only`!" +#: src/guides/collecting/sparrow-wallet.md:61 +msgid "" +"Initially this transaction may have an \"Unconfirmed\" status, and you will " +"need to wait for it to be mined into a bitcoin block before it is fully " +"received." msgstr "" +"最初,这笔交易可能有一个\"未确认\"的状态,你需要等待它被挖矿到一个比特币块" +"中,才算真正收到。" -#: src/guides/sat-hunting.md:106 -msgid "Load the `foo-watch-only` wallet:" +#: src/guides/collecting/sparrow-wallet.md:63 +msgid "![](images/validating_viewing_01.png)" msgstr "" -#: src/guides/sat-hunting.md:108 src/guides/sat-hunting.md:199 +#: src/guides/collecting/sparrow-wallet.md:65 msgid "" -"```sh\n" -"bitcoin-cli loadwallet foo-watch-only\n" -"```" +"To track the status of your transaction you can right-click on it, select " +"`Copy Transaction ID` and then paste that transaction id into [mempool.space]" +"(https://mempool.space)." msgstr "" +"要跟踪你的交易状态,你可以右键点击它,选择`Copy Transaction ID`,然后将该交" +"易 id 粘贴到 [mempool.space](https://mempool.space)。" -#: src/guides/sat-hunting.md:112 -msgid "Import your wallet descriptors into `foo-watch-only`:" +#: src/guides/collecting/sparrow-wallet.md:67 +msgid "![](images/validating_viewing_02.png)" msgstr "" -#: src/guides/sat-hunting.md:114 +#: src/guides/collecting/sparrow-wallet.md:69 msgid "" -"```sh\n" -"bitcoin-cli importdescriptors \\\n" -" '[{ \"desc\": " -"\"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" -"*)#tpnxnxax\", \"timestamp\":0 }]'\n" -"```" +"Once the transaction has confirmed, you can validate and view your " +"inscription by heading over to the `UTXOs` tab, finding the UTXO you want to " +"check, right-clicking on the `Output` and selecting `Copy Transaction " +"Output`. This transaction output id can then be pasted into the [ordinals." +"com](https://ordinals.com) search." msgstr "" +"一旦交易被确认,你可以通过前往`UTXOs`选项卡,找到你想要检查的 UTXO,右键点击 " +"`Output` 并选择 `Copy Transaction Output` 来验证和查看你的铭文。然后,这个交" +"易输出 id 可以粘贴到 [ordinals.com](https://ordinals.com) 搜索。" -#: src/guides/sat-hunting.md:119 +#: src/guides/collecting/sparrow-wallet.md:72 +msgid "Freezing UTXO's" +msgstr "冻结 UTXO" + +#: src/guides/collecting/sparrow-wallet.md:73 msgid "" -"If you know the Unix timestamp when your wallet first started receive " -"transactions, you may use it for the value of `\"timestamp\"` instead of " -"`0`. This will reduce the time it takes for Bitcoin Core to search for your " -"wallet's UTXOs." +"As explained above, each of your inscriptions is stored in an Unspent " +"Transaction Output (UTXO). You want to be very careful not to accidentally " +"spend your inscriptions, and one way to make it harder for this to happen is " +"to freeze the UTXO." msgstr "" +"如上所述,你的每一条铭文都存储在一个未花费的交易输出 (UTXO) 中。你需要非常小" +"心不要意外花费你的铭文,而冻结 UTXO 是使这种情况发生的难度增加的一种方式。" -#: src/guides/sat-hunting.md:124 src/guides/sat-hunting.md:225 -msgid "Check that everything worked:" +#: src/guides/collecting/sparrow-wallet.md:75 +msgid "" +"To do this, go to the `UTXOs` tab, find the UTXO you want to freeze, right-" +"click on the `Output` and select `Freeze UTXO`." msgstr "" +"要做到这一点,去 UTXOs 选项卡,找到你想要冻结的 `UTXOs`,右键点击 `Output` " +"并选择`Freeze UTXO`。" -#: src/guides/sat-hunting.md:126 src/guides/sat-hunting.md:227 +#: src/guides/collecting/sparrow-wallet.md:77 msgid "" -"```sh\n" -"bitcoin-cli getwalletinfo\n" -"```" -msgstr "" +"This UTXO (Inscription) is now un-spendable within the Sparrow Wallet until " +"you unfreeze it." +msgstr "这个 UTXO (铭文) 现在在 Sparrow 钱包中是不可消费的,直到你解冻它。" -#: src/guides/sat-hunting.md:130 src/guides/sat-hunting.md:231 -msgid "Display your wallet's rare ordinals:" -msgstr "" +#: src/guides/collecting/sparrow-wallet.md:79 +msgid "Importing into `ord` wallet" +msgstr "导入 `ord` 钱包" -#: src/guides/sat-hunting.md:136 +#: src/guides/collecting/sparrow-wallet.md:81 msgid "" -"Searching for Rare Ordinals in a Wallet that Exports Multi-path Descriptors" +"For details on setting up Bitcoin Core and the `ord` wallet check out the " +"[Wallet Guide](../wallet.md)" msgstr "" +"关于设置比特币核心和 `ord` 钱包的详细信息,请查看[钱包指南](../wallet.md)" -#: src/guides/sat-hunting.md:138 +#: src/guides/collecting/sparrow-wallet.md:83 msgid "" -"Some descriptors describe multiple paths in one descriptor using angle " -"brackets, e.g., `<0;1>`. Multi-path descriptors are not yet supported by " -"Bitcoin Core, so you'll first need to convert them into multiple " -"descriptors, and then import those multiple descriptors into Bitcoin Core." +"When setting up `ord`, instead of running `ord wallet create` to create a " +"brand-new wallet, you can import your existing wallet using `ord wallet " +"restore \"BIP39 SEED PHRASE\"` using the seed phrase you generated with " +"Sparrow Wallet." msgstr "" +"设置 `ord` 时,你可以使用 `ord wallet restore \"BIP39 SEED PHRASE\"` 命令和你" +"用Sparrow Wallet生成的种子短语,导入你现有的钱包,而不是运行 `ord wallet " +"create` 来创建一个全新的钱包。" -#: src/guides/sat-hunting.md:143 +#: src/guides/collecting/sparrow-wallet.md:85 msgid "" -"First get the multi-path descriptor from your wallet. It will look something " -"like this:" +"There is currently a [bug](https://github.com/ordinals/ord/issues/1589) " +"which causes an imported wallet to not be automatically rescanned against " +"the blockchain. To work around this you will need to manually trigger a " +"rescan using the bitcoin core cli: `bitcoin-cli -rpcwallet=ord " +"rescanblockchain 767430`" msgstr "" +"目前存在一个[程序错误](https://github.com/ordinals/ord/issues/1589) 导致导入" +"的钱包无法自动重新扫描区块链。为解决这个问题,你需要手动触发重新扫描,使用比" +"特币核心命令行界面:" -#: src/guides/sat-hunting.md:146 +#: src/guides/collecting/sparrow-wallet.md:88 msgid "" -"```\n" -"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/" -"<0;1>/*)#fw76ulgt\n" -"```" -msgstr "" - -#: src/guides/sat-hunting.md:150 -msgid "Create a descriptor for the receive address path:" -msgstr "" +"You can then check your wallet's inscriptions using `ord wallet inscriptions`" +msgstr "然后,你可以使用`ord wallet inscriptions`检查你的钱包的铭文." -#: src/guides/sat-hunting.md:152 +#: src/guides/collecting/sparrow-wallet.md:90 msgid "" -"```\n" -"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" -"*)\n" -"```" +"Note that if you have previously created a wallet with `ord`, then you will " +"already have a wallet with the default name, and will need to give your " +"imported wallet a different name. You can use the `--wallet` parameter in " +"all `ord` commands to reference a different wallet, eg:" msgstr "" +"注意,如果你之前已经用 `ord` 创建过一个钱包,那么你已经有一个默认名称的钱包," +"需要给你导入的钱包取一个不同的名称。你可以在所有的 `ord`命令中使用 `--" +"wallet` 参数来引用不同的钱包,例如:" -#: src/guides/sat-hunting.md:156 -msgid "And the change address path:" +#: src/guides/collecting/sparrow-wallet.md:92 +msgid "`ord --wallet ord_from_sparrow wallet restore \"BIP39 SEED PHRASE\"`" msgstr "" -#: src/guides/sat-hunting.md:158 -msgid "" -"```\n" -"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" -"*)\n" -"```" +#: src/guides/collecting/sparrow-wallet.md:94 +msgid "`ord --wallet ord_from_sparrow wallet inscriptions`" msgstr "" -#: src/guides/sat-hunting.md:162 -msgid "" -"Get and note the checksum for the receive address descriptor, in this case " -"`tpnxnxax`:" +#: src/guides/collecting/sparrow-wallet.md:96 +msgid "`bitcoin-cli -rpcwallet=ord_from_sparrow rescanblockchain 767430`" msgstr "" -#: src/guides/sat-hunting.md:165 -msgid "" -"```sh\n" -"bitcoin-cli getdescriptorinfo \\\n" -" 'wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" -"*)'\n" -"```" +#: src/guides/collecting/sparrow-wallet.md:98 +msgid "Sending inscriptions with Sparrow Wallet" +msgstr "使用麻雀钱包发送铭文" + +#: src/guides/collecting/sparrow-wallet.md:100 +msgid "⚠️⚠️ Warning ⚠️⚠️" +msgstr "⚠️⚠️ 警告 ⚠️⚠️" + +#: src/guides/collecting/sparrow-wallet.md:101 +msgid "" +"While it is highly recommended that you set up a bitcoin core node and run " +"the `ord` software, there are certain limited ways you can send inscriptions " +"out of Sparrow Wallet in a safe way. Please note that this is not " +"recommended, and you should only do this if you fully understand what you " +"are doing." msgstr "" +"虽然强烈建议你设置一个比特币核心节点并运行 `ord` 软件,但是你可以通过一些安全" +"的方式在 Sparrow 钱包中发送铭文。请注意,这并不推荐,只有在你完全理解你正在做" +"什么的情况下才能这么做。" -#: src/guides/sat-hunting.md:170 +#: src/guides/collecting/sparrow-wallet.md:103 msgid "" -"```json\n" -"{\n" -" \"descriptor\": " -"\"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" -"*)#csvefu29\",\n" -" \"checksum\": \"tpnxnxax\",\n" -" \"isrange\": true,\n" -" \"issolvable\": true,\n" -" \"hasprivatekeys\": false\n" -"}\n" +"Using the `ord` software will remove much of the complexity we are " +"describing here, as it is able to automatically and safely handle sending " +"inscriptions in an easy way." +msgstr "" +"使用 `ord` 软件将大大简化我们在这里描述的复杂性,因为它能以一种简单的方式自动" +"并安全地处理发送铭文。" + +#: src/guides/collecting/sparrow-wallet.md:105 +msgid "⚠️⚠️ Additional Warning ⚠️⚠️" +msgstr "⚠️⚠️ 额外警告 ⚠️⚠️" + +#: src/guides/collecting/sparrow-wallet.md:106 +msgid "" +"Don't use your sparrow inscriptions wallet to do general sends of non-" +"inscription bitcoin. You can setup a separate wallet in sparrow if you need " +"to do normal bitcoin transactions, and keep your inscriptions wallet " +"separate." +msgstr "" +"不要用你的sparrow麻雀铭文钱包去发送非铭文比特币。如果你需要进行普通的比特币交" +"易,你可以在麻雀中设置一个单独的钱包,并保持你的铭文钱包独立。" + +#: src/guides/collecting/sparrow-wallet.md:108 +msgid "Bitcoin's UTXO model" +msgstr "比特币的UTXO模型" + +#: src/guides/collecting/sparrow-wallet.md:109 +msgid "" +"Before sending any transaction it's important that you have a good mental " +"model for bitcoin's Unspent Transaction Output (UTXO) system. The way " +"Bitcoin works is fundamentally different to many other blockchains such as " +"Ethereum. In Ethereum generally you have a single address in which you store " +"ETH, and you cannot differentiate between any of the ETH - it is just all a " +"single value of the total amount in that address. Bitcoin works very " +"differently in that we generate a new address in the wallet for each " +"receive, and every time you receive sats to an address in your wallet you " +"are creating a new UTXO. Each UTXO can be seen and managed individually. You " +"can select specific UTXO's which you want to spend, and you can choose not " +"to spend certain UTXO's." +msgstr "" +"在发送任何交易之前,你必须对比特币的未消费交易输出(UTXO)系统有一个良好的理" +"解。比特币的工作方式与以太坊等许多其他区块链有着根本的不同。在以太坊中,通常" +"你有一个存储ETH的单一地址,你无法区分其中的任何ETH - 它们只是该地址中的总金额" +"的单一值。而比特币的工作方式完全不同,我们为每个接收生成一个新地址,每次你向" +"钱包中的一个地址接收sats时,你都在创建一个新的UTXO。每个UTXO都可以单独查看和" +"管理。你可以选择想要花费的特定UTXO,也可以选择不花费某些UTXO。" + +#: src/guides/collecting/sparrow-wallet.md:111 +msgid "" +"Some Bitcoin wallets do not expose this level of detail, and they just show " +"you a single summed up value of all the bitcoin in your wallet. However, " +"when sending inscriptions it is important that you use a wallet like Sparrow " +"which allows for UTXO control." +msgstr "" +"有些比特币钱包并不显示这个级别的详细信息,它们只向你显示钱包中所有比特币的单" +"一总和值。然而,当发送铭文时,使用如麻雀这样允许UTXO控制的钱包非常重要。" + +#: src/guides/collecting/sparrow-wallet.md:113 +msgid "Inspecting your inscription before sending" +msgstr "在发送之前检查你的铭文" + +#: src/guides/collecting/sparrow-wallet.md:114 +msgid "" +"Like we have previously described inscriptions are inscribed onto sats, and " +"sats are stored within UTXOs. UTXO's are a collection of satoshis with some " +"particular value of the number of satoshis (the output value). Usually (but " +"not always) the inscription will be inscribed on the first satoshi in the " +"UTXO." +msgstr "" +"如我们之前所述,铭文是刻在聪上的,sats存储在UTXO中。UTXO是具有某个特定数量的" +"satoshi(输出值)的satoshi集合。通常(但不总是)铭文会被刻在UTXO中的第一个" +"satoshi上。" + +#: src/guides/collecting/sparrow-wallet.md:116 +msgid "" +"When inspecting your inscription before sending the main thing you will want " +"to check is which satoshi in the UTXO your inscription is inscribed on." +msgstr "" +"在发送前检查你的铭文时,你主要要检查的是你的铭文刻在UTXO中的哪个satoshi上。" + +#: src/guides/collecting/sparrow-wallet.md:118 +msgid "" +"To do this, you can follow the [Validating / Viewing Received Inscriptions]" +"(./sparrow-wallet.md#validating--viewing-received-inscriptions) described " +"above to find the inscription page for your inscription on ordinals.com" +msgstr "" +"为此,你可以按照上述 [验证/查看收到的铭文](./sparrow-wallet.md#validating--" +"viewing-received-inscriptions)来找到ordinals.com上你的铭文的铭文页面。" + +#: src/guides/collecting/sparrow-wallet.md:120 +msgid "" +"There you will find some metadata about your inscription which looks like " +"the following:" +msgstr "在那里,你会找到一些关于你铭文的元数据,如下所示:" + +#: src/guides/collecting/sparrow-wallet.md:122 +msgid "![](images/sending_01.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:124 +msgid "There is a few of important things to check here:" +msgstr "以下是需要检查的几个重要事项:" + +#: src/guides/collecting/sparrow-wallet.md:125 +msgid "" +"The `output` identifier matches the identifier of the UTXO you are going to " +"send" +msgstr "`output` 标识符与您将要发送的UTXO的标识符匹配" + +#: src/guides/collecting/sparrow-wallet.md:126 +msgid "" +"The `offset` of the inscription is `0` (this means that the inscription is " +"located on the first sat in the UTXO)" +msgstr "铭文的`offset`是 `0` (这意味着铭文位于UTXO的第一个sat上)" + +#: src/guides/collecting/sparrow-wallet.md:127 +msgid "" +"the `output_value` has enough sats to cover the transaction fee (postage) " +"for sending the transaction. The exact amount you will need depends on the " +"fee rate you will select for the transaction" +msgstr "" +"`output_value` 有足够的sats来支付发送交易的交易费(邮资),您需要的确切金额取" +"决于您为交易选择的费率" + +#: src/guides/collecting/sparrow-wallet.md:129 +msgid "" +"If all of the above are true for your inscription, it should be safe for you " +"to send it using the method below." +msgstr "" +"如果以上所有内容对于您的铭文都是正确的,那么您应该可以安全地使用以下方法发送" +"它。" + +#: src/guides/collecting/sparrow-wallet.md:131 +msgid "" +"⚠️⚠️ Be very careful sending your inscription particularly if the `offset` " +"value is not `0`. It is not recommended to use this method if that is the " +"case, as doing so you could accidentally send your inscription to a bitcoin " +"miner unless you know what you are doing." +msgstr "" +"⚠️⚠️ 发送铭文时要非常小心,特别是如果`offset` 值不是`0`。如果是这种情况,不建议" +"使用这种方法,否则您可能会无意中将您的雕文发送给比特币矿工,除非您知道自己在" +"做什么。" + +#: src/guides/collecting/sparrow-wallet.md:133 +msgid "Sending your inscription" +msgstr "发送您的铭文" + +#: src/guides/collecting/sparrow-wallet.md:134 +msgid "" +"To send an inscription navigate to the `UTXOs` tab, and find the UTXO which " +"you previously validated contains your inscription." +msgstr "" +"要发送铭文,请导航到`UTXOs`选项卡,并找到您之前验证包含您的雕文的UTXO。" + +#: src/guides/collecting/sparrow-wallet.md:136 +msgid "" +"If you previously froze the UXTO you will need to right-click on it and " +"unfreeze it." +msgstr "如果您之前冻结了UXTO,您将需要右键单击它并解冻它。" + +#: src/guides/collecting/sparrow-wallet.md:138 +msgid "" +"Select the UTXO you want to send, and ensure that is the _only_ UTXO is " +"selected. You should see `UTXOs 1/1` in the interface. Once you are sure " +"this is the case you can hit `Send Selected`." +msgstr "" +"选择您想要发送的UTXO,并确保这是唯一选中的UTXO。在界面中,您应该看到`UTXOs " +"1/1`。确定这个后,您可以点击`Send Selected`。" + +#: src/guides/collecting/sparrow-wallet.md:140 +msgid "![](images/sending_02.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:142 +msgid "" +"You will then be presented with the transaction construction interface. " +"There is a few things you need to check here to make sure that this is a " +"safe send:" +msgstr "" +"然后,您将看到交易构建界面。在这里,您需要检查几件事以确保这是一个安全的发" +"送:" + +#: src/guides/collecting/sparrow-wallet.md:144 +msgid "" +"The transaction should have only 1 input, and this should be the UTXO with " +"the label you want to send" +msgstr "交易应该只有1个输入,这应该是您想要发送的带有标签的UTXO" + +#: src/guides/collecting/sparrow-wallet.md:145 +msgid "" +"The transaction should have only 1 output, which is the address/label where " +"you want to send the inscription" +msgstr "交易应该只有1个输出,这是您想要发送铭文的地址/标签" + +#: src/guides/collecting/sparrow-wallet.md:147 +msgid "" +"If your transaction looks any different, for example you have multiple " +"inputs, or multiple outputs then this may not be a safe transfer of your " +"inscription, and you should abandon sending until you understand more, or " +"can import into the `ord` wallet." +msgstr "" +"如果您的交易看起来与此不同,例如您有多个输入或多个输出,那么这可能不是一种安" +"全的铭文传输方式,您应该放弃发送,直到您更了解或可以导入到`ord`钱包。" + +#: src/guides/collecting/sparrow-wallet.md:149 +msgid "" +"You should set an appropriate transaction fee, Sparrow will usually " +"recommend a reasonable one, but you can also check [mempool.space](https://" +"mempool.space) to see what the recommended fee rate is for sending a " +"transaction." +msgstr "" +"您应该设置合适的交易费用,Sparrow通常会推荐一个合理的费用,但您也可以查看" +"[mempool.space](https://mempool.space) 以查看发送交易的推荐费率。" + +#: src/guides/collecting/sparrow-wallet.md:151 +msgid "" +"You should add a label for the recipient address, a label like `alice " +"address for inscription #123` would be ideal." +msgstr "" +"您应该为收件人地址添加一个标签,如`alice address for inscription #123`就很理" +"想。" + +#: src/guides/collecting/sparrow-wallet.md:153 +msgid "" +"Once you have checked the transaction is a safe transaction using the checks " +"above, and you are confident to send it you can click `Create Transaction`." +msgstr "" +"在使用上述检查确认交易是安全的交易,并且有信心发送它后,您可以点击`Create " +"Transaction`。" + +#: src/guides/collecting/sparrow-wallet.md:155 +msgid "![](images/sending_03.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:157 +msgid "" +"Here again you can double check that your transaction looks safe, and once " +"you are confident you can click `Finalize Transaction for Signing`." +msgstr "" +"在这里,您可以再次确认您的交易是否安全,在确认后,您可以点击`Finalize " +"Transaction for Signing`。" + +#: src/guides/collecting/sparrow-wallet.md:159 +msgid "![](images/sending_04.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:161 +msgid "Here you can triple check everything before hitting `Sign`." +msgstr "在这里,你可以在点击`Sign`之前再次确认所有内容。" + +#: src/guides/collecting/sparrow-wallet.md:163 +msgid "![](images/sending_05.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:165 +msgid "" +"And then actually you get very very last chance to check everything before " +"hitting `Broadcast Transaction`. Once you broadcast the transaction it is " +"sent to the bitcoin network, and starts being propagated into the mempool." +msgstr "" +"然后实际上在点击`Broadcast Transaction`之前,你有最后一次检查所有内容的机会。" +"一旦你广播交易,它就会被发送到比特币网络,并开始在mempool中传播。" + +#: src/guides/collecting/sparrow-wallet.md:167 +msgid "![](images/sending_06.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:169 +msgid "" +"If you want to track the status of your transaction you can copy the " +"`Transaction Id (Txid)` and paste that into [mempool.space](https://mempool." +"space)" +msgstr "" +"如果你想跟踪你的交易状态,你可以复制`Transaction Id (Txid)`并粘贴到[mempool." +"space](https://mempool.space)" + +#: src/guides/collecting/sparrow-wallet.md:171 +msgid "" +"Once the transaction has confirmed you can check the inscription page on " +"[ordinals.com](https://ordinals.com) to validate that it has moved to the " +"new output location and address." +msgstr "" +"一旦交易确认,你可以在[ordinals.com](https://ordinals.com) 的铭文页面上验证它" +"是否已移动到新的输出位置和地址。" + +#: src/guides/collecting/sparrow-wallet.md:175 +msgid "" +"Sparrow wallet is not showing a transaction/UTXO, but I can see it on " +"mempool.space!" +msgstr "Sparrow钱包没有显示交易/UTXO,但我在mempool.space上看到了" + +#: src/guides/collecting/sparrow-wallet.md:177 +msgid "" +"Make sure that your wallet is connected to a bitcoin node. To validate this, " +"head into the `Preferences`\\-> `Server` settings, and click `Edit Existing " +"Connection`." +msgstr "" +"确保你的钱包连接到一个比特币节点。要验证这一点,转到`Preferences`\\-> " +"`Server` 设置,并点击 `Edit Existing Connection`。" + +#: src/guides/collecting/sparrow-wallet.md:179 +msgid "![](images/troubleshooting_01.png)" +msgstr "" + +#: src/guides/collecting/sparrow-wallet.md:181 +msgid "" +"From there you can select a node and click `Test Connection` to validate " +"that Sparrow is able to connect successfully." +msgstr "" +"从那里你可以选择一个节点并点击 `Test Connection` 来验证Sparrow是否能够成功连" +"接。" + +#: src/guides/collecting/sparrow-wallet.md:183 +msgid "![](images/troubleshooting_02.png)" +msgstr "" + +#: src/guides/moderation.md:4 +msgid "" +"`ord` includes a block explorer, which you can run locally with `ord server`." +msgstr "" +"`ord` 包含了一个区块浏览器,你可以在本地运行`ord server`." + +#: src/guides/moderation.md:6 +msgid "" +"The block explorer allows viewing inscriptions. Inscriptions are user-" +"generated content, which may be objectionable or unlawful." +msgstr "" +"区块浏览器允许查看铭文。铭文是用户生成的内容,因此可能令人反感或非法的。" + +#: src/guides/moderation.md:9 +msgid "" +"It is the responsibility of each individual who runs an ordinal block " +"explorer instance to understand their responsibilities with respect to " +"unlawful content, and decide what moderation policy is appropriate for their " +"instance." +msgstr "" +"运行ord区块浏览器实例的每个人都有责任了解他们对非法内容的责任,并决定适合他们" +"实例的审核政策。" + +#: src/guides/moderation.md:13 +msgid "" +"In order to prevent particular inscriptions from being displayed on an `ord` " +"instance, they can be included in a YAML config file, which is loaded with " +"the `--config` option." +msgstr "" +"为了防止特定的铭文显示在`ord`实例上,它们可以包含在 YAML 配置文件中,该文件使" +"用 `--config`选项加载。" + +#: src/guides/moderation.md:17 +msgid "" +"To hide inscriptions, first create a config file, with the inscription ID " +"you want to hide:" +msgstr "要隐藏铭文,首先创建一个配置文件,其中包含要隐藏的铭文 ID:" + +#: src/guides/moderation.md:20 +msgid "" +"```yaml\n" +"hidden:\n" +"- 0000000000000000000000000000000000000000000000000000000000000000i0\n" "```" msgstr "" -#: src/guides/sat-hunting.md:180 -msgid "And for the change address descriptor, in this case `64k8wnd7`:" +#: src/guides/moderation.md:25 +msgid "" +"The suggested name for `ord` config files is `ord.yaml`, but any filename " +"can be used." +msgstr "`ord` 配置文件的建议名称是 `ord.yaml`,但可以使用任何文件名。" + +#: src/guides/moderation.md:28 +msgid "Then pass the file to `--config` when starting the server:" +msgstr "然后将文件在服务启动的使用使用 `--config` :" + +#: src/guides/moderation.md:30 +msgid "`ord --config ord.yaml server`" msgstr "" -#: src/guides/sat-hunting.md:182 +#: src/guides/moderation.md:32 msgid "" -"```sh\n" -"bitcoin-cli getdescriptorinfo \\\n" -" 'wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" -"*)'\n" -"```" +"Note that the `--config` option comes after `ord` but before the `server` " +"subcommand." +msgstr "请注意, `--config` 选项的位置在 `ord` 之后但是在 `server`子命令前。" + +#: src/guides/moderation.md:35 +msgid "`ord` must be restarted in to load changes to the config file." +msgstr "`ord` 必须重启才可以加载在配置文件中的更改。" + +#: src/guides/moderation.md:37 +msgid "`ordinals.com`" msgstr "" -#: src/guides/sat-hunting.md:187 +#: src/guides/moderation.md:40 msgid "" -"```json\n" -"{\n" -" \"descriptor\": " -"\"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" -"*)#fyfc5f6a\",\n" -" \"checksum\": \"64k8wnd7\",\n" -" \"isrange\": true,\n" -" \"issolvable\": true,\n" -" \"hasprivatekeys\": false\n" -"}\n" -"```" +"The `ordinals.com` instances use `systemd` to run the `ord server` service, " +"which is called `ord`, with a config file located at `/var/lib/ord/ord.yaml`." msgstr "" +"`ordinals.com` 实例使用 `systemd` 运行名为 `ord`的 `ord server` 服务,配置文" +"件在 `/var/lib/ord/ord.yaml`." -#: src/guides/sat-hunting.md:197 -msgid "Load the wallet you want to import the descriptors into:" -msgstr "" +#: src/guides/moderation.md:43 +msgid "To hide an inscription on `ordinals.com`:" +msgstr "要在 ordinals.com 上隐藏铭文:" -#: src/guides/sat-hunting.md:203 +#: src/guides/moderation.md:45 +msgid "SSH into the server" +msgstr "使用SSH登陆服务器" + +#: src/guides/moderation.md:46 +msgid "Add the inscription ID to `/var/lib/ord/ord.yaml`" +msgstr "在 `/var/lib/ord/ord.yaml`中增加铭文ID" + +#: src/guides/moderation.md:47 +msgid "Restart the service with `systemctl restart ord`" +msgstr "使用 `systemctl restart ord` 重启服务" + +#: src/guides/moderation.md:48 +msgid "Monitor the restart with `journalctl -u ord`" +msgstr "通过 `journalctl -u ord` 重启" + +#: src/guides/moderation.md:50 msgid "" -"Now import the descriptors, with the correct checksums, into Bitcoin Core." -msgstr "" +"Currently, `ord` is slow to restart, so the site will not come back online " +"immediately." +msgstr "目前,ord 重启速度较慢,因此站点不会立即恢复在线。" -#: src/guides/sat-hunting.md:205 +#: src/guides/reindexing.md:4 msgid "" -"```sh\n" -"bitcoin-cli \\\n" -" importdescriptors \\\n" -" '[\n" -" {\n" -" \"desc\": " -"\"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" -"*)#tpnxnxax\"\n" -" \"timestamp\":0\n" -" },\n" -" {\n" -" \"desc\": " -"\"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" -"*)#64k8wnd7\",\n" -" \"timestamp\":0\n" -" }\n" -" ]'\n" -"```" +"Sometimes the `ord` database must be reindexed, which means deleting the " +"database and restarting the indexing process with either `ord index update` " +"or `ord server`. Reasons to reindex are:" msgstr "" +"有时必须重新索引‘ord’数据库,这意味着删除数据库并使用 `ord index update`或" +"`ord server`来重新索引数据库。重新索引的原因是:" -#: src/guides/sat-hunting.md:220 +#: src/guides/reindexing.md:8 +msgid "A new major release of ord, which changes the database scheme" +msgstr "ord 发布新的主要版本,更改了数据库架构" + +#: src/guides/reindexing.md:9 +msgid "The database got corrupted somehow" +msgstr "数据库可能会损坏" + +#: src/guides/reindexing.md:11 msgid "" -"If you know the Unix timestamp when your wallet first started receive " -"transactions, you may use it for the value of the `\"timestamp\"` fields " -"instead of `0`. This will reduce the time it takes for Bitcoin Core to " -"search for your wallet's UTXOs." +"The database `ord` uses is called [redb](https://github.com/cberner/redb), " +"so we give the index the default file name `index.redb`. By default we store " +"this file in different locations depending on your operating system." msgstr "" +"`ord` 使用的数据库称为 [redb](https://github.com/cberner/redb),所以我们为索" +"引指定默认文件名‘index.redb’。默认情况下我们存储根据您的操作系统,此文件位于" +"不同的位置。" -#: src/guides/sat-hunting.md:237 -msgid "Exporting Descriptors" -msgstr "" +#: src/guides/reindexing.md:15 +msgid "Platform" +msgstr "平台" -#: src/guides/sat-hunting.md:241 -msgid "" -"Navigate to the `Settings` tab, then to `Script Policy`, and press the edit " -"button to display the descriptor." +#: src/guides/reindexing.md:15 +msgid "Value" msgstr "" -#: src/guides/sat-hunting.md:244 -msgid "Transferring Ordinals" +#: src/guides/reindexing.md:17 +msgid "Linux" msgstr "" -#: src/guides/sat-hunting.md:246 -msgid "" -"The `ord` wallet supports transferring specific satoshis. You can also use " -"`bitcoin-cli` commands `createrawtransaction`, " -"`signrawtransactionwithwallet`, and `sendrawtransaction`, how to do so is " -"complex and outside the scope of this guide." +#: src/guides/reindexing.md:17 +msgid "`$XDG_DATA_HOME`/ord or `$HOME`/.local/share/ord" msgstr "" -#: src/guides/teleburning.md:4 -msgid "" -"Teleburn addresses can be used to burn assets on other blockchains, leaving " -"behind in the smoking rubble a sort of forwarding address pointing to an " -"inscription on Bitcoin." +#: src/guides/reindexing.md:17 +msgid "/home/alice/.local/share/ord" msgstr "" -"燃烧传送Teleburn地址可以用于燃烧其他区块链上的资产, 留下一个转发地址指向一个比特币上的铭文" -"这些地址就像是烟熏弹火后的废墟。 " -#: src/guides/teleburning.md:8 -msgid "" -"Teleburning an asset means something like, \"I'm out. Find me on Bitcoin.\"" +#: src/guides/reindexing.md:18 +msgid "macOS" msgstr "" -"燃烧传送一个资产似乎意味着 \"我走了,在比特币链上找我。\"" -#: src/guides/teleburning.md:10 -msgid "" -"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 supported. Pull requests " -"adding teleburn addresses for other chains are welcome." +#: src/guides/reindexing.md:18 +msgid "`$HOME`/Library/Application Support/ord" msgstr "" -"Teleburn 地址源自铭文的ID,他们没有私钥,因此发往燃烧传送地址的资产将被烧毁 " -"当前只支持以太坊的燃烧地址,欢迎提交关于其他链上的燃烧传送地址的拉取请求 " -#: src/guides/teleburning.md:15 -#, fuzzy -msgid "Ethereum" -msgstr "以太坊" +#: src/guides/reindexing.md:18 +msgid "/Users/Alice/Library/Application Support/ord" +msgstr "" -#: src/guides/teleburning.md:18 -msgid "" -"Ethereum teleburn addresses are derived by taking the first 20 bytes of the " -"SHA-256 hash of the inscription ID, serialized as 36 bytes, with the first " -"32 bytes containing the transaction ID, and the last four bytes containing " -"big-endian inscription index, and interpreting it as an Ethereum address." +#: src/guides/reindexing.md:19 +msgid "Windows" msgstr "" -"以太坊的燃烧传送teleburn地址是根据取铭文ID的SHA-256哈希的前20字节来生成的 " -"这个哈希被序列化为36字节,其中前32字节包含交易ID, " -"最后四个字节包含大端序的铭文索引,并将其解释为一个Ethereum地址。" -#: src/guides/teleburning.md:26 -msgid "" -"The ENS domain name [rodarmor.eth](https://app.ens.domains/rodarmor.eth), " -"was teleburned to [inscription zero](https://ordinals.com/" -"inscription/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0)." +#: src/guides/reindexing.md:19 +msgid "`{FOLDERID_RoamingAppData}`\\\\ord" msgstr "" -"ENS 域名 [rodarmor.eth](https://app.ens.domains/rodarmor.eth), " -"被燃烧传输teleburned 到 [inscription zero](https://ordinals.com/" -"inscription/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0)." -#: src/guides/teleburning.md:30 -msgid "" -"Running the inscription ID of inscription zero is " -"`6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0`." +#: src/guides/reindexing.md:19 +msgid "C:\\Users\\Alice\\AppData\\Roaming\\ord" msgstr "" -"零号铭文的铭文ID是" -"`6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0`." -#: src/guides/teleburning.md:33 +#: src/guides/reindexing.md:21 msgid "" -"Passing `6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0` " -"to the teleburn command:" -msgstr "" -"使用teleburn命令 `6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0` " +"So to delete the database and reindex on MacOS you would have to run the " +"following commands in the terminal:" +msgstr "因此,要在 MacOS 上删除数据库并重新索引,您必须在终端中执行以下命令:" -#: src/guides/teleburning.md:36 +#: src/guides/reindexing.md:24 msgid "" "```bash\n" -"$ ord teleburn " -"6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0\n" +"rm ~/Library/Application Support/ord/index.redb\n" +"ord index update\n" "```" msgstr "" -#: src/guides/teleburning.md:40 -msgid "Returns:" -msgstr "返回" - -#: src/guides/teleburning.md:42 +#: src/guides/reindexing.md:29 msgid "" -"```json\n" -"{\n" -" \"ethereum\": \"0xe43A06530BdF8A4e067581f48Fae3b535559dA9e\"\n" -"}\n" -"```" +"You can of course also set the location of the data directory yourself with " +"`ord --datadir index update` or give it a specific filename and path " +"with `ord --index index update`." msgstr "" +"您当然也可以自己设置数据目录的位置,`ord --datadir index update` 或为其" +"指定特定的文件名和路径,使用‘ord --index 索引运行’。" -#: src/guides/teleburning.md:48 +#: src/guides/sat-hunting.md:4 msgid "" -"Indicating that `0xe43A06530BdF8A4e067581f48Fae3b535559dA9e` is the Ethereum " -"teleburn address for inscription zero, which is, indeed, the current owner, " -"on Ethereum, of `rodarmor.eth`." +"_This guide is out of date. Since it was written, the `ord` binary was " +"changed to only build the full satoshi index when the `--index-sats` flag is " +"supplied. Additionally, `ord` now has a built-in wallet that wraps a Bitcoin " +"Core wallet. See `ord wallet --help`._" msgstr "" -"显示出 `0xe43A06530BdF8A4e067581f48Fae3b535559dA9e` 是零号铭文的 teleburn地址 ," -"事实上,它是在Ethereum上的`rodarmor.eth`的当前所有者" +"_本指南已过时。自编写以来,“ord”安装文件已更改仅当提供“--index-sats”标志时才" +"构建完整的聪索引。此外,“ord”现在有一个内置钱包,其中包含比特币核心钱包。请参" +"阅`ord wallet --help`。_" -#: src/guides/collecting.md:4 +#: src/guides/sat-hunting.md:9 msgid "" -"Currently, [ord](https://github.com/ordinals/ord/) is the only wallet " -"supporting sat-control and sat-selection, which are required to safely store " -"and send rare sats and inscriptions, hereafter ordinals." +"Ordinal hunting is difficult but rewarding. The feeling of owning a wallet " +"full of UTXOs, redolent with the scent of rare and exotic sats, is beyond " +"compare." msgstr "" -"目前,[ord](https://github.com/ordinals/ord/) 是唯一支持 sat-control 和 sat-" -"selection 的钱包,这是安全存储和发送稀有 sats 和铭文(以下简称序数)所必需" -"的。" -#: src/guides/collecting.md:8 +#: src/guides/sat-hunting.md:12 msgid "" -"The recommended way to send, receive, and store ordinals is with `ord`, but " -"if you are careful, it is possible to safely store, and in some cases send, " -"ordinals with other wallets." +"Ordinals are numbers for satoshis. Every satoshi has an ordinal number and " +"every ordinal number has a satoshi." msgstr "" -"发送、接收和存储序号的推荐方法是使用 `ord`,但如果你小心,可以安全地存储,在" -"某些情况下,使用其他钱包发送序号。" -#: src/guides/collecting.md:12 -msgid "" -"As a general note, receiving ordinals in an unsupported wallet is not " -"dangerous. Ordinals can be sent to any bitcoin address, and are safe as long " -"as the UTXO that contains them is not spent. However, if that wallet is then " -"used to send bitcoin, it may select the UTXO containing the ordinal as an " -"input, and send the inscription or spend it to fees." +#: src/guides/sat-hunting.md:15 +msgid "Preparation" +msgstr "" + +#: src/guides/sat-hunting.md:18 +msgid "There are a few things you'll need before you start." msgstr "" -"作为一般说明,在不受支持的钱包中接收序号并不危险。 序号可以发送到任何比特币地" -"址,只要包含它们的 UTXO 没有被花费,它就是安全的。 但是,如果该钱包随后用于发" -"送比特币,它可能会选择包含序号的 UTXO 作为输入,并发送铭文或将其用于费用。" -#: src/guides/collecting.md:18 +#: src/guides/sat-hunting.md:20 msgid "" -"A [guide](./collecting/sparrow-wallet.md) to creating an `ord`\\-compatible " -"wallet with [Sparrow Wallet](https://sparrowwallet.com/), is available in " -"this handbook." +"First, you'll need a synced Bitcoin Core node with a transaction index. To " +"turn on transaction indexing, pass `-txindex` on the command-line:" msgstr "" -"本手册提供了使用[Sparrow Wallet](https://sparrowwallet.com/)创建与 `ord`兼容" -"的钱包的[指南](./collecting/sparrow-wallet.md) 。" -#: src/guides/collecting.md:21 +#: src/guides/sat-hunting.md:23 msgid "" -"Please note that if you follow this guide, you should not use the wallet you " -"create to send BTC, unless you perform manual coin-selection to avoid " -"sending ordinals." +"```sh\n" +"bitcoind -txindex\n" +"```" msgstr "" -"请注意,如果您遵循本指南,则不应使用您创建的钱包发送 BTC,除非您执行手动硬币" -"选择以避免发送序号。" - -#: src/guides/collecting/sparrow-wallet.md:1 -msgid "Collecting Inscriptions and Ordinals with Sparrow Wallet" -msgstr "使用麻雀Sparrow钱包收藏铭文" -#: src/guides/collecting/sparrow-wallet.md:4 +#: src/guides/sat-hunting.md:27 msgid "" -"Users who cannot or have not yet set up the [ord](https://github.com/" -"ordinals/ord) wallet can receive inscriptions and ordinals with alternative " -"bitcoin wallets, as long as they are _very_ careful about how they spend " -"from that wallet." +"Or put the following in your [Bitcoin configuration file](https://github.com/" +"bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md#configuration-file-path):" msgstr "" -"那些无法活着尚未设置[ord](https://github.com/ordinals/ord) 钱包的用户可以使用" -"其他比特币钱包接收铭文和序数,只要他们在使用该钱包时非常小心。" -#: src/guides/collecting/sparrow-wallet.md:6 +#: src/guides/sat-hunting.md:34 msgid "" -"This guide gives some basic steps on how to create a wallet with [Sparrow " -"Wallet](https://sparrowwallet.com/) which is compatible with `ord` and can " -"be later imported into `ord`" +"Launch it and wait for it to catch up to the chain tip, at which point the " +"following command should print out the current block height:" msgstr "" -"本指南提供了一些基本步骤,说明如何使用 [Sparrow Wallet](https://" -"sparrowwallet.com/) 创建一个与`ord`兼容的钱包,稍后可以将其导入到`ord`" - -#: src/guides/collecting/sparrow-wallet.md:8 -msgid "⚠️⚠️ Warning!! ⚠️⚠️" -msgstr "⚠️⚠️ 警告!! ⚠️⚠️" -#: src/guides/collecting/sparrow-wallet.md:9 +#: src/guides/sat-hunting.md:37 msgid "" -"As a general rule if you take this approach, you should use this wallet with " -"the Sparrow software as a receive-only wallet." +"```sh\n" +"bitcoin-cli getblockcount\n" +"```" msgstr "" -"一般来说,如果你选择这种方法,你应该将这个钱包作为接收款项的钱包,使用Sparrow" -"软件。" -#: src/guides/collecting/sparrow-wallet.md:11 -msgid "" -"Do not spend any satoshis from this wallet unless you are sure you know what " -"you are doing. You could very easily inadvertently lose access to your " -"ordinals and inscriptions if you don't heed this warning." +#: src/guides/sat-hunting.md:41 +msgid "Second, you'll need a synced `ord` index." msgstr "" -"除非你确定知道自己在做什么,否则不要从这个钱包中花费任何比特币。如果你不注意" -"这个警告,你可能会很容易无意间失去对序数和铭文的访问权限。" -#: src/guides/collecting/sparrow-wallet.md:13 -msgid "Wallet Setup & Receiving" -msgstr "钱包设置和接收" +#: src/guides/sat-hunting.md:43 +msgid "Get a copy of `ord` from [the repo](https://github.com/ordinals/ord/)." +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:15 +#: src/guides/sat-hunting.md:45 msgid "" -"Download the Sparrow Wallet from the [releases page](https://sparrowwallet." -"com/download/) for your particular operating system." +"Run `RUST_LOG=info ord index`. It should connect to your bitcoin core node " +"and start indexing." msgstr "" -"根据你的操作系统从 [发布页面](https://sparrowwallet.com/download/) 下载" -"Sparrow钱包。" -#: src/guides/collecting/sparrow-wallet.md:17 -msgid "Select `File -> New Wallet` and create a new wallet called `ord`." -msgstr "选择 `File -> New Wallet`并创建一个名为`ord`的新钱包。" +#: src/guides/sat-hunting.md:48 +msgid "Wait for it to finish indexing." +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:19 -msgid "![](images/wallet_setup_01.png)" +#: src/guides/sat-hunting.md:50 +msgid "Third, you'll need a wallet with UTXOs that you want to search." msgstr "" -#: src/guides/collecting/sparrow-wallet.md:21 -msgid "" -"Change the `Script Type` to `Taproot (P2TR)` and select the `New or Imported " -"Software Wallet` option." +#: src/guides/sat-hunting.md:52 +msgid "Searching for Rare Ordinals" msgstr "" -"将`Script Type`更改为`Taproot (P2TR)`,然后选择`New or Imported Software " -"Wallet`选项。" -#: src/guides/collecting/sparrow-wallet.md:23 -msgid "![](images/wallet_setup_02.png)" +#: src/guides/sat-hunting.md:55 +msgid "Searching for Rare Ordinals in a Bitcoin Core Wallet" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:25 +#: src/guides/sat-hunting.md:57 msgid "" -"Select `Use 12 Words` and then click `Generate New`. Leave the passphrase " -"blank." -msgstr "选择`Use 12 Words`,然后点击 `Generate New`。密码短语留空。" +"The `ord wallet` command is just a wrapper around Bitcoin Core's RPC API, so " +"searching for rare ordinals in a Bitcoin Core wallet is Easy. Assuming your " +"wallet is named `foo`:" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:27 -msgid "![](images/wallet_setup_03.png)" +#: src/guides/sat-hunting.md:61 +msgid "Load your wallet:" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:29 +#: src/guides/sat-hunting.md:63 msgid "" -"A new 12 word BIP39 seed phrase will be generated for you. Write this down " -"somewhere safe as this is your backup to get access to your wallet. NEVER " -"share or show this seed phrase to anyone else." +"```sh\n" +"bitcoin-cli loadwallet foo\n" +"```" msgstr "" -"将为你生成一个新的12词BIP39种子短语。将此短语写在安全的地方,这是获取钱包访问" -"权限的备份。切勿与他人分享或显示这个种子短语。" - -#: src/guides/collecting/sparrow-wallet.md:31 -msgid "Once you have written down the seed phrase click `Confirm Backup`." -msgstr "一旦你把种子短语写下来,点击 `Confirm Backup`." -#: src/guides/collecting/sparrow-wallet.md:33 -msgid "![](images/wallet_setup_04.png)" +#: src/guides/sat-hunting.md:67 +msgid "Display any rare ordinals wallet `foo`'s UTXOs:" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:35 +#: src/guides/sat-hunting.md:69 msgid "" -"Re-enter the seed phrase which you wrote down, and then click `Create " -"Keystore`." -msgstr "重新输入你记下的种子短语,然后点击 `Create Keystore`." +"```sh\n" +"ord --index-sats wallet --name foo sats\n" +"```" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:37 -msgid "![](images/wallet_setup_05.png)" +#: src/guides/sat-hunting.md:73 +msgid "Searching for Rare Ordinals in a Non-Bitcoin Core Wallet" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:39 -msgid "Click `Import Keystore`." -msgstr "点击 `Import Keystore`." +#: src/guides/sat-hunting.md:75 +msgid "" +"The `ord wallet` command is just a wrapper around Bitcoin Core's RPC API, so " +"to search for rare ordinals in a non-Bitcoin Core wallet, you'll need to " +"import your wallet's descriptors into Bitcoin Core." +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:41 -msgid "![](images/wallet_setup_06.png)" +#: src/guides/sat-hunting.md:79 +msgid "" +"[Descriptors](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors." +"md) describe the ways that wallets generate private keys and public keys." msgstr "" -#: src/guides/collecting/sparrow-wallet.md:43 -msgid "Click `Apply`. Add a password for the wallet if you want to." -msgstr "点击 `Apply`。如果你想的话,可以为钱包添加一个密码。" +#: src/guides/sat-hunting.md:82 +msgid "" +"You should only import descriptors into Bitcoin Core for your wallet's " +"public keys, not its private keys." +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:45 -msgid "![](images/wallet_setup_07.png)" +#: src/guides/sat-hunting.md:85 +msgid "" +"If your wallet's public key descriptor is compromised, an attacker will be " +"able to see your wallet's addresses, but your funds will be safe." msgstr "" -#: src/guides/collecting/sparrow-wallet.md:47 +#: src/guides/sat-hunting.md:88 msgid "" -"You now have a wallet which is compatible with `ord`, and can be imported " -"into `ord` using the BIP39 Seed Phrase. To receive ordinals or inscriptions, " -"click on the `Receive` tab and copy a new address." +"If your wallet's private key descriptor is compromised, an attacker can " +"drain your wallet of funds." msgstr "" -"你现在有了一个兼容`ord`的钱包,可以使用BIP39种子短语导入到 `ord`。要接收序数" -"或铭文,点击 `Receive`选项卡并复制一个新地址。" -#: src/guides/collecting/sparrow-wallet.md:49 +#: src/guides/sat-hunting.md:91 msgid "" -"Each time you want to receive you should use a brand-new address, and not re-" -"use existing addresses." -msgstr "每次你想接收时,都应该使用一个全新的地址,而不是重复使用现有的地址。" +"Get the wallet descriptor from the wallet whose UTXOs you want to search for " +"rare ordinals. It will look something like this:" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:51 +#: src/guides/sat-hunting.md:94 msgid "" -"Note that bitcoin is different to some other blockchain wallets, in that " -"this wallet can generate an unlimited number of new addresses. You can " -"generate a new address by clicking on the `Get Next Address` button. You can " -"see all of your addresses in the `Addresses` tab of the app." +"```\n" +"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" +"*)#csvefu29\n" +"```" msgstr "" -"注意,比特币与一些其他区块链钱包不同,这个钱包可以生成无限数量的新地址。你可" -"以通过点击获取下一个地址按钮生成新地址。你可以在应用程序的`Addresses`选项卡中" -"看到所有的地址。" -#: src/guides/collecting/sparrow-wallet.md:53 +#: src/guides/sat-hunting.md:98 +msgid "Create a watch-only wallet named `foo-watch-only`:" +msgstr "" + +#: src/guides/sat-hunting.md:100 msgid "" -"You can add a label to each address, so you can keep track of what it was " -"used for." -msgstr "你可以给每个地址添加一个标签,这样你就可以跟踪它的用途。" +"```sh\n" +"bitcoin-cli createwallet foo-watch-only true true\n" +"```" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:55 -msgid "![](images/wallet_setup_08.png)" +#: src/guides/sat-hunting.md:104 +msgid "Feel free to give it a better name than `foo-watch-only`!" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:57 -msgid "Validating / Viewing Received Inscriptions" -msgstr "验证/查看收到的铭文" +#: src/guides/sat-hunting.md:106 +msgid "Load the `foo-watch-only` wallet:" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:59 +#: src/guides/sat-hunting.md:108 src/guides/sat-hunting.md:199 msgid "" -"Once you have received an inscription you will see a new transaction in the " -"`Transactions` tab of Sparrow, as well as a new UTXO in the `UTXOs` tab." +"```sh\n" +"bitcoin-cli loadwallet foo-watch-only\n" +"```" msgstr "" -"一旦你收到一条铭文,你将在 Sparrow 的 `Transactions` 选项卡中看到一个新的交" -"易,以及在`UTXOs`选项卡中看到一个新的 UTXO。" -#: src/guides/collecting/sparrow-wallet.md:61 -msgid "" -"Initially this transaction may have an \"Unconfirmed\" status, and you will " -"need to wait for it to be mined into a bitcoin block before it is fully " -"received." +#: src/guides/sat-hunting.md:112 +msgid "Import your wallet descriptors into `foo-watch-only`:" msgstr "" -"最初,这笔交易可能有一个\"未确认\"的状态,你需要等待它被挖矿到一个比特币块" -"中,才算真正收到。" -#: src/guides/collecting/sparrow-wallet.md:63 -msgid "![](images/validating_viewing_01.png)" +#: src/guides/sat-hunting.md:114 +msgid "" +"```sh\n" +"bitcoin-cli importdescriptors \\\n" +" '[{ \"desc\": " +"\"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" +"*)#tpnxnxax\", \"timestamp\":0 }]'\n" +"```" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:65 +#: src/guides/sat-hunting.md:119 msgid "" -"To track the status of your transaction you can right-click on it, select " -"`Copy Transaction ID` and then paste that transaction id into [mempool.space]" -"(https://mempool.space)." +"If you know the Unix timestamp when your wallet first started receive " +"transactions, you may use it for the value of `\"timestamp\"` instead of " +"`0`. This will reduce the time it takes for Bitcoin Core to search for your " +"wallet's UTXOs." msgstr "" -"要跟踪你的交易状态,你可以右键点击它,选择`Copy Transaction ID`,然后将该交" -"易 id 粘贴到 [mempool.space](https://mempool.space)。" -#: src/guides/collecting/sparrow-wallet.md:67 -msgid "![](images/validating_viewing_02.png)" +#: src/guides/sat-hunting.md:124 src/guides/sat-hunting.md:225 +msgid "Check that everything worked:" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:69 +#: src/guides/sat-hunting.md:126 src/guides/sat-hunting.md:227 msgid "" -"Once the transaction has confirmed, you can validate and view your " -"inscription by heading over to the `UTXOs` tab, finding the UTXO you want to " -"check, right-clicking on the `Output` and selecting `Copy Transaction " -"Output`. This transaction output id can then be pasted into the [ordinals." -"com](https://ordinals.com) search." +"```sh\n" +"bitcoin-cli getwalletinfo\n" +"```" msgstr "" -"一旦交易被确认,你可以通过前往`UTXOs`选项卡,找到你想要检查的 UTXO,右键点击 " -"`Output` 并选择 `Copy Transaction Output` 来验证和查看你的铭文。然后,这个交" -"易输出 id 可以粘贴到 [ordinals.com](https://ordinals.com) 搜索。" -#: src/guides/collecting/sparrow-wallet.md:72 -msgid "Freezing UTXO's" -msgstr "冻结 UTXO" +#: src/guides/sat-hunting.md:130 src/guides/sat-hunting.md:231 +msgid "Display your wallet's rare ordinals:" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:73 +#: src/guides/sat-hunting.md:132 src/guides/sat-hunting.md:233 msgid "" -"As explained above, each of your inscriptions is stored in an Unspent " -"Transaction Output (UTXO). You want to be very careful not to accidentally " -"spend your inscriptions, and one way to make it harder for this to happen is " -"to freeze the UTXO." +"```sh\n" +"ord wallet sats\n" +"```" msgstr "" -"如上所述,你的每一条铭文都存储在一个未花费的交易输出 (UTXO) 中。你需要非常小" -"心不要意外花费你的铭文,而冻结 UTXO 是使这种情况发生的难度增加的一种方式。" -#: src/guides/collecting/sparrow-wallet.md:75 +#: src/guides/sat-hunting.md:136 msgid "" -"To do this, go to the `UTXOs` tab, find the UTXO you want to freeze, right-" -"click on the `Output` and select `Freeze UTXO`." +"Searching for Rare Ordinals in a Wallet that Exports Multi-path Descriptors" msgstr "" -"要做到这一点,去 UTXOs 选项卡,找到你想要冻结的 `UTXOs`,右键点击 `Output` " -"并选择`Freeze UTXO`。" -#: src/guides/collecting/sparrow-wallet.md:77 +#: src/guides/sat-hunting.md:138 msgid "" -"This UTXO (Inscription) is now un-spendable within the Sparrow Wallet until " -"you unfreeze it." -msgstr "这个 UTXO (铭文) 现在在 Sparrow 钱包中是不可消费的,直到你解冻它。" - -#: src/guides/collecting/sparrow-wallet.md:79 -msgid "Importing into `ord` wallet" -msgstr "导入 `ord` 钱包" +"Some descriptors describe multiple paths in one descriptor using angle " +"brackets, e.g., `<0;1>`. Multi-path descriptors are not yet supported by " +"Bitcoin Core, so you'll first need to convert them into multiple " +"descriptors, and then import those multiple descriptors into Bitcoin Core." +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:81 +#: src/guides/sat-hunting.md:143 msgid "" -"For details on setting up Bitcoin Core and the `ord` wallet check out the " -"[Inscriptions Guide](../inscriptions.md)" +"First get the multi-path descriptor from your wallet. It will look something " +"like this:" msgstr "" -"关于设置比特币核心和 `ord` 钱包的详细信息,请查看[铭文指南](../inscriptions." -"md)" -#: src/guides/collecting/sparrow-wallet.md:83 +#: src/guides/sat-hunting.md:146 msgid "" -"When setting up `ord`, instead of running `ord wallet create` to create a " -"brand-new wallet, you can import your existing wallet using `ord wallet " -"restore \"BIP39 SEED PHRASE\"` using the seed phrase you generated with " -"Sparrow Wallet." +"```\n" +"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/" +"<0;1>/*)#fw76ulgt\n" +"```" msgstr "" -"设置 `ord` 时,你可以使用 `ord wallet restore \"BIP39 SEED PHRASE\"` 命令和你" -"用Sparrow Wallet生成的种子短语,导入你现有的钱包,而不是运行 `ord wallet " -"create` 来创建一个全新的钱包。" -#: src/guides/collecting/sparrow-wallet.md:85 -msgid "" -"There is currently a [bug](https://github.com/ordinals/ord/issues/1589) " -"which causes an imported wallet to not be automatically rescanned against " -"the blockchain. To work around this you will need to manually trigger a " -"rescan using the bitcoin core cli: `bitcoin-cli -rpcwallet=ord " -"rescanblockchain 767430`" +#: src/guides/sat-hunting.md:150 +msgid "Create a descriptor for the receive address path:" msgstr "" -"目前存在一个[程序错误](https://github.com/ordinals/ord/issues/1589) 导致导入" -"的钱包无法自动重新扫描区块链。为解决这个问题,你需要手动触发重新扫描,使用比" -"特币核心命令行界面:" -#: src/guides/collecting/sparrow-wallet.md:88 +#: src/guides/sat-hunting.md:152 msgid "" -"You can then check your wallet's inscriptions using `ord wallet inscriptions`" -msgstr "然后,你可以使用`ord wallet inscriptions`检查你的钱包的铭文." +"```\n" +"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" +"*)\n" +"```" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:90 -msgid "" -"Note that if you have previously created a wallet with `ord`, then you will " -"already have a wallet with the default name, and will need to give your " -"imported wallet a different name. You can use the `--wallet` parameter in " -"all `ord` commands to reference a different wallet, eg:" +#: src/guides/sat-hunting.md:156 +msgid "And the change address path:" msgstr "" -"注意,如果你之前已经用 `ord` 创建过一个钱包,那么你已经有一个默认名称的钱包," -"需要给你导入的钱包取一个不同的名称。你可以在所有的 `ord`命令中使用 `--" -"wallet` 参数来引用不同的钱包,例如:" -#: src/guides/collecting/sparrow-wallet.md:92 -msgid "`ord --wallet ord_from_sparrow wallet restore \"BIP39 SEED PHRASE\"`" +#: src/guides/sat-hunting.md:158 +msgid "" +"```\n" +"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" +"*)\n" +"```" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:94 -msgid "`ord --wallet ord_from_sparrow wallet inscriptions`" +#: src/guides/sat-hunting.md:162 +msgid "" +"Get and note the checksum for the receive address descriptor, in this case " +"`tpnxnxax`:" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:96 -msgid "`bitcoin-cli -rpcwallet=ord_from_sparrow rescanblockchain 767430`" +#: src/guides/sat-hunting.md:165 +msgid "" +"```sh\n" +"bitcoin-cli getdescriptorinfo \\\n" +" 'wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" +"*)'\n" +"```" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:98 -msgid "Sending inscriptions with Sparrow Wallet" -msgstr "使用麻雀钱包发送铭文" +#: src/guides/sat-hunting.md:170 +msgid "" +"```json\n" +"{\n" +" \"descriptor\": " +"\"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" +"*)#csvefu29\",\n" +" \"checksum\": \"tpnxnxax\",\n" +" \"isrange\": true,\n" +" \"issolvable\": true,\n" +" \"hasprivatekeys\": false\n" +"}\n" +"```" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:100 -msgid "⚠️⚠️ Warning ⚠️⚠️" -msgstr "⚠️⚠️ 警告 ⚠️⚠️" +#: src/guides/sat-hunting.md:180 +msgid "And for the change address descriptor, in this case `64k8wnd7`:" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:101 +#: src/guides/sat-hunting.md:182 msgid "" -"While it is highly recommended that you set up a bitcoin core node and run " -"the `ord` software, there are certain limited ways you can send inscriptions " -"out of Sparrow Wallet in a safe way. Please note that this is not " -"recommended, and you should only do this if you fully understand what you " -"are doing." +"```sh\n" +"bitcoin-cli getdescriptorinfo \\\n" +" 'wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" +"*)'\n" +"```" msgstr "" -"虽然强烈建议你设置一个比特币核心节点并运行 `ord` 软件,但是你可以通过一些安全" -"的方式在 Sparrow 钱包中发送铭文。请注意,这并不推荐,只有在你完全理解你正在做" -"什么的情况下才能这么做。" -#: src/guides/collecting/sparrow-wallet.md:103 +#: src/guides/sat-hunting.md:187 msgid "" -"Using the `ord` software will remove much of the complexity we are " -"describing here, as it is able to automatically and safely handle sending " -"inscriptions in an easy way." +"```json\n" +"{\n" +" \"descriptor\": " +"\"wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" +"*)#fyfc5f6a\",\n" +" \"checksum\": \"64k8wnd7\",\n" +" \"isrange\": true,\n" +" \"issolvable\": true,\n" +" \"hasprivatekeys\": false\n" +"}\n" +"```" msgstr "" -"使用 `ord` 软件将大大简化我们在这里描述的复杂性,因为它能以一种简单的方式自动" -"并安全地处理发送铭文。" -#: src/guides/collecting/sparrow-wallet.md:105 -msgid "⚠️⚠️ Additional Warning ⚠️⚠️" -msgstr "⚠️⚠️ 额外警告 ⚠️⚠️" +#: src/guides/sat-hunting.md:197 +msgid "Load the wallet you want to import the descriptors into:" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:106 +#: src/guides/sat-hunting.md:203 msgid "" -"Don't use your sparrow inscriptions wallet to do general sends of non-" -"inscription bitcoin. You can setup a separate wallet in sparrow if you need " -"to do normal bitcoin transactions, and keep your inscriptions wallet " -"separate." +"Now import the descriptors, with the correct checksums, into Bitcoin Core." msgstr "" -"不要用你的sparrow麻雀铭文钱包去发送非铭文比特币。如果你需要进行普通的比特币交" -"易,你可以在麻雀中设置一个单独的钱包,并保持你的铭文钱包独立。" -#: src/guides/collecting/sparrow-wallet.md:108 -msgid "Bitcoin's UTXO model" -msgstr "比特币的UTXO模型" +#: src/guides/sat-hunting.md:205 +msgid "" +"```sh\n" +"bitcoin-cli \\\n" +" importdescriptors \\\n" +" '[\n" +" {\n" +" \"desc\": " +"\"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/" +"*)#tpnxnxax\"\n" +" \"timestamp\":0\n" +" },\n" +" {\n" +" \"desc\": " +"\"wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/" +"*)#64k8wnd7\",\n" +" \"timestamp\":0\n" +" }\n" +" ]'\n" +"```" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:109 +#: src/guides/sat-hunting.md:220 msgid "" -"Before sending any transaction it's important that you have a good mental " -"model for bitcoin's Unspent Transaction Output (UTXO) system. The way " -"Bitcoin works is fundamentally different to many other blockchains such as " -"Ethereum. In Ethereum generally you have a single address in which you store " -"ETH, and you cannot differentiate between any of the ETH - it is just all a " -"single value of the total amount in that address. Bitcoin works very " -"differently in that we generate a new address in the wallet for each " -"receive, and every time you receive sats to an address in your wallet you " -"are creating a new UTXO. Each UTXO can be seen and managed individually. You " -"can select specific UTXO's which you want to spend, and you can choose not " -"to spend certain UTXO's." +"If you know the Unix timestamp when your wallet first started receive " +"transactions, you may use it for the value of the `\"timestamp\"` fields " +"instead of `0`. This will reduce the time it takes for Bitcoin Core to " +"search for your wallet's UTXOs." msgstr "" -"在发送任何交易之前,你必须对比特币的未消费交易输出(UTXO)系统有一个良好的理" -"解。比特币的工作方式与以太坊等许多其他区块链有着根本的不同。在以太坊中,通常" -"你有一个存储ETH的单一地址,你无法区分其中的任何ETH - 它们只是该地址中的总金额" -"的单一值。而比特币的工作方式完全不同,我们为每个接收生成一个新地址,每次你向" -"钱包中的一个地址接收sats时,你都在创建一个新的UTXO。每个UTXO都可以单独查看和" -"管理。你可以选择想要花费的特定UTXO,也可以选择不花费某些UTXO。" -#: src/guides/collecting/sparrow-wallet.md:111 +#: src/guides/sat-hunting.md:237 +msgid "Exporting Descriptors" +msgstr "" + +#: src/guides/sat-hunting.md:241 msgid "" -"Some Bitcoin wallets do not expose this level of detail, and they just show " -"you a single summed up value of all the bitcoin in your wallet. However, " -"when sending inscriptions it is important that you use a wallet like Sparrow " -"which allows for UTXO control." +"Navigate to the `Settings` tab, then to `Script Policy`, and press the edit " +"button to display the descriptor." msgstr "" -"有些比特币钱包并不显示这个级别的详细信息,它们只向你显示钱包中所有比特币的单" -"一总和值。然而,当发送铭文时,使用如麻雀这样允许UTXO控制的钱包非常重要。" -#: src/guides/collecting/sparrow-wallet.md:113 -msgid "Inspecting your inscription before sending" -msgstr "在发送之前检查你的铭文" +#: src/guides/sat-hunting.md:244 +msgid "Transferring Ordinals" +msgstr "" -#: src/guides/collecting/sparrow-wallet.md:114 +#: src/guides/sat-hunting.md:246 msgid "" -"Like we have previously described inscriptions are inscribed onto sats, and " -"sats are stored within UTXOs. UTXO's are a collection of satoshis with some " -"particular value of the number of satoshis (the output value). Usually (but " -"not always) the inscription will be inscribed on the first satoshi in the " -"UTXO." +"The `ord` wallet supports transferring specific satoshis. You can also use " +"`bitcoin-cli` commands `createrawtransaction`, " +"`signrawtransactionwithwallet`, and `sendrawtransaction`, how to do so is " +"complex and outside the scope of this guide." msgstr "" -"如我们之前所述,铭文是刻在聪上的,sats存储在UTXO中。UTXO是具有某个特定数量的" -"satoshi(输出值)的satoshi集合。通常(但不总是)铭文会被刻在UTXO中的第一个" -"satoshi上。" -#: src/guides/collecting/sparrow-wallet.md:116 +#: src/guides/settings.md:4 msgid "" -"When inspecting your inscription before sending the main thing you will want " -"to check is which satoshi in the UTXO your inscription is inscribed on." +"`ord` can be configured with the command line, environment variables, a " +"configuration file, and default values." msgstr "" -"在发送前检查你的铭文时,你主要要检查的是你的铭文刻在UTXO中的哪个satoshi上。" +"`ord`可以通过命令行、环境变量、配置文件以及默认值进行配置。" -#: src/guides/collecting/sparrow-wallet.md:118 +#: src/guides/settings.md:7 msgid "" -"To do this, you can follow the [Validating / Viewing Received Inscriptions]" -"(./sparrow-wallet.md#validating--viewing-received-inscriptions) described " -"above to find the inscription page for your inscription on ordinals.com" +"The command line takes precedence over environment variables, which take " +"precedence over the configuration file, which takes precedence over defaults." msgstr "" -"为此,你可以按照上述 [验证/查看收到的铭文](./sparrow-wallet.md#validating--" -"viewing-received-inscriptions)来找到ordinals.com上你的铭文的铭文页面。" +"命令行的优先级高于环境变量,环境变量的优先级又高于配置文件,配置文件的优先级高于默认值。" -#: src/guides/collecting/sparrow-wallet.md:120 +#: src/guides/settings.md:10 msgid "" -"There you will find some metadata about your inscription which looks like " -"the following:" -msgstr "在那里,你会找到一些关于你铭文的元数据,如下所示:" - -#: src/guides/collecting/sparrow-wallet.md:122 -msgid "![](images/sending_01.png)" +"The path to the configuration file can be given with `--config " +"`. `ord` will error if `` doesn't exist." msgstr "" +"配置文件的路径可以通过 `--config `给出." +" 如果 `` 不存在则`ord` 会显示错误 ." -#: src/guides/collecting/sparrow-wallet.md:124 -msgid "There is a few of important things to check here:" -msgstr "以下是需要检查的几个重要事项:" - -#: src/guides/collecting/sparrow-wallet.md:125 +#: src/guides/settings.md:13 msgid "" -"The `output` identifier matches the identifier of the UTXO you are going to " -"send" -msgstr "`output` 标识符与您将要发送的UTXO的标识符匹配" +"The path to a directory containing a configuration file name named `ord." +"yaml` can be given with `--config-dir ` or `--datadir " +"` in which case the config path is `/ord." +"yaml` or `/ord.yaml`. It is not an error if it does not exist." +msgstr "" +"可以使用`--config-dir ` 或 `--datadir ` 指定包含名为ord.yaml的配置文件的目录路径。" +"在这种情况下,配置路径为`/ord.yaml`或`/ord.yaml`。如果它不存在,这不是一个错误。" -#: src/guides/collecting/sparrow-wallet.md:126 +#: src/guides/settings.md:18 msgid "" -"The `offset` of the inscription is `0` (this means that the inscription is " -"located on the first sat in the UTXO)" -msgstr "铭文的`offset`是 `0` (这意味着铭文位于UTXO的第一个sat上)" +"If none of `--config`, `--config-dir`, or `--datadir` are given, and a file " +"named `ord.yaml` exists in the default data directory, it will be loaded." +msgstr "" +"如果没有给出`--config`、`--config-dir`或`--datadir`中的任何一个," +"并且在默认数据目录中存在一个名为ord.yaml的文件,它将会被加载。" -#: src/guides/collecting/sparrow-wallet.md:127 +#: src/guides/settings.md:21 msgid "" -"the `output_value` has enough sats to cover the transaction fee (postage) " -"for sending the transaction. The exact amount you will need depends on the " -"fee rate you will select for the transaction" +"For a setting named `--setting-name` on the command line, the environment " +"variable will be named `ORD_SETTING_NAME`, and the config file field will be " +"named `setting_name`. For example, the data directory can be configured with " +"`--datadir` on the command line, the `ORD_DATA_DIR` environment variable, or " +"`data_dir` in the config file." msgstr "" -"`output_value` 有足够的sats来支付发送交易的交易费(邮资),您需要的确切金额取" -"决于您为交易选择的费率" +"对于命令行中名为`--setting-name`的设置,环境变量将被命名为`ORD_SETTING_NAME`," +"配置文件中的字段将被命名为`setting_name`。例如,数据目录可以通过命令行中的`--datadir`、" +"环境变量`ORD_DATA_DIR`或配置文件中的`data_dir`来配置。" -#: src/guides/collecting/sparrow-wallet.md:129 +#: src/guides/settings.md:27 +msgid "See `ord --help` for documentation of all the settings." +msgstr "查看`ord --help`可以获取所有设置的文档。" + +#: src/guides/settings.md:29 msgid "" -"If all of the above are true for your inscription, it should be safe for you " -"to send it using the method below." +"`ord`'s current configuration can be viewed as JSON with the `ord settings` " +"command." msgstr "" -"如果以上所有内容对于您的铭文都是正确的,那么您应该可以安全地使用以下方法发送" -"它。" +"`ord`当前的配置可以通过`ord settings`命令以JSON格式查看。" -#: src/guides/collecting/sparrow-wallet.md:131 +#: src/guides/settings.md:32 +msgid "Example Configuration" +msgstr "示例配置" + +#: src/guides/settings.md:35 msgid "" -"⚠️⚠️ Be very careful sending your inscription particularly if the `offset` " -"value is not `0`. It is not recommended to use this method if that is the " -"case, as doing so you could accidentally send your inscription to a bitcoin " -"miner unless you know what you are doing." +"```yaml\n" +"# example config\n" +"\n" +"# see `ord --help` for setting documentation\n" +"\n" +"bitcoin_data_dir: /var/lib/bitcoin\n" +"bitcoin_rpc_password: bar\n" +"bitcoin_rpc_url: https://localhost:8000\n" +"bitcoin_rpc_username: foo\n" +"chain: mainnet\n" +"commit_interval: 10000\n" +"config: /var/lib/ord/ord.yaml\n" +"config_dir: /var/lib/ord\n" +"cookie_file: /var/lib/bitcoin/.cookie\n" +"data_dir: /var/lib/ord\n" +"first_inscription_height: 100\n" +"height_limit: 1000\n" +"hidden:\n" +"- 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0\n" +"- 703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0\n" +"index: /var/lib/ord/index.redb\n" +"index_cache_size: 1000000000\n" +"index_runes: true\n" +"index_sats: true\n" +"index_spent_sats: true\n" +"index_transactions: true\n" +"integration_test: true\n" +"no_index_inscriptions: true\n" +"server_password: bar\n" +"server_url: http://localhost:8888\n" +"server_username: foo\n" +"```" msgstr "" -"⚠️⚠️ 发送铭文时要非常小心,特别是如果`offset` 值不是`0`。如果是这种情况,不建议" -"使用这种方法,否则您可能会无意中将您的雕文发送给比特币矿工,除非您知道自己在" -"做什么。" -#: src/guides/collecting/sparrow-wallet.md:133 -msgid "Sending your inscription" -msgstr "发送您的铭文" +#: src/guides/settings.md:68 +msgid "Hiding Inscription Content" +msgstr "隐藏铭文内容" -#: src/guides/collecting/sparrow-wallet.md:134 +#: src/guides/settings.md:71 msgid "" -"To send an inscription navigate to the `UTXOs` tab, and find the UTXO which " -"you previously validated contains your inscription." +"Inscription content can be selectively prevented from being served by `ord " +"server`." msgstr "" -"要发送铭文,请导航到`UTXOs`选项卡,并找到您之前验证包含您的雕文的UTXO。" +"铭文内容可以被选择性地阻止由`ord server`提供服务。" -#: src/guides/collecting/sparrow-wallet.md:136 +#: src/guides/settings.md:74 msgid "" -"If you previously froze the UXTO you will need to right-click on it and " -"unfreeze it." -msgstr "如果您之前冻结了UXTO,您将需要右键单击它并解冻它。" +"Unlike other settings, this can only be configured with the configuration " +"file or environment variables." +msgstr "" +"与其他设置不同,这只能通过配置文件或环境变量来配置。" -#: src/guides/collecting/sparrow-wallet.md:138 +#: src/guides/settings.md:77 +msgid "To hide inscriptions with an environment variable:" +msgstr "要在 ordinals.com 上隐藏铭文:" + +#: src/guides/settings.md:79 msgid "" -"Select the UTXO you want to send, and ensure that is the _only_ UTXO is " -"selected. You should see `UTXOs 1/1` in the interface. Once you are sure " -"this is the case you can hit `Send Selected`." +"```\n" +"export " +"ORD_HIDDEN='6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0 " +"703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0'\n" +"```" msgstr "" -"选择您想要发送的UTXO,并确保这是唯一选中的UTXO。在界面中,您应该看到`UTXOs " -"1/1`。确定这个后,您可以点击`Send Selected`。" -#: src/guides/collecting/sparrow-wallet.md:140 -msgid "![](images/sending_02.png)" +#: src/guides/settings.md:83 +msgid "Or with the configuration file:" +msgstr "或者使用配置文件" + +#: src/guides/settings.md:85 +msgid "" +"```yaml\n" +"hidden:\n" +"- 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0\n" +"- 703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0\n" +"```" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:142 +#: src/guides/teleburning.md:4 msgid "" -"You will then be presented with the transaction construction interface. " -"There is a few things you need to check here to make sure that this is a " -"safe send:" +"Teleburn addresses can be used to burn assets on other blockchains, leaving " +"behind in the smoking rubble a sort of forwarding address pointing to an " +"inscription on Bitcoin." msgstr "" -"然后,您将看到交易构建界面。在这里,您需要检查几件事以确保这是一个安全的发" -"送:" +"燃烧传送Teleburn地址可以用于燃烧其他区块链上的资产, 留下一个转发地址指向一个" +"比特币上的铭文这些地址就像是烟熏弹火后的废墟。 " -#: src/guides/collecting/sparrow-wallet.md:144 +#: src/guides/teleburning.md:8 msgid "" -"The transaction should have only 1 input, and this should be the UTXO with " -"the label you want to send" -msgstr "交易应该只有1个输入,这应该是您想要发送的带有标签的UTXO" +"Teleburning an asset means something like, \"I'm out. Find me on Bitcoin.\"" +msgstr "燃烧传送一个资产似乎意味着 \"我走了,在比特币链上找我。\"" -#: src/guides/collecting/sparrow-wallet.md:145 +#: src/guides/teleburning.md:10 msgid "" -"The transaction should have only 1 output, which is the address/label where " -"you want to send the inscription" -msgstr "交易应该只有1个输出,这是您想要发送铭文的地址/标签" +"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 supported. Pull requests " +"adding teleburn addresses for other chains are welcome." +msgstr "" +"Teleburn 地址源自铭文的ID,他们没有私钥,因此发往燃烧传送地址的资产将被烧毁 " +"当前只支持以太坊的燃烧地址,欢迎提交关于其他链上的燃烧传送地址的拉取请求 " -#: src/guides/collecting/sparrow-wallet.md:147 +#: src/guides/teleburning.md:15 +msgid "Ethereum" +msgstr "以太坊" + +#: src/guides/teleburning.md:18 msgid "" -"If your transaction looks any different, for example you have multiple " -"inputs, or multiple outputs then this may not be a safe transfer of your " -"inscription, and you should abandon sending until you understand more, or " -"can import into the `ord` wallet." +"Ethereum teleburn addresses are derived by taking the first 20 bytes of the " +"SHA-256 hash of the inscription ID, serialized as 36 bytes, with the first " +"32 bytes containing the transaction ID, and the last four bytes containing " +"big-endian inscription index, and interpreting it as an Ethereum address." msgstr "" -"如果您的交易看起来与此不同,例如您有多个输入或多个输出,那么这可能不是一种安" -"全的铭文传输方式,您应该放弃发送,直到您更了解或可以导入到`ord`钱包。" +"以太坊的燃烧传送teleburn地址是根据取铭文ID的SHA-256哈希的前20字节来生成的 这" +"个哈希被序列化为36字节,其中前32字节包含交易ID, 最后四个字节包含大端序的铭文" +"索引,并将其解释为一个Ethereum地址。" -#: src/guides/collecting/sparrow-wallet.md:149 +#: src/guides/teleburning.md:26 msgid "" -"You should set an appropriate transaction fee, Sparrow will usually " -"recommend a reasonable one, but you can also check [mempool.space](https://" -"mempool.space) to see what the recommended fee rate is for sending a " -"transaction." +"The ENS domain name [rodarmor.eth](https://app.ens.domains/rodarmor.eth), " +"was teleburned to [inscription zero](https://ordinals.com/" +"inscription/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0)." msgstr "" -"您应该设置合适的交易费用,Sparrow通常会推荐一个合理的费用,但您也可以查看" -"[mempool.space](https://mempool.space) 以查看发送交易的推荐费率。" +"ENS 域名 [rodarmor.eth](https://app.ens.domains/rodarmor.eth), 被燃烧传输" +"teleburned 到 [inscription zero](https://ordinals.com/" +"inscription/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0)." -#: src/guides/collecting/sparrow-wallet.md:151 +#: src/guides/teleburning.md:30 msgid "" -"You should add a label for the recipient address, a label like `alice " -"address for inscription #123` would be ideal." +"Running the inscription ID of inscription zero is " +"`6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0`." msgstr "" -"您应该为收件人地址添加一个标签,如`alice address for inscription #123`就很理" -"想。" +"零号铭文的铭文ID是" +"`6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0`." -#: src/guides/collecting/sparrow-wallet.md:153 +#: src/guides/teleburning.md:33 msgid "" -"Once you have checked the transaction is a safe transaction using the checks " -"above, and you are confident to send it you can click `Create Transaction`." +"Passing `6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0` " +"to the teleburn command:" msgstr "" -"在使用上述检查确认交易是安全的交易,并且有信心发送它后,您可以点击`Create " -"Transaction`。" +"使用teleburn命令 " +"`6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0` " -#: src/guides/collecting/sparrow-wallet.md:155 -msgid "![](images/sending_03.png)" +#: src/guides/teleburning.md:36 +msgid "" +"```bash\n" +"$ ord teleburn " +"6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0\n" +"```" msgstr "" -#: src/guides/collecting/sparrow-wallet.md:157 +#: src/guides/teleburning.md:40 +msgid "Returns:" +msgstr "返回" + +#: src/guides/teleburning.md:42 msgid "" -"Here again you can double check that your transaction looks safe, and once " -"you are confident you can click `Finalize Transaction for Signing`." +"```json\n" +"{\n" +" \"ethereum\": \"0xe43A06530BdF8A4e067581f48Fae3b535559dA9e\"\n" +"}\n" +"```" msgstr "" -"在这里,您可以再次确认您的交易是否安全,在确认后,您可以点击`Finalize " -"Transaction for Signing`。" -#: src/guides/collecting/sparrow-wallet.md:159 -msgid "![](images/sending_04.png)" +#: src/guides/teleburning.md:48 +msgid "" +"Indicating that `0xe43A06530BdF8A4e067581f48Fae3b535559dA9e` is the Ethereum " +"teleburn address for inscription zero, which is, indeed, the current owner, " +"on Ethereum, of `rodarmor.eth`." msgstr "" +"显示出 `0xe43A06530BdF8A4e067581f48Fae3b535559dA9e` 是零号铭文的 teleburn地" +"址 ,事实上,它是在Ethereum上的`rodarmor.eth`的当前所有者" -#: src/guides/collecting/sparrow-wallet.md:161 -msgid "Here you can triple check everything before hitting `Sign`." -msgstr "在这里,你可以在点击`Sign`之前再次确认所有内容。" +#: src/guides/testing.md:4 +msgid "Test Environment" +msgstr "测试环境" -#: src/guides/collecting/sparrow-wallet.md:163 -msgid "![](images/sending_05.png)" +#: src/guides/testing.md:7 +msgid "" +"`ord env ` creates a test environment in ``, spins up " +"`bitcoind` and `ord server` instances, prints example commands for " +"interacting with the test `bitcoind` and `ord server` instances, waits for " +"`CTRL-C`, and then shuts down `bitcoind` and `ord server`." msgstr "" +"`ord env `在``中创建一个测试环境,启动`bitcoind`和`ord server`实例," +"打印与测试`bitcoind`和`ord server`实例交互的示例命令,等待`CTRL-C`,然后关闭`bitcoind`和`ord server`。" -#: src/guides/collecting/sparrow-wallet.md:165 +#: src/guides/testing.md:12 msgid "" -"And then actually you get very very last chance to check everything before " -"hitting `Broadcast Transaction`. Once you broadcast the transaction it is " -"sent to the bitcoin network, and starts being propagated into the mempool." +"`ord env` tries to use port 9000 for `bitcoind`'s RPC interface, and port " +"`9001` for `ord`'s RPC interface, but will fall back to random unused ports." msgstr "" -"然后实际上在点击`Broadcast Transaction`之前,你有最后一次检查所有内容的机会。" -"一旦你广播交易,它就会被发送到比特币网络,并开始在mempool中传播。" +"`ord env`尝试使用端口9000作为`bitcoind`的RPC接口,以及端口`9001`作为`ord`的RPC接口," +"但如果这些端口被占用,它将回退到随机的未使用端口。" -#: src/guides/collecting/sparrow-wallet.md:167 -msgid "![](images/sending_06.png)" +#: src/guides/testing.md:15 +msgid "" +"Inside of the env directory, `ord env` will write `bitcoind`'s configuration " +"to `bitcoin.conf`, `ord`'s configuration to `ord.yaml`, and the env " +"configuration to `env.json`." msgstr "" +"在env目录内部,`ord env`将会将`bitcoind`的配置写入`bitcoin.conf`," +"`ord`的配置写入`ord.yaml`,以及环境配置写入`env.json`。" -#: src/guides/collecting/sparrow-wallet.md:169 +#: src/guides/testing.md:19 msgid "" -"If you want to track the status of your transaction you can copy the " -"`Transaction Id (Txid)` and paste that into [mempool.space](https://mempool." -"space)" +"`env.json` contains the commands needed to invoke `bitcoin-cli` and `ord " +"wallet`, as well as the ports `bitcoind` and `ord server` are listening on." msgstr "" -"如果你想跟踪你的交易状态,你可以复制`Transaction Id (Txid)`并粘贴到[mempool." -"space](https://mempool.space)" +"`env.json`包含了调用`bitcoin-cli`和`ord wallet`所需的命令," +"以及`bitcoind`和`ord server`正在监听的端口信息。" -#: src/guides/collecting/sparrow-wallet.md:171 +#: src/guides/testing.md:22 +msgid "These can be extracted into shell commands using `jq`:" +msgstr "这些可以使用`jq`提取成shell命令:" + +#: src/guides/testing.md:24 msgid "" -"Once the transaction has confirmed you can check the inscription page on " -"[ordinals.com](https://ordinals.com) to validate that it has moved to the " -"new output location and address." +"```shell\n" +"bitcoin=`jq -r '.bitcoin_cli_command | join(\" \")' env/env.json`\n" +"$bitcoin listunspent\n" +"\n" +"ord=`jq -r '.ord_wallet_command | join(\" \")' env/env.json`\n" +"$ord outputs\n" +"```" msgstr "" -"一旦交易确认,你可以在[ordinals.com](https://ordinals.com) 的铭文页面上验证它" -"是否已移动到新的输出位置和地址。" -#: src/guides/collecting/sparrow-wallet.md:175 +#: src/guides/testing.md:32 msgid "" -"Sparrow wallet is not showing a transaction/UTXO, but I can see it on " -"mempool.space!" -msgstr "Sparrow钱包没有显示交易/UTXO,但我在mempool.space上看到了" +"If `ord` is in the `$PATH` and the env directory is `env`, the `bitcoin-cli` " +"command will be:" +msgstr "" +"如果`ord`在`$PATH`中,并且环境目录是`env`,那么`bitcoin-cli`命令将会是:" -#: src/guides/collecting/sparrow-wallet.md:177 +#: src/guides/testing.md:35 msgid "" -"Make sure that your wallet is connected to a bitcoin node. To validate this, " -"head into the `Preferences`\\-> `Server` settings, and click `Edit Existing " -"Connection`." +"```\n" +"bitcoin-cli -datadir=env`\n" +"```" msgstr "" -"确保你的钱包连接到一个比特币节点。要验证这一点,转到`Preferences`\\-> " -"`Server` 设置,并点击 `Edit Existing Connection`。" -#: src/guides/collecting/sparrow-wallet.md:179 -msgid "![](images/troubleshooting_01.png)" -msgstr "" +#: src/guides/testing.md:39 +msgid "And the `ord` will be:" +msgstr "`ord`将是" -#: src/guides/collecting/sparrow-wallet.md:181 +#: src/guides/testing.md:41 msgid "" -"From there you can select a node and click `Test Connection` to validate " -"that Sparrow is able to connect successfully." +"```\n" +"ord --datadir env\n" +"```" msgstr "" -"从那里你可以选择一个节点并点击 `Test Connection` 来验证Sparrow是否能够成功连" -"接。" -#: src/guides/collecting/sparrow-wallet.md:183 -msgid "![](images/troubleshooting_02.png)" -msgstr "" +#: src/guides/testing.md:45 +msgid "Test Networks" +msgstr "测试网络" -#: src/guides/testing.md:4 +#: src/guides/testing.md:48 msgid "" "Ord can be tested using the following flags to specify the test network. For " "more information on running Bitcoin Core for testing, see [Bitcoin's " @@ -4838,133 +5976,145 @@ msgstr "" "息,请参见[比特币的开发者文档](https://developer.bitcoin.org/examples/" "testing。" -#: src/guides/testing.md:7 +#: src/guides/testing.md:51 msgid "" -"Most `ord` commands in [inscriptions](inscriptions.md) and [explorer]" -"(explorer.md) can be run with the following network flags:" +"Most `ord` commands in [wallet](wallet.md) and [explorer](explorer.md) can " +"be run with the following network flags:" msgstr "" -"大多数在[铭文](inscriptions.md) 和 [浏览器](explorer.md) 中的 `ord`命令可以使" +"大多数在[钱包](wallet.md) 和 [浏览器](explorer.md) 中的 `ord`命令可以使" "用以下网络标志运行:" -#: src/guides/testing.md:10 +#: src/guides/testing.md:54 msgid "Network" -msgstr "" +msgstr "网络" -#: src/guides/testing.md:10 +#: src/guides/testing.md:54 msgid "Flag" -msgstr "" +msgstr "标记" -#: src/guides/testing.md:12 +#: src/guides/testing.md:56 msgid "Testnet" -msgstr "" +msgstr "测试网" -#: src/guides/testing.md:12 +#: src/guides/testing.md:56 msgid "`--testnet` or `-t`" msgstr "" -#: src/guides/testing.md:13 +#: src/guides/testing.md:57 msgid "Signet" msgstr "" -#: src/guides/testing.md:13 +#: src/guides/testing.md:57 msgid "`--signet` or `-s`" msgstr "" -#: src/guides/testing.md:14 +#: src/guides/testing.md:58 msgid "Regtest" msgstr "" -#: src/guides/testing.md:14 +#: src/guides/testing.md:58 msgid "`--regtest` or `-r`" msgstr "" -#: src/guides/testing.md:16 +#: src/guides/testing.md:60 msgid "Regtest doesn't require downloading the blockchain or indexing ord." msgstr "Regtest不需要下载区块链或者建立ord索引" -#: src/guides/testing.md:21 -msgid "Run bitcoind in regtest with:" +#: src/guides/testing.md:65 +msgid "Run `bitcoind` in regtest with:" msgstr "在regtest里运行bitcoind,使用:" -#: src/guides/testing.md:22 +#: src/guides/testing.md:67 msgid "" "```\n" "bitcoind -regtest -txindex\n" "```" msgstr "" -#: src/guides/testing.md:25 +#: src/guides/testing.md:71 +msgid "Run `ord server` in regtest with:" +msgstr "在regtest里运行bitcoind,使用:" + +#: src/guides/testing.md:73 +msgid "" +"```\n" +"ord --regtest server\n" +"```" +msgstr "" + +#: src/guides/testing.md:77 msgid "Create a wallet in regtest with:" msgstr "在regtest里创建钱包" -#: src/guides/testing.md:26 +#: src/guides/testing.md:79 msgid "" "```\n" -"ord -r wallet create\n" +"ord --regtest wallet create\n" "```" msgstr "" -#: src/guides/testing.md:29 +#: src/guides/testing.md:83 msgid "Get a regtest receive address with:" msgstr "创建一个regtest接收地址" -#: src/guides/testing.md:30 +#: src/guides/testing.md:85 msgid "" "```\n" -"ord -r wallet receive\n" +"ord --regtest wallet receive\n" "```" msgstr "" -#: src/guides/testing.md:33 +#: src/guides/testing.md:89 msgid "Mine 101 blocks (to unlock the coinbase) with:" msgstr "挖取101个区块(解锁coinbase)使用:" -#: src/guides/testing.md:34 +#: src/guides/testing.md:91 msgid "" "```\n" "bitcoin-cli -regtest generatetoaddress 101 \n" "```" msgstr "" -#: src/guides/testing.md:37 +#: src/guides/testing.md:95 msgid "Inscribe in regtest with:" msgstr "在regtest上铭刻" -#: src/guides/testing.md:38 +#: src/guides/testing.md:97 msgid "" "```\n" -"ord -r wallet inscribe --fee-rate 1 --file \n" +"ord --regtest wallet inscribe --fee-rate 1 --file \n" "```" msgstr "" -#: src/guides/testing.md:41 +#: src/guides/testing.md:101 msgid "Mine the inscription with:" msgstr "挖取铭文" -#: src/guides/testing.md:42 +#: src/guides/testing.md:103 msgid "" "```\n" "bitcoin-cli -regtest generatetoaddress 1 \n" "```" msgstr "" -#: src/guides/testing.md:45 -msgid "View the inscription in the regtest explorer:" -msgstr "在regtest浏览器里查看铭文" +#: src/guides/testing.md:107 +msgid "" +"By default, browsers don't support compression over HTTP. To test compressed " +"content over HTTP, use the `--decompress` flag:" +msgstr "" -#: src/guides/testing.md:46 +#: src/guides/testing.md:110 msgid "" "```\n" -"ord -r server\n" +"ord --regtest server --decompress\n" "```" msgstr "" -#: src/guides/testing.md:50 +#: src/guides/testing.md:114 msgid "Testing Recursion" msgstr "测试递归" -#: src/guides/testing.md:53 -#, fuzzy +#: src/guides/testing.md:117 msgid "" "When testing out [recursion](../inscriptions/recursion.md), inscribe the " "dependencies first (example with [p5.js](https://p5js.org)):" @@ -4972,251 +6122,80 @@ msgstr "" "测试 [recursion](../inscriptions/recursion.md) 时,首先记下依赖项(以 [p5.js]" "(https://p5js.org) 为例:" -#: src/guides/testing.md:55 +#: src/guides/testing.md:120 msgid "" "```\n" -"ord -r wallet inscribe --fee-rate 1 --file p5.js\n" +"ord --regtest wallet inscribe --fee-rate 1 --file p5.js\n" "```" msgstr "" -#: src/guides/testing.md:58 +#: src/guides/testing.md:124 msgid "" -"This should return a `inscription_id` which you can then reference in your " -"recursive inscription." +"This will return the inscription ID of the dependency which you can then " +"reference in your inscription." msgstr "这应该返回一个`inscription_id`,然后您可以在递归铭文中引用它。" -#: src/guides/testing.md:61 +#: src/guides/testing.md:127 msgid "" -"ATTENTION: These ids will be different when inscribing on mainnet or signet, " -"so be sure to change those in your recursive inscription for each chain." +"However, inscription IDs differ between mainnet and test chains, so you must " +"change the inscription IDs in your inscription to the mainnet inscription " +"IDs of your dependencies before making the final inscription on mainnet." msgstr "" -"请注意,在主网和signet上铭刻的时候这些id有所不同,因此请务必更改每个链的递归" -"铭文中的内容。" +"然而,铭文ID在主网和测试链之间是不同的,因此在在主网上进行最终铭文之前," +"你必须将你铭文中的铭文ID更改为你依赖项的主网铭文ID。" -#: src/guides/testing.md:65 +#: src/guides/testing.md:131 msgid "Then you can inscribe your recursive inscription with:" msgstr "现在你可以使用以下命令来铭刻你的递归铭文:" -#: src/guides/testing.md:66 +#: src/guides/testing.md:133 msgid "" "```\n" -"ord -r wallet inscribe --fee-rate 1 --file recursive-inscription.html\n" +"ord --regtest wallet inscribe --fee-rate 1 --file recursive-inscription." +"html\n" "```" msgstr "" -#: src/guides/testing.md:69 +#: src/guides/testing.md:137 msgid "Finally you will have to mine some blocks and start the server:" msgstr "最终你可以挖取一些区块来开始服务器:" -#: src/guides/testing.md:70 +#: src/guides/testing.md:139 msgid "" "```\n" "bitcoin-cli generatetoaddress 6 \n" -"ord -r server\n" -"```" -msgstr "" - -#: src/guides/moderation.md:4 -msgid "" -"`ord` includes a block explorer, which you can run locally with `ord server`." -msgstr "`ord` 包含了一个区块浏览器,你可以在本地运行`ord server`." - -#: src/guides/moderation.md:6 -msgid "" -"The block explorer allows viewing inscriptions. Inscriptions are user-" -"generated content, which may be objectionable or unlawful." -msgstr "" -"区块浏览器允许查看铭文。铭文是用户生成的内容,因此可能令人反感或非法的。" - -#: src/guides/moderation.md:9 -msgid "" -"It is the responsibility of each individual who runs an ordinal block " -"explorer instance to understand their responsibilities with respect to " -"unlawful content, and decide what moderation policy is appropriate for their " -"instance." -msgstr "" -"运行ord区块浏览器实例的每个人都有责任了解他们对非法内容的责任,并决定适合他们" -"实例的审核政策。" - -#: src/guides/moderation.md:13 -msgid "" -"In order to prevent particular inscriptions from being displayed on an `ord` " -"instance, they can be included in a YAML config file, which is loaded with " -"the `--config` option." -msgstr "" -"为了防止特定的铭文显示在`ord`实例上,它们可以包含在 YAML 配置文件中,该文件使" -"用 `--config`选项加载。" - -#: src/guides/moderation.md:17 -msgid "" -"To hide inscriptions, first create a config file, with the inscription ID " -"you want to hide:" -msgstr "要隐藏铭文,首先创建一个配置文件,其中包含要隐藏的铭文 ID:" - -#: src/guides/moderation.md:20 -msgid "" -"```yaml\n" -"hidden:\n" -"- 0000000000000000000000000000000000000000000000000000000000000000i0\n" "```" msgstr "" -#: src/guides/moderation.md:25 -msgid "" -"The suggested name for `ord` config files is `ord.yaml`, but any filename " -"can be used." -msgstr "`ord` 配置文件的建议名称是 `ord.yaml`,但可以使用任何文件名。" - -#: src/guides/moderation.md:28 -msgid "Then pass the file to `--config` when starting the server:" -msgstr "然后将文件在服务启动的使用使用 `--config` :" - -#: src/guides/moderation.md:30 -msgid "`ord --config ord.yaml server`" -msgstr "" - -#: src/guides/moderation.md:32 -msgid "" -"Note that the `--config` option comes after `ord` but before the `server` " -"subcommand." -msgstr "请注意, `--config` 选项的位置在 `ord` 之后但是在 `server`子命令前。" - -#: src/guides/moderation.md:35 -msgid "`ord` must be restarted in to load changes to the config file." -msgstr "`ord` 必须重启才可以加载在配置文件中的更改。" - -#: src/guides/moderation.md:37 -msgid "`ordinals.com`" -msgstr "" - -#: src/guides/moderation.md:40 -msgid "" -"The `ordinals.com` instances use `systemd` to run the `ord server` service, " -"which is called `ord`, with a config file located at `/var/lib/ord/ord.yaml`." -msgstr "" -"`ordinals.com` 实例使用 `systemd` 运行名为 `ord`的 `ord server` 服务,配置文" -"件在 `/var/lib/ord/ord.yaml`." - -#: src/guides/moderation.md:43 -msgid "To hide an inscription on `ordinals.com`:" -msgstr "要在 ordinals.com 上隐藏铭文:" - -#: src/guides/moderation.md:45 -msgid "SSH into the server" -msgstr "使用SSH登陆服务器" - -#: src/guides/moderation.md:46 -msgid "Add the inscription ID to `/var/lib/ord/ord.yaml`" -msgstr "在 `/var/lib/ord/ord.yaml`中增加铭文ID" - -#: src/guides/moderation.md:47 -msgid "Restart the service with `systemctl restart ord`" -msgstr "使用 `systemctl restart ord` 重启服务" - -#: src/guides/moderation.md:48 -msgid "Monitor the restart with `journalctl -u ord`" -msgstr "通过 `journalctl -u ord` 重启" - -#: src/guides/moderation.md:50 -msgid "" -"Currently, `ord` is slow to restart, so the site will not come back online " -"immediately." -msgstr "目前,ord 重启速度较慢,因此站点不会立即恢复在线。" - -#: src/guides/reindexing.md:4 -msgid "" -"Sometimes the `ord` database must be reindexed, which means deleting the " -"database and restarting the indexing process with either `ord index update` " -"or `ord server`. Reasons to reindex are:" -msgstr "" -"有时必须重新索引‘ord’数据库,这意味着删除数据库并使用 `ord index update`或" -"`ord server`来重新索引数据库。重新索引的原因是:" - -#: src/guides/reindexing.md:8 -msgid "A new major release of ord, which changes the database scheme" -msgstr "ord 发布新的主要版本,更改了数据库架构" - -#: src/guides/reindexing.md:9 -msgid "The database got corrupted somehow" -msgstr "数据库可能会损坏" +#: src/guides/testing.md:143 +msgid "Mainnet Dependencies" +msgstr "主网依赖" -#: src/guides/reindexing.md:11 +#: src/guides/testing.md:145 msgid "" -"The database `ord` uses is called [redb](https://github.com/cberner/redb), " -"so we give the index the default file name `index.redb`. By default we store " -"this file in different locations depending on your operating system." -msgstr "" -"`ord` 使用的数据库称为 [redb](https://github.com/cberner/redb),所以我们为索" -"引指定默认文件名‘index.redb’。默认情况下我们存储根据您的操作系统,此文件位于" -"不同的位置。" - -#: src/guides/reindexing.md:15 -msgid "Platform" -msgstr "平台" - -#: src/guides/reindexing.md:15 -msgid "Value" -msgstr "" - -#: src/guides/reindexing.md:17 -msgid "Linux" -msgstr "" - -#: src/guides/reindexing.md:17 -msgid "`$XDG_DATA_HOME`/ord or `$HOME`/.local/share/ord" -msgstr "" - -#: src/guides/reindexing.md:17 -msgid "/home/alice/.local/share/ord" -msgstr "" - -#: src/guides/reindexing.md:18 -msgid "macOS" -msgstr "" - -#: src/guides/reindexing.md:18 -msgid "`$HOME`/Library/Application Support/ord" -msgstr "" - -#: src/guides/reindexing.md:18 -msgid "/Users/Alice/Library/Application Support/ord" -msgstr "" - -#: src/guides/reindexing.md:19 -msgid "Windows" -msgstr "" - -#: src/guides/reindexing.md:19 -msgid "`{FOLDERID_RoamingAppData}`\\\\ord" -msgstr "" - -#: src/guides/reindexing.md:19 -msgid "C:\\Users\\Alice\\AppData\\Roaming\\ord" +"To avoid having to change dependency inscription IDs to mainnet inscription " +"IDs, you may utilize a content proxy when testing. `ord server` accepts a `--" +"content-proxy` option, which takes the URL of a another `ord server` " +"instance. When making a request to `/content/` when a " +"content proxy is set and the inscription is not found, `ord server` will " +"forward the request to the content proxy. This allows you to run a test `ord " +"server` instance with a mainnet content proxy. You can then use mainnet " +"inscription IDs in your test inscription, which will then return the content " +"of the mainnet inscriptions." msgstr "" +"为了避免在测试时必须将依赖铭文ID更改为主网铭文ID,你可以在测试时使用内容代理。" +"`ord server`接受一个`--content-proxy`选项,它需要另一个`ord server`实例的URL。" +"当设置了内容代理并且铭文未找到时,向`/content/`发出请求," +"`ord server`将会将请求转发给内容代理。这允许你运行一个带有主网内容代理的测试`ord server`实例。" +"然后你可以在测试铭文中使用主网铭文ID,这将返回主网铭文的内容。" -#: src/guides/reindexing.md:21 -msgid "" -"So to delete the database and reindex on MacOS you would have to run the " -"following commands in the terminal:" -msgstr "因此,要在 MacOS 上删除数据库并重新索引,您必须在终端中执行以下命令:" - -#: src/guides/reindexing.md:24 +#: src/guides/testing.md:155 msgid "" -"```bash\n" -"rm ~/Library/Application Support/ord/index.redb\n" -"ord index update\n" +"```\n" +"ord --regtest server --content-proxy https://ordinals.com\n" "```" msgstr "" -#: src/guides/reindexing.md:29 -msgid "" -"You can of course also set the location of the data directory yourself with " -"`ord --data-dir index update` or give it a specific filename and path " -"with `ord --index index update`." -msgstr "" -"您当然也可以自己设置数据目录的位置,`ord --data-dir index update` 或为其" -"指定特定的文件名和路径,使用‘ord --index 索引运行’。" - #: src/bounties.md:1 msgid "Ordinal Bounty Hunting Hints" msgstr "Ordinals赏金计划提示" @@ -5420,7 +6399,7 @@ msgstr "" #: src/bounty/3.md:14 msgid "" "The bounty is open for submissions until block 840000—the first block after " -"the fourth halvening. Submissions included in block 840000 or later will not " +"the fourth halving. Submissions included in block 840000 or later will not " "be considered." msgstr "" "赏金计划开放到区块高度840000-第四次减半后的第一个区块。区块高度840000以及以后" @@ -5545,6 +6524,34 @@ msgstr "" msgid "Unclaimed!" msgstr "仍然有效!" +#~ msgid "" +#~ "[Ordinal Art: Mint Your own NFTs on Bitcoin w/ @rodarmor](https://www." +#~ "youtube.com/watch?v=j5V33kV3iqo)" +#~ msgstr "" +#~ "[序数艺术:在比特币上铸造你自己的NFT w/ @rodarmor](https://www.youtube." +#~ "com/watch?v=j5V33kV3iqo)" + +#~ msgid "" +#~ "A few other endpoints that inscriptions may access are the following:" +#~ msgstr "铭文可以访问的其他几个端点如下:" + +#~ msgid "To test how your inscriptions will look you can run:" +#~ msgstr "测试你的铭文你可以运行:" + +#~ msgid "Ordinal Inscription Guide" +#~ msgstr "铭文指引" + +#~ msgid "View the inscription in the regtest explorer:" +#~ msgstr "在regtest浏览器里查看铭文" + +#~ msgid "" +#~ "ATTENTION: These ids will be different when inscribing on mainnet or " +#~ "signet, so be sure to change those in your recursive inscription for each " +#~ "chain." +#~ msgstr "" +#~ "请注意,在主网和signet上铭刻的时候这些id有所不同,因此请务必更改每个链的递" +#~ "归铭文中的内容。" + #~ msgid "`uncommon`: 745,855" #~ msgstr "`非普通`: 745,855" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 902d3fafb7..5700fdc31a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -11,6 +11,8 @@ Summary - [Provenance](inscriptions/provenance.md) - [Recursion](inscriptions/recursion.md) - [Rendering](inscriptions/rendering.md) +- [Runes](runes.md) + - [Specification](runes/specification.md) - [FAQ](faq.md) - [Contributing](contributing.md) - [Donate](donate.md) diff --git a/docs/src/guides/batch-inscribing.md b/docs/src/guides/batch-inscribing.md index a20706200b..9ccb539cb0 100644 --- a/docs/src/guides/batch-inscribing.md +++ b/docs/src/guides/batch-inscribing.md @@ -11,7 +11,7 @@ To create a batch inscription using a batchfile in `batch.yaml`, run the following command: ```bash -ord wallet inscribe --fee-rate 21 --batch batch.yaml +ord wallet batch --fee-rate 21 --batch batch.yaml ``` Example `batch.yaml` diff --git a/docs/src/guides/reindexing.md b/docs/src/guides/reindexing.md index 97aaba20e9..093d8b1c78 100644 --- a/docs/src/guides/reindexing.md +++ b/docs/src/guides/reindexing.md @@ -27,5 +27,5 @@ ord index update ``` You can of course also set the location of the data directory yourself with `ord ---data-dir index update` or give it a specific filename and path with `ord +--datadir index update` or give it a specific filename and path with `ord --index index update`. diff --git a/docs/src/guides/settings.md b/docs/src/guides/settings.md index ba6f893199..ca9e15c771 100644 --- a/docs/src/guides/settings.md +++ b/docs/src/guides/settings.md @@ -11,17 +11,17 @@ The path to the configuration file can be given with `--config `. `ord` will error if `` doesn't exist. The path to a directory containing a configuration file name named `ord.yaml` -can be given with `--config-dir ` or `--data-dir +can be given with `--config-dir ` or `--datadir ` in which case the config path is `/ord.yaml` or `/ord.yaml`. It is not an error if it does not exist. -If none of `--config`, `--config-dir`, or `--data-dir` are given, and a file +If none of `--config`, `--config-dir`, or `--datadir` are given, and a file named `ord.yaml` exists in the default data directory, it will be loaded. For a setting named `--setting-name` on the command line, the environment variable will be named `ORD_SETTING_NAME`, and the config file field will be named `setting_name`. For example, the data directory can be configured with -`--data-dir` on the command line, the `ORD_DATA_DIR` environment variable, or +`--datadir` on the command line, the `ORD_DATA_DIR` environment variable, or `data_dir` in the config file. See `ord --help` for documentation of all the settings. diff --git a/docs/src/guides/testing.md b/docs/src/guides/testing.md index f1f1fa070c..9218c7e1a1 100644 --- a/docs/src/guides/testing.md +++ b/docs/src/guides/testing.md @@ -39,7 +39,7 @@ bitcoin-cli -datadir=env` And the `ord` will be: ``` -ord --data-dir env +ord --datadir env ``` Test Networks diff --git a/docs/src/guides/wallet.md b/docs/src/guides/wallet.md index 6e9b30d9a1..bf6bf204b0 100644 --- a/docs/src/guides/wallet.md +++ b/docs/src/guides/wallet.md @@ -386,6 +386,41 @@ running: ord wallet inscriptions ``` +Sending Runes +------------- + +Ask the recipient to generate a new address by running: + +``` +ord wallet receive +``` + +Send the runes by running: + +``` +ord wallet send --fee-rate
        +``` + +Where `RUNES_AMOUNT` is the number of runes to send, a `:` character, and the +name of the rune. For example if you want to send 1000 of the EXAMPLE rune, you +would use `1000:EXAMPLE`. + +``` +ord wallet send --fee-rate 1 SOME_ADDRESS 1000:EXAMPLE +``` + +See the pending transaction with: + +``` +ord wallet transactions +``` + +Once the send transaction confirms, the recipient can confirm receipt with: + +``` +ord wallet balance +``` + Receiving Inscriptions ---------------------- diff --git a/docs/src/inscriptions/delegate.md b/docs/src/inscriptions/delegate.md index 51b17ab30a..534173be0d 100644 --- a/docs/src/inscriptions/delegate.md +++ b/docs/src/inscriptions/delegate.md @@ -36,4 +36,4 @@ OP_ENDIF Note that the value of tag `11` is decimal, not hex. The delegate field value uses the same encoding as the parent field. See -[provenance](provenance.md) for more examples of inscrpition ID encodings; +[provenance](provenance.md) for more examples of inscription ID encodings; diff --git a/docs/src/runes.md b/docs/src/runes.md new file mode 100644 index 0000000000..31b21cf4f9 --- /dev/null +++ b/docs/src/runes.md @@ -0,0 +1,160 @@ +Runes +===== + +Runes allow Bitcoin transactions to etch, mint, and transfer Bitcoin-native +digital commodities. + +Whereas every inscription is unique, every unit of a rune is the same. They are +interchangeable tokens, fit for a variety of purposes. + +Runestones +---------- + +Rune protocol messages, called runestones, are stored in Bitcoin transaction +outputs. + +A runestone output's script pubkey begins with an `OP_RETURN`, followed by +`OP_13`, followed by zero or more data pushes. These data pushes are +concatenated and decoded into a sequence of 128-bit integers, and finally +parsed into a runestone. + +A transaction may have at most one runestone. + +A runestone may etch a new rune, mint an existing rune, and transfer runes from +a transaction's inputs to its outputs. + +A transaction output may hold balances of any number of runes. + +Runes are identified by IDs, which consist of the block in which a rune was +etched and the index of the etching transaction within that block, represented +in text as `BLOCK:TX`. For example, the ID of the rune minted the 20th +transaction of the 500th block is `500:20`. + +Etching +------- + +Runes come into existence by being etched. Etching creates a rune and sets its +properties. Once set, these properties are immutable, even to its etcher. + +### Name + +Names consist of the letters A through Z and are between one and twenty-eight +characters long. For example `UNCOMMONGOODS` is a rune name. + +Names may contain spacers, represented as bullets, to aid readability. +`UNCOMMONGOODS` might be etched as `UNCOMMON•GOODS`. + +The uniqueness of a name does not depend on spacers. Thus, a rune may not be +etched with the same sequence of letters as an existing rune, even if it has +different spacers. + +### Divisibility + +A rune's divisibility is how finely it may be divided into its atomic units. +Divisibility is expressed as the number of digits permissible after the decimal +point in an amount of runes. A rune with divisibility 0 may not be divided. A +unit of a rune with divisibility 1 may be divided into ten sub-units, a rune +with divisibility 2 may be divided into a hundred, and so on. + +### Symbol + +A rune's currency symbol is a single Unicode code point, for example `$`, `⧉`, +or `🧿`, displayed after quantities of that rune. + +101 atomic units of a rune with divisibility 2 and symbol `🧿` would be +rendered as `1.01 🧿`. + +If a rune does not have a symbol, the generic currency sign `¤`, also called a +scarab, should be used. + +### Premine + +The etcher of a rune may optionally allocate to themselves units of the rune +being etched. This allocation is called a premine. + +### Terms + +A rune may have an open mint, allowing anyone to create and allocate units of +that rune for themselves. An open mint is subject to terms, which are set upon +etching. + +A mint is open while all terms of the mint are satisfied, and closed when any +of them are not. For example, a mint may be limited to a starting height, an +ending height, and a cap, and will be open between the starting height and +ending height, or until the cap is reached, whichever comes first. + +#### Cap + +The number of times a rune may be minted is its cap. A mint is closed once the +cap is reached. + +#### Amount + +Each mint transaction creates a fixed amount of new units of a rune. + +#### Start Height + +A mint is open starting in the block with the given start height. + +#### End Height + +A rune may not be minted in or after the block with the given end height. + +#### Start Offset + +A mint is open starting in the block whose height is equal to the start offset +plus the height of the block in which the rune was etched. + +#### End Offset + +A rune may not be minted in or after the block whose height is equal to the end +offset plus the height of the block in which the rune was etched. + +Minting +------- + +While a rune's mint is open, anyone may create a mint transaction that creates +a fixed amount of new units of that rune, subject to the terms of the mint. + +Transferring +------------ + +When transaction inputs contain runes, or new runes are created by a premine or +mint, those runes are transferred to that transaction's outputs. A +transaction's runestone may change how input runes transfer to outputs. + +### Edicts + +A runestone may contain any number of edicts. Edicts consist of a rune ID, an +amount, and an output number. Edicts are processed in order, allocating +unallocated runes to outputs. + +### Pointer + +After all edicts are processed, remaining unallocated runes are transferred to +the transaction's first non-`OP_RETURN` output. A runestone may optionally +contain a pointer that specifies an alternative default output. + +### Burning + +Runes may be burned by transferring them to an `OP_RETURN` output with an edict +or pointer. + +Cenotaphs +--------- + +Runestones may be malformed for a number of reasons, including non-pushdata +opcodes in the runestone `OP_RETURN`, invalid varints, or unrecognized +runestone fields. + +Malformed runestones are termed +[cenotaphs](https://en.wikipedia.org/wiki/Cenotaph). + +Runes input to a transaction with a cenotaph are burned. Runes etched in a +transaction with a cenotaph are set as unmintable. Mints in a transaction with +a cenotaph count towards the mint cap, but the minted runes are burned. + +Cenotaphs are an upgrade mechanism, allowing runestones to be given new +semantics that change how runes are created and transferred, while not +misleading unupgraded clients as to the location of those runes, as unupgraded +clients will see those runes as having been burned. diff --git a/docs/src/runes/specification.md b/docs/src/runes/specification.md new file mode 100644 index 0000000000..b247790bf2 --- /dev/null +++ b/docs/src/runes/specification.md @@ -0,0 +1,526 @@ +Runes Does Not Have a Specification +=================================== + +The Runes reference implementation, `ord`, is the normative specification of +the Runes protocol. + +Nothing you read here or elsewhere, aside from the code of `ord`, is a +specification. This prose description of the runes protocol is provided as a +guide to the behavior of `ord`, and the code of `ord` itself should always be +consulted to confirm the correctness of any prose description. + +If, due to a bug in `ord`, this document diverges from the actual behavior of +`ord` and it is impractically disruptive to change `ord`'s behavior, this +document will be amended to agree with `ord`'s actual behavior. + +Users of alternative implementations do so at their own risk, and services +wishing to integrate Runes are strongly encouraged to use `ord` itself to make +Runes transactions, and to determine the state of runes, mints, and balances. + +Runestones +---------- + +Rune protocol messages are termed "runestones". + +The Runes protocol activates on block 840,000. Runestones in earlier blocks are +ignored. + +Abstractly, runestones contain the following fields: + +```rust +struct Runestone { + edicts: Vec, + etching: Option, + mint: Option, + pointer: Option, +} +``` + +Runes are created by etchings: + +```rust +struct Etching { + divisibility: Option, + premine: Option, + rune: Option, + spacers: Option, + symbol: Option, + terms: Option, +} +``` + +Which may contain mint terms: + +```rust +struct Terms { + amount: Option, + cap: Option, + height: (Option, Option), + offset: (Option, Option), +} +``` + +Runes are transferred by edict: + +```rust +struct Edict { + id: RuneId, + amount: u128, + output: u32, +} +``` + +Rune IDs are encoded as the block height and transaction index of the +transaction in which the rune was etched: + +```rust +struct RuneId { + block: u64, + tx: u32, +} +``` + +Rune IDs are represented in text as `BLOCK:TX`. + +Rune names are encoded as modified base-26 integers: + +```rust +struct Rune(u128); +``` + +### Deciphering + +Runestones are deciphered from transactions with the following steps: + +1. Find the first transaction output whose script pubkey begins with `OP_RETURN + OP_13`. + +2. Concatenate all following data pushes into a payload buffer. + +3. Decode a sequence 128-bit [LEB128](https://en.wikipedia.org/wiki/LEB128) + integers from the payload buffer. + +4. Parse the sequence of integers into an untyped message. + +5. Parse the untyped message into a runestone. + +Deciphering may produce a malformed runestone, termed a +[cenotaph](https://en.wikipedia.org/wiki/Cenotaph). + +#### Locating the Runestone Output + +Outputs are searched for the first script pubkey that beings with `OP_RETURN +OP_13`. If deciphering fails, later matching outputs are not considered. + +#### Assembling the Payload Buffer + +The payload buffer is assembled by concatenating data pushes. If a non-data +push opcode is encountered, the deciphered runestone is a cenotaph with no +etching, mint, or edicts. + +#### Decoding the Integer Sequence + +A sequence of 128-bit integers are decoded from the payload as LEB128 varints. + +LEB128 varints are encoded as sequence of bytes, each of which has the +most-significant bit set, except for the last. + +If a LEB128 varint contains more than 18 bytes, would overflow a u128, or is +truncated, meaning that the end of the payload buffer is reached before +encountering a byte with the continuation bit not set, the decoded runestone is +a cenotaph with no etching, mint, or edicts. + +#### Parsing the Message + +The integer sequence is parsed into an untyped message: + +```rust +struct Message { + fields: Map>, + edicts: Vec, +} +``` + +The integers are interpreted as a sequence of tag/value pairs, with duplicate +tags appending their value to the field value. + +If a tag with value zero is encountered, all following integers are interpreted +as a series of four-integer edicts, each consisting of a rune ID block height, +rune ID transaction index, amount, and output. + +```rust +struct Edict { + id: RuneId, + amount: u128, + output: u32, +} +``` + +Rune ID block heights and transaction indices in edicts are delta encoded. + +Edict rune ID decoding starts with a base block height and transaction index of +zero. When decoding each rune ID, first the encoded block height delta is added +to the base block height. If the block height delta is zero, the next integer +is a transaction index delta. If the block height delta is greater than zero, +the next integer is instead an absolute transaction index. + +This implies that edicts must first be sorted by rune ID before being encoded +in a runestone. + +For example, to encode the following edicts: + +| block | TX | amount | output | +|-------|----|--------|--------| +| 10 | 5 | 5 | 1 | +| 50 | 1 | 25 | 4 | +| 10 | 7 | 1 | 8 | +| 10 | 5 | 10 | 3 | + +They are first sorted by block height and transaction index: + +| block | TX | amount | output | +|-------|----|--------|--------| +| 10 | 5 | 5 | 1 | +| 10 | 5 | 10 | 3 | +| 10 | 7 | 1 | 8 | +| 50 | 1 | 25 | 4 | + +And then delta encoded as: + +| block delta | TX delta | amount | output | +|-------------|----------|--------|--------| +| 10 | 5 | 5 | 1 | +| 0 | 0 | 10 | 3 | +| 0 | 2 | 1 | 8 | +| 40 | 1 | 25 | 4 | + +If an edict output is greater than the number of outputs of the transaction, an +edict rune ID is encountered with block zero and nonzero transaction index, or +a field is truncated, meaning a tag is encountered without a value, the decoded +runestone is a cenotaph. + +Note that if a cenotaph is produced here, the cenotaph is not empty, meaning +that it contains the fields and edicts, which may include an etching and mint. + +#### Parsing the Runestone + +The runestone: + +```rust +struct Runestone { + edicts: Vec, + etching: Option, + mint: Option, + pointer: Option, +} +``` + +Is parsed from the unsigned message using the following tags: + +```rust +enum Tag { + Body = 0, + Flags = 2, + Rune = 4, + Premine = 6, + Cap = 8, + Amount = 10, + HeightStart = 12, + HeightEnd = 14, + OffsetStart = 16, + OffsetEnd = 18, + Mint = 20, + Pointer = 22, + Cenotaph = 126, + + Divisibility = 1, + Spacers = 3, + Symbol = 5, + Nop = 127, +} +``` + +Note that tags are grouped by parity, i.e., whether they are even or odd. +Unrecognized odd tags are ignored. Unrecognized even tags produce a cenotaph. + +All unused tags are reserved for use by the protocol, may be assigned at any +time, and must not be used. + +##### Body + +The `Body` tag marks the end of the runestone's fields, causing all following +integers to be interpreted as edicts. + +##### Flags + +The `Flag` field contains a bitmap of flags, whose position is `1 << +FLAG_VALUE`: + +```rust +enum Flag { + Etching = 0, + Terms = 1, + Cenotaph = 127, +} +``` + +The `Etching` flag marks this transaction as containing an etching. + +The `Terms` flag marks this transaction's etching as having open mint terms. + +The `Cenotaph` flag is unrecognized. + +If the value of the flags field after removing recognized flags is nonzero, the +runestone is a cenotaph. + +##### Rune + +The `Rune` field contains the name of the rune being etched. If the `Etching` +flag is set but the `Rune` field is omitted, a reserved rune name is +allocated. + +##### Premine + +The `Premine` field contains the amount of premined runes. + +##### Cap + +The `Cap` field contains the allowed number of mints. + +##### Amount + +The `Amount` field contains the amount of runes each mint transaction receives. + +##### HeightStart and HeightEnd + +The `HeightStart` and `HeightEnd` fields contain the mint's starting and ending +absolute block heights, respectively. The mint is open starting in the block +with height `HeightStart`, and closes in the block with height `HeightEnd`. + +##### OffsetStart and OffsetEnd + +The `OffsetStart` and `OffsetEnd` fields contain the mint's starting and ending +block heights, relative to the block in which the etching is mined. The mint is +open starting in the block with height `OffsetStart` + `ETCHING_HEIGHT`, and +closes in the block with height `OffsetEnd` + `ETCHING_HEIGHT`. + +##### Mint + +The `Mint` field contains the Rune ID of the rune to be minted in this +transaction. + +##### Pointer + +The `Pointer` field contains the index of the output to which runes unallocated +by edicts should be transferred. If the `Pointer` field is absent, unallocated +runes are transferred to the first non-`OP_RETURN` output. + +##### Cenotaph + +The `Cenotaph` field is unrecognized. + +##### Divisibility + +The `Divisibility` field, raised to the power of ten, is the number of subunits +in a super unit of runes. + +For example, the amount `1234` of different runes with divisibility 0 through 3 +is displayed as follows: + +| Divisibility | Display | +|--------------|---------| +| 0 | 1234 | +| 1 | 123.4 | +| 2 | 12.34 | +| 3 | 1.234 | + +##### Spacers + +The `Spacers` field is a bitfield of `•` spacers that should be displayed +between the letters of the rune's name. + +The Nth field of the bitfield, starting from the least significant, determines +whether or not a spacer should be displayed between the Nth and N+1th +character, starting from the left of the rune's name. + +For example, the rune name `AAAA` rendered with different spacers: + +| Spacers | Display | +|---------|---------| +| 0b1 | A•AAA | +| 0b11 | A•A•AA | +| 0b10 | AA•AA | +| 0b111 | A•A•A•A | + +Trailing spacers are ignored. + +##### Symbol + +The `Symbol` field is the Unicode codepoint of the Rune's currency symbol, +which should be displayed after amounts of that rune. If a rune does not have a +currency symbol, the generic currency character `¤` should be used. + +For example, if the `Symbol` is `#` and the divisibility is 2, the amount of +`1234` units should be displayed as `12.34 #`. + +##### Nop + +The `Nop` field is unrecognized. + +#### Cenotaphs + +Cenotaphs have the following effects: + +- All runes input to a transaction containing a cenotaph are burned. + +- If the runestone that produced the cenotaph contained an etching, the etched + rune has supply zero and is unmintable. + +- If the runestone that produced the cenotaph is a mint, the mint counts + against the mint cap and the minted runes are burned. + +Cenotaphs may be created if a runestone contains an unrecognized even tag, an +unrecognized flag, an edict with an output number greater than the number of +inputs, a rune ID with block zero and nonzero transaction index, a malformed +varint, a non-datapush instruction in the runestone output script pubkey, a tag +without a following value, or trailing integers not part of an edict. + +#### Executing the Runestone + +Runestones are executed in the order their transactions are included in blocks. + +##### Etchings + +A runestone may contain an etching: + +```rust +struct Etching { + divisibility: Option, + premine: Option, + rune: Option, + spacers: Option, + symbol: Option, + terms: Option, +} +``` + +`rune` is the name of the rune to be etched, encoded as modified base-26 +integer. + +Rune names consist of the letters A through Z, with the following encoding: + +| Name | Encoding | +|------|----------| +| A | 0 | +| B | 1 | +| … | … | +| Y | 24 | +| Z | 25 | +| AA | 26 | +| AB | 27 | +| … | … | +| AY | 50 | +| AZ | 51 | +| BA | 52 | + +And so on and so on. + +Rune names `AAAAAAAAAAAAAAAAAAAAAAAAAAA` and above are reserved. + +If `rune` is omitted a reserved rune name is allocated as follows: + +```rust +fn reserve(block: u64, tx: u32) -> Rune { + Rune( + 6402364363415443603228541259936211926 + + (u128::from(block) << 32 | u128::from(tx)) + ) +} +``` + +`6402364363415443603228541259936211926` corresponds to the rune name +`AAAAAAAAAAAAAAAAAAAAAAAAAAA`. + +If `rune` is present, it must be unlocked as of the block in which the etching +appears. + +Initially, all rune names of length thirteen and longer, up until the first +reserved rune name, are unlocked. + +Runes begin unlocking in block 840,000, the block in which the runes protocol +activates. + +Thereafter, every 17,500 block period, the next shortest length of rune names +is continuously unlocked. So, between block 840,000 and block 857,500, the +twelve-character rune names are unlocked, between block 857,500 and block +875,000 the eleven character rune names are unlocked, and so on and so on, +until the one-character rune names are unlocked between block 1,032,500 and +block 1,050,000. See the `ord` codebase for the precise unlocking schedule. + +To prevent front running an etching that has been broadcast but not mined, if a +non-reserved rune name is being etched, the etching transaction must contain a +valid commitment to the name being etched. + +A commitment consists of a data push of the rune name, encoded as a +little-endian integer with trailing zero bytes elided, present in an input +witness tapscript where the output being spent has at least six confirmations. + +If a valid commitment is not present, the etching is ignored. + +#### Minting + +A runestone may mint a rune by including the rune's ID in the `Mint` field. + +If the mint is open, the mint amount is added to the unallocated runes in the +transaction's inputs. These runes may be transferred using edicts, and will +otherwise be transferred to the first non-`OP_RETURN` output, or the output +designated by the `Pointer` field. + +Mints may be made in any transaction after an etching, including in the same +block. + +#### Transferring + +Runes are transferred by edict: + +```rust +struct Edict { + id: RuneId, + amount: u128, + output: u32, +} +``` + +A runestone may contain any number of edicts, which are processed in sequence. + +Before edicts are processed, input runes, as well as minted or premined runes, +if any, are unallocated. + +Each edict decrements the unallocated balance of rune `id` and increments the +balance allocated to transaction outputs of rune `id`. + +If an edict would allocate more runes than are currently unallocated, the +`amount` is reduced to the number of currently unallocated runes. In other +words, the edict allocates all remaining unallocated units of rune `id`. + +Because the ID of an etched rune is not known before it is included in a block, +ID `0:0` is used to mean the rune being etched in this transaction, if any. + +An edict with `amount` zero allocates all remaining units of rune `id`. + +An edict with `output` equal to the number of transaction outputs allocates +`amount` runes to each non-`OP_RETURN` output. + +An edict with `amount` zero and `output` equal to the number of transaction +outputs divides all unallocated units of rune `id` between each non-`OP_RETURN` +output. If the number of unallocated runes is not divisible by the number of +non-`OP_RETURN` outputs, 1 additional rune is assigned to the first `R` +non-`OP_RETURN` outputs, where `R` is the remainder after dividing the balance +of unallocated units of rune `id` by the number of non-`OP_RETURN` outputs. + +If any edict in a runestone has a rune ID with `block` zero and `tx` greater +than zero, or `output` greater than the number of transaction outputs, the +runestone is a cenotaph. + +Note that edicts in cenotaphs are not processed, and all input runes are +burned. diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4bde2e7bb7..4d7c65be2c 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -72,61 +72,70 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" dependencies = [ "backtrace", ] [[package]] name = "arbitrary" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" dependencies = [ "derive_arbitrary", ] [[package]] name = "arc-swap" -version = "1.6.0" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "array-init" +version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" +dependencies = [ + "nodrop", +] [[package]] name = "asn1-rs" @@ -141,7 +150,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.23", + "time", ] [[package]] @@ -169,22 +178,22 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", - "event-listener 4.0.0", - "event-listener-strategy", + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b74f44609f0f91493e3082d3734d98497e094777144380ea4db9f9905dd5b6" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" dependencies = [ "brotli", "flate2", @@ -202,7 +211,7 @@ checksum = "096146020b08dbc4587685b0730a7ba905625af13c65f8028035cdfd69573c91" dependencies = [ "anyhow", "futures", - "http 1.0.0", + "http 1.1.0", "httparse", "log", ] @@ -213,7 +222,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ - "async-lock 2.7.0", + "async-lock 2.8.0", "autocfg", "cfg-if", "concurrent-queue", @@ -221,59 +230,58 @@ dependencies = [ "log", "parking", "polling", - "rustix 0.37.23", + "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener 2.5.3", ] [[package]] name = "async-lock" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" dependencies = [ - "event-listener 4.0.0", - "event-listener-strategy", + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", "pin-project-lite", ] [[package]] name = "async-net" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4051e67316bc7eff608fe723df5d32ed639946adcd69e07df41fd42a7b411f1f" +checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" dependencies = [ "async-io", - "autocfg", "blocking", "futures-lite 1.13.0", ] [[package]] name = "async-task" -version = "4.4.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] @@ -288,11 +296,11 @@ dependencies = [ "futures", "futures-rustls", "gloo-net", - "http 1.0.0", + "http 1.1.0", "js-sys", "lazy_static", "log", - "rustls 0.22.1", + "rustls 0.22.3", "rustls-pki-types", "thiserror", "wasm-bindgen", @@ -312,7 +320,7 @@ dependencies = [ "futures", "futures-lite 1.13.0", "generic_static", - "http 1.0.0", + "http 1.1.0", "log", "rand", "ring 0.16.20", @@ -336,29 +344,28 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axum" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", "bytes", "futures-util", - "headers", - "http 0.2.9", + "http 0.2.12", "http-body", "hyper", "itoa", @@ -388,7 +395,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.9", + "http 0.2.12", "http-body", "mime", "rustversion", @@ -405,11 +412,11 @@ dependencies = [ "arc-swap", "bytes", "futures-util", - "http 0.2.9", + "http 0.2.12", "http-body", "hyper", "pin-project-lite", - "rustls 0.21.5", + "rustls 0.21.10", "rustls-pemfile", "tokio", "tokio-rustls", @@ -418,9 +425,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -439,9 +446,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bech32" @@ -449,6 +462,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bip39" version = "2.0.0" @@ -462,11 +481,11 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" dependencies = [ - "bech32", + "bech32 0.9.1", "bitcoin-private", "bitcoin_hashes 0.12.0", "hex_lit", @@ -504,9 +523,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -524,11 +543,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ "async-channel", - "async-lock 3.2.0", + "async-lock 3.3.0", "async-task", - "fastrand 2.0.0", + "fastrand 2.0.2", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.3.0", "piper", "tracing", ] @@ -539,19 +558,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1906889b1f805a715eac02b2dea416e25c5cfa00f099530fa9d137a3cff93113" dependencies = [ - "darling 0.20.3", + "darling 0.20.8", "mime", "new_mime_guess", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "brotli" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -570,27 +589,27 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ "jobserver", "libc", @@ -602,27 +621,32 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.52.4", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -631,15 +655,15 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", @@ -647,9 +671,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -657,33 +681,33 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.0", ] [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" @@ -691,26 +715,36 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -719,72 +753,70 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", - "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" @@ -798,12 +830,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.0" +version = "3.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a011bbe2c35ce9c1f143b7af6f94f29a167beb4cd1d29e6740ce836f723120e" +checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" dependencies = [ "nix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -818,12 +850,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ - "darling_core 0.20.3", - "darling_macro 0.20.3", + "darling_core 0.20.8", + "darling_macro 0.20.8", ] [[package]] @@ -836,22 +868,22 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.27", + "strsim 0.10.0", + "syn 2.0.55", ] [[package]] @@ -867,20 +899,20 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ - "darling_core 0.20.3", + "darling_core 0.20.8", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "der-parser" @@ -896,15 +928,25 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive_arbitrary" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] @@ -999,14 +1041,14 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encode_unicode" @@ -1016,24 +1058,34 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -1044,36 +1096,36 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "event-listener" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "2.5.3" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] [[package]] name = "event-listener" -version = "4.0.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" dependencies = [ "concurrent-queue", "parking", @@ -1086,7 +1138,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.0", + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.2.0", "pin-project-lite", ] @@ -1101,15 +1163,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -1121,20 +1183,35 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1147,9 +1224,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1157,15 +1234,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1174,9 +1251,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" @@ -1195,9 +1272,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "futures-core", "pin-project-lite", @@ -1205,43 +1282,43 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "futures-rustls" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afda89bce8f65072d24f8b99a2127e229462d8008182ca93f1d5d2e5df8f22f" +checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" dependencies = [ "futures-io", - "rustls 0.22.1", + "rustls 0.22.3", "rustls-pki-types", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1263,6 +1340,7 @@ dependencies = [ "bitcoin", "libfuzzer-sys", "ord", + "ordinals", ] [[package]] @@ -1295,20 +1373,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gloo-net" @@ -1337,17 +1415,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.20" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.9", - "indexmap 1.9.3", + "http 0.2.12", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1356,9 +1434,13 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", +] [[package]] name = "hashbrown" @@ -1368,46 +1450,27 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" - -[[package]] -name = "headers" -version = "0.3.8" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" -dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "bytes", - "headers-core", - "http 0.2.9", - "httpdate", - "mime", - "sha1", -] +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] -name = "headers-core" -version = "0.2.0" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.9", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1429,9 +1492,9 @@ checksum = "459a0ca33ee92551e0a3bb1774f2d3bdd1c09fb6341845736662dd25e1fcb52a" [[package]] name = "http" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1440,9 +1503,9 @@ dependencies = [ [[package]] name = "http" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1451,12 +1514,12 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.9", + "http 0.2.12", "pin-project-lite", ] @@ -1474,9 +1537,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1486,40 +1549,53 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http 0.2.9", + "http 0.2.12", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -1537,6 +1613,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1545,23 +1631,25 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] name = "indexmap" -version = "2.0.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.3", + "serde", ] [[package]] name = "indicatif" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff8cc23a7393a397ed1d7f56e6365cba772aba9f9912ab968b03043c395d057" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -1591,36 +1679,31 @@ dependencies = [ ] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "ipnet" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix 0.38.4", - "windows-sys 0.48.0", -] +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1638,9 +1721,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -1653,21 +1736,32 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libfuzzer-sys" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" dependencies = [ "arbitrary", "cc", "once_cell", ] +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1676,36 +1770,33 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" -version = "0.4.19" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] -name = "memchr" -version = "2.5.0" +name = "maybe-uninit" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] -name = "memoffset" -version = "0.9.0" +name = "memchr" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -1741,21 +1832,21 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] @@ -1773,6 +1864,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "never" version = "0.1.0" @@ -1791,16 +1900,22 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "cfg-if", + "cfg_aliases", "libc", - "static_assertions", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -1822,22 +1937,27 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] @@ -1856,9 +1976,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -1881,9 +2001,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.31.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1899,26 +2019,70 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "option-ext" -version = "0.2.0" +name = "openssl" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ord" -version = "0.12.3" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", "axum", "axum-server", - "base64 0.21.2", - "bech32", + "base64 0.22.0", + "bech32 0.11.0", "bip39", "bitcoin", "boilerplate", @@ -1926,14 +2090,14 @@ dependencies = [ "chrono", "ciborium", "clap", + "colored", "ctrlc", - "derive_more", "dirs", "env_logger", "futures", "hex", "html-escaper", - "http 0.2.9", + "http 0.2.12", "humantime", "hyper", "indicatif", @@ -1944,15 +2108,19 @@ dependencies = [ "miniscript", "mp4", "ord-bitcoincore-rpc", + "ordinals", "pulldown-cmark", "redb", "regex", + "reqwest", "rss", "rust-embed", - "rustls 0.22.1", + "rustls 0.22.3", "rustls-acme", "serde", + "serde-hex", "serde_json", + "serde_with", "serde_yaml", "sha3", "sysinfo", @@ -1961,13 +2129,14 @@ dependencies = [ "tokio-stream", "tokio-util", "tower-http", + "urlencoding", ] [[package]] name = "ord-bitcoincore-rpc" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d57a4297d466506cde088e020b33819f9a496d50272b14d7890e6fde5595a3e" +checksum = "16b622f69d68d7201d5186615978aca36ceb7e7c57d7771491d3e261c64ff4d8" dependencies = [ "bitcoin-private", "jsonrpc", @@ -1989,11 +2158,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ordinals" +version = "0.0.4" +dependencies = [ + "bitcoin", + "derive_more", + "serde", + "serde_with", + "thiserror", +] + [[package]] name = "parking" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "pem" @@ -2006,35 +2186,35 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2049,10 +2229,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.0", + "fastrand 2.0.2", "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "polling" version = "2.8.0" @@ -2071,9 +2257,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.4.2" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f32154ba0af3a075eefa1eda8bb414ee928f62303a54ea85b8d6638ff1a6ee9e" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -2083,25 +2275,32 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "pulldown-cmark" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "getopts", "memchr", + "pulldown-cmark-escape", "unicase", ] +[[package]] +name = "pulldown-cmark-escape" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b" + [[package]] name = "quick-xml" version = "0.30.0" @@ -2114,9 +2313,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2153,9 +2352,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2163,14 +2362,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -2181,53 +2378,44 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring 0.16.20", - "time 0.3.23", + "time", "yasna", ] [[package]] name = "redb" -version = "1.4.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08837f9a129bde83c51953b8c96cbb3422b940166b730caa954836106eb1dfd2" +checksum = "a1100a056c5dcdd4e5513d5333385223b26ef1bf92f31eb38f407e8c20549256" dependencies = [ "libc", ] [[package]] name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", - "redox_syscall 0.2.16", + "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.9.1" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -2237,9 +2425,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -2248,9 +2436,49 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] [[package]] name = "ring" @@ -2269,23 +2497,24 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rss" -version = "2.0.5" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32100fb1eea9edc4be79553ae97914add78b55892a517995fe96d1ada50d09d7" +checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a" dependencies = [ "atom_syndication", "derive_builder", @@ -2295,9 +2524,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.0.0" +version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" +checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -2306,22 +2535,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.0.0" +version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" +checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.27", + "syn 2.0.55", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.0.0" +version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" +checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" dependencies = [ "sha2", "walkdir", @@ -2353,9 +2582,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.23" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", @@ -2367,39 +2596,39 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.3", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.5" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.16.20", - "rustls-webpki 0.101.2", + "ring 0.17.8", + "rustls-webpki 0.101.7", "sct", ] [[package]] name = "rustls" -version = "0.22.1" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6b63262c9fcac8659abfaa96cac103d28166d3ff3eaf8f412e19f3ae9e5a48" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" dependencies = [ "log", - "ring 0.17.7", + "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.0", + "rustls-webpki 0.102.2", "subtle", "zeroize", ] @@ -2419,7 +2648,7 @@ dependencies = [ "chrono", "futures", "futures-rustls", - "http 1.0.0", + "http 1.1.0", "log", "pem", "rcgen", @@ -2435,36 +2664,36 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.2", + "base64 0.21.7", ] [[package]] name = "rustls-pki-types" -version = "1.0.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7673e0aa20ee4937c6aacfc12bb8341cfbf054cdd21df6bec5fd0629fe9339b" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] name = "rustls-webpki" -version = "0.101.2" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] name = "rustls-webpki" -version = "0.102.0" +version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2635c8bc2b88d367767c5de8ea1d8db9af3f6219eba28442242d9ab81d1b89" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "rustls-pki-types", "untrusted 0.9.0", ] @@ -2477,9 +2706,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -2491,19 +2720,22 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "schannel" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -2527,39 +2759,73 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.177" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ba2516aa6bf82e0b19ca8b50019d52df58455d3cf9bdaf6315225fdd0c560a" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hex" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" +dependencies = [ + "array-init", + "serde", + "smallvec", +] + [[package]] name = "serde_derive" -version = "1.0.177" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2567,9 +2833,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", @@ -2588,34 +2854,53 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.25" +name = "serde_with" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ - "indexmap 2.0.0", - "itoa", - "ryu", + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", "serde", - "unsafe-libyaml", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", ] [[package]] -name = "sha1" -version = "0.10.5" +name = "serde_with_macros" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "darling 0.20.8", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -2634,23 +2919,42 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -2664,16 +2968,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" @@ -2690,7 +2994,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -2716,9 +3020,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -2745,9 +3049,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.7" +version = "0.30.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165d6d8539689e3d3bc8b98ac59541e1f21c7de7c85d60dc80e43ae0ed2113db" +checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2755,69 +3059,72 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "winapi", + "windows", ] [[package]] -name = "tempfile" -version = "3.7.0" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "cfg-if", - "fastrand 2.0.0", - "redox_syscall 0.3.5", - "rustix 0.38.4", - "windows-sys 0.48.0", + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "termcolor" -version = "1.2.0" +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "winapi-util", + "core-foundation-sys", + "libc", ] [[package]] -name = "thiserror" -version = "1.0.44" +name = "tempfile" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ - "thiserror-impl", + "cfg-if", + "fastrand 2.0.2", + "rustix 0.38.32", + "windows-sys 0.52.0", ] [[package]] -name = "thiserror-impl" -version = "1.0.44" +name = "thiserror" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", + "thiserror-impl", ] [[package]] -name = "time" -version = "0.1.45" +name = "thiserror-impl" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "proc-macro2", + "quote", + "syn 2.0.55", ] [[package]] name = "time" -version = "0.3.23" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ + "deranged", "itoa", + "num-conv", + "powerfmt", "serde", "time-core", "time-macros", @@ -2825,16 +3132,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -2855,31 +3163,40 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -2888,15 +3205,15 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.5", + "rustls 0.21.10", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2905,9 +3222,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2936,18 +3253,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "async-compression", - "bitflags 2.3.3", + "base64 0.21.7", + "bitflags 2.5.0", "bytes", "futures-core", "futures-util", - "http 0.2.9", + "http 0.2.12", "http-body", "http-range-header", + "mime", "pin-project-lite", "tokio", "tokio-util", @@ -2969,11 +3288,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-core", @@ -2981,39 +3299,45 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3026,9 +3350,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode-xid" @@ -3038,9 +3362,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" @@ -3054,6 +3378,23 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -3066,6 +3407,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -3074,15 +3421,15 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3097,12 +3444,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3111,9 +3452,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3121,24 +3462,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3148,9 +3489,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3158,28 +3499,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.55", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3187,9 +3528,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "winapi" @@ -3209,9 +3550,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -3224,20 +3565,21 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-targets 0.48.1", + "windows-core", + "windows-targets 0.52.4", ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.4", ] [[package]] @@ -3246,122 +3588,141 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "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.4", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winreg" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] [[package]] name = "x509-parser" @@ -3378,7 +3739,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.23", + "time", ] [[package]] @@ -3387,7 +3748,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.23", + "time", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 60c9958501..7aace48a45 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -18,6 +18,7 @@ arbitrary = { version = "1", features = ["derive"] } bitcoin = { version = "0.30.0", features = ["rand"] } libfuzzer-sys = "0.4" ord = { path = ".." } +ordinals = { path = "../crates/ordinals" } [[bin]] name = "runestone-decipher" diff --git a/fuzz/fuzz_targets/runestone_decipher.rs b/fuzz/fuzz_targets/runestone_decipher.rs index 16b6a863ee..4895530074 100644 --- a/fuzz/fuzz_targets/runestone_decipher.rs +++ b/fuzz/fuzz_targets/runestone_decipher.rs @@ -7,13 +7,13 @@ use { Transaction, TxOut, }, libfuzzer_sys::fuzz_target, - ord::runes::Runestone, + ordinals::Runestone, }; fuzz_target!(|input: Vec>| { let mut builder = script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST"); + .push_opcode(opcodes::all::OP_PUSHNUM_13); for slice in input { let Ok(push): Result<&PushBytes, _> = slice.as_slice().try_into() else { diff --git a/fuzz/fuzz_targets/transaction_builder.rs b/fuzz/fuzz_targets/transaction_builder.rs index 32ac50323c..41948b559e 100644 --- a/fuzz/fuzz_targets/transaction_builder.rs +++ b/fuzz/fuzz_targets/transaction_builder.rs @@ -7,7 +7,8 @@ use { Amount, OutPoint, }, libfuzzer_sys::fuzz_target, - ord::{FeeRate, SatPoint, Target, TransactionBuilder}, + ord::{FeeRate, Target, TransactionBuilder}, + ordinals::SatPoint, std::collections::{BTreeMap, BTreeSet}, }; diff --git a/fuzz/fuzz_targets/varint_decode.rs b/fuzz/fuzz_targets/varint_decode.rs index 3b566549df..cd54a8712e 100644 --- a/fuzz/fuzz_targets/varint_decode.rs +++ b/fuzz/fuzz_targets/varint_decode.rs @@ -1,16 +1,19 @@ #![no_main] -use {libfuzzer_sys::fuzz_target, ord::runes::varint}; +use {libfuzzer_sys::fuzz_target, ordinals::varint}; fuzz_target!(|input: &[u8]| { let mut i = 0; while i < input.len() { - let (decoded, length) = varint::decode(&input[i..]); + let Some((decoded, length)) = varint::decode(&input[i..]) else { + break; + }; let mut encoded = Vec::new(); varint::encode_to_vec(decoded, &mut encoded); - let (redecoded, _) = varint::decode(&input[i..]); + let (redecoded, redecoded_length) = varint::decode(&input[i..]).unwrap(); assert_eq!(redecoded, decoded); + assert_eq!(redecoded_length, length); i += length; } }); diff --git a/fuzz/fuzz_targets/varint_encode.rs b/fuzz/fuzz_targets/varint_encode.rs index 75622322ff..c4e459e95b 100644 --- a/fuzz/fuzz_targets/varint_encode.rs +++ b/fuzz/fuzz_targets/varint_encode.rs @@ -1,11 +1,11 @@ #![no_main] -use {libfuzzer_sys::fuzz_target, ord::runes::varint}; +use {libfuzzer_sys::fuzz_target, ordinals::varint}; fuzz_target!(|input: u128| { let mut encoded = Vec::new(); varint::encode_to_vec(input, &mut encoded); - let (decoded, length) = varint::decode(&encoded); + let (decoded, length) = varint::decode(&encoded).unwrap(); assert_eq!(length, encoded.len()); assert_eq!(decoded, input); }); diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000000..72b2381c4b --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,2 @@ +#!/bin/sh +exec cargo fmt --check diff --git a/justfile b/justfile index 8b590dac14..8cffcbe396 100644 --- a/justfile +++ b/justfile @@ -17,6 +17,16 @@ fmt: clippy: cargo clippy --all --all-targets -- --deny warnings +install-git-hooks: + #!/usr/bin/env bash + set -euo pipefail + for hook in hooks/*; do + name=$(basename "$hook") + if [ ! -e ".git/hooks/$name" ]; then + ln -s "$PWD/$hook" ".git/hooks/$name" + fi + done + deploy branch remote chain domain: ssh root@{{domain}} '\ export DEBIAN_FRONTEND=noninteractive \ @@ -53,6 +63,14 @@ deploy-all: \ deploy-mainnet-bravo \ deploy-mainnet-charlie +delete-indices: \ + (delete-index "regtest.ordinals.net") \ + (delete-index "signet.ordinals.net") \ + (delete-index "testnet.ordinals.net") + +delete-index domain: + ssh root@{{domain}} 'systemctl stop ord && rm -f /var/lib/ord/*/index.redb' + servers := 'alpha bravo charlie regtest signet testnet' initialize-server-keys: @@ -89,10 +107,10 @@ fuzz: set -euxo pipefail cd fuzz while true; do - cargo +nightly fuzz run transaction-builder -- -max_total_time=60 cargo +nightly fuzz run runestone-decipher -- -max_total_time=60 cargo +nightly fuzz run varint-decode -- -max_total_time=60 cargo +nightly fuzz run varint-encode -- -max_total_time=60 + cargo +nightly fuzz run transaction-builder -- -max_total_time=60 done open: diff --git a/src/api.rs b/src/api.rs index f4cd30b2e5..9093465334 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,8 +1,5 @@ use { - super::{ - target_as_block_hash, BlockHash, Chain, Deserialize, Height, InscriptionId, OutPoint, Pile, - Rarity, SatPoint, Serialize, SpacedRune, TxMerkleNode, TxOut, - }, + super::*, serde_hex::{SerHex, Strict}, }; @@ -79,16 +76,17 @@ pub struct Children { #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct Inscription { pub address: Option, - pub charms: Vec, + pub charms: Vec, pub children: Vec, pub content_length: Option, pub content_type: Option, + pub effective_content_type: Option, pub fee: u64, pub height: u32, pub id: InscriptionId, pub next: Option, pub number: i32, - pub parent: Option, + pub parents: Vec, pub previous: Option, pub rune: Option, pub sat: Option, @@ -105,7 +103,7 @@ pub struct Inscription { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct InscriptionRecursive { - pub charms: Vec, + pub charms: Vec, pub content_type: Option, pub content_length: Option, pub fee: u64, @@ -128,7 +126,7 @@ pub struct Inscriptions { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Output { - pub address: Option, + pub address: Option>, pub indexed: bool, pub inscriptions: Vec, pub runes: Vec<(SpacedRune, Pile)>, @@ -154,7 +152,7 @@ impl Output { address: chain .address_from_script(&output.script_pubkey) .ok() - .map(|address| address.to_string()), + .map(|address| uncheck(&address)), indexed, inscriptions, runes, @@ -169,20 +167,21 @@ impl Output { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Sat { - pub number: u64, - pub decimal: String, - pub degree: String, - pub name: String, pub block: u32, + pub charms: Vec, pub cycle: u32, + pub decimal: String, + pub degree: String, pub epoch: u32, - pub period: u32, + pub inscriptions: Vec, + pub name: String, + pub number: u64, pub offset: u64, - pub rarity: Rarity, pub percentile: String, + pub period: u32, + pub rarity: Rarity, pub satpoint: Option, pub timestamp: i64, - pub inscriptions: Vec, } #[derive(Debug, PartialEq, Serialize, Deserialize)] diff --git a/src/blocktime.rs b/src/blocktime.rs index 0908bed309..c65eb0bd96 100644 --- a/src/blocktime.rs +++ b/src/blocktime.rs @@ -8,7 +8,7 @@ pub(crate) enum Blocktime { impl Blocktime { pub(crate) fn confirmed(seconds: u32) -> Self { - Self::Confirmed(timestamp(seconds)) + Self::Confirmed(timestamp(seconds.into())) } pub(crate) fn timestamp(self) -> DateTime { diff --git a/src/chain.rs b/src/chain.rs index fe925a2d63..d21b05aa61 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -14,12 +14,7 @@ pub enum Chain { impl Chain { pub(crate) fn network(self) -> Network { - match self { - Self::Mainnet => Network::Bitcoin, - Self::Testnet => Network::Testnet, - Self::Signet => Network::Signet, - Self::Regtest => Network::Regtest, - } + self.into() } pub(crate) fn default_rpc_port(self) -> u16 { @@ -48,13 +43,7 @@ impl Chain { } pub(crate) fn first_rune_height(self) -> u32 { - SUBSIDY_HALVING_INTERVAL - * match self { - Self::Mainnet => 4, - Self::Regtest => 0, - Self::Signet => 0, - Self::Testnet => 12, - } + Rune::first_rune_height(self.into()) } pub(crate) fn jubilee_height(self) -> u32 { @@ -94,6 +83,17 @@ impl Chain { } } +impl From for Network { + fn from(chain: Chain) -> Network { + match chain { + Chain::Mainnet => Network::Bitcoin, + Chain::Testnet => Network::Testnet, + Chain::Signet => Network::Signet, + Chain::Regtest => Network::Regtest, + } + } +} + impl Display for Chain { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( diff --git a/src/decimal.rs b/src/decimal.rs index f66b533ddc..91cb86b6ee 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -1,13 +1,13 @@ use super::*; -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)] pub struct Decimal { value: u128, scale: u8, } impl Decimal { - pub(crate) fn to_amount(self, divisibility: u8) -> Result { + pub fn to_integer(self, divisibility: u8) -> Result { match divisibility.checked_sub(self.scale) { Some(difference) => Ok( self @@ -85,24 +85,6 @@ impl FromStr for Decimal { } } -impl Serialize for Decimal { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for Decimal { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) - } -} - #[cfg(test)] mod tests { use super::*; @@ -146,7 +128,7 @@ mod tests { assert_eq!( s.parse::() .unwrap() - .to_amount(divisibility) + .to_integer(divisibility) .unwrap(), amount, ); @@ -154,7 +136,7 @@ mod tests { assert_eq!( Decimal { value: 0, scale: 0 } - .to_amount(255) + .to_integer(255) .unwrap_err() .to_string(), "divisibility out of range" @@ -165,7 +147,7 @@ mod tests { value: u128::MAX, scale: 0, } - .to_amount(1) + .to_integer(1) .unwrap_err() .to_string(), "amount out of range", @@ -173,7 +155,7 @@ mod tests { assert_eq!( Decimal { value: 1, scale: 1 } - .to_amount(0) + .to_integer(0) .unwrap_err() .to_string(), "excessive precision", @@ -196,6 +178,7 @@ mod tests { assert_eq!(decimal, string.parse::().unwrap()); } + case(Decimal { value: 0, scale: 0 }, "0"); case(Decimal { value: 1, scale: 0 }, "1"); case(Decimal { value: 1, scale: 1 }, "0.1"); case( diff --git a/src/deserialize_from_str.rs b/src/deserialize_from_str.rs new file mode 100644 index 0000000000..edd90d9b76 --- /dev/null +++ b/src/deserialize_from_str.rs @@ -0,0 +1,32 @@ +use super::*; + +pub struct DeserializeFromStr(pub T); + +impl<'de, T: FromStr> Deserialize<'de> for DeserializeFromStr +where + T::Err: Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(Self( + FromStr::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)?, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize_from_str() { + assert_eq!( + serde_json::from_str::>("\"1\"") + .unwrap() + .0, + 1, + ); + } +} diff --git a/src/index.rs b/src/index.rs index 00e9cf475f..bec84d0a25 100644 --- a/src/index.rs +++ b/src/index.rs @@ -5,12 +5,13 @@ use { OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TxidValue, }, event::Event, - reorg::*, - runes::{Rune, RuneId}, + lot::Lot, + reorg::Reorg, updater::Updater, }, super::*, crate::{ + runes::MintError, subcommand::{find::FindRangeOutput, server::query}, templates::StatusHtml, }, @@ -24,21 +25,22 @@ use { log::log_enabled, redb::{ Database, DatabaseError, MultimapTable, MultimapTableDefinition, MultimapTableHandle, - ReadOnlyTable, ReadableMultimapTable, ReadableTable, RepairSession, StorageError, Table, - TableDefinition, TableHandle, TableStats, WriteTransaction, + ReadOnlyTable, ReadableMultimapTable, ReadableTable, ReadableTableMetadata, RepairSession, + StorageError, Table, TableDefinition, TableHandle, TableStats, WriteTransaction, }, std::{ collections::HashMap, io::{BufWriter, Write}, - sync::{Mutex, Once}, + sync::Once, }, }; -pub use {self::entry::RuneEntry, entry::MintEntry}; +pub use self::entry::RuneEntry; pub(crate) mod entry; pub mod event; mod fetcher; +mod lot; mod reorg; mod rtx; mod updater; @@ -46,20 +48,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 18; - -macro_rules! define_table { - ($name:ident, $key:ty, $value:ty) => { - const $name: TableDefinition<$key, $value> = TableDefinition::new(stringify!($name)); - }; -} - -macro_rules! define_multimap_table { - ($name:ident, $key:ty, $value:ty) => { - const $name: MultimapTableDefinition<$key, $value> = - MultimapTableDefinition::new(stringify!($name)); - }; -} +const SCHEMA_VERSION: u64 = 24; define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, &SatPointValue, u32 } define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 } @@ -171,7 +160,7 @@ pub(crate) struct TransactionInfo { pub(crate) struct InscriptionInfo { pub(crate) children: Vec, pub(crate) entry: InscriptionEntry, - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) output: Option, pub(crate) satpoint: SatPoint, pub(crate) inscription: Inscription, @@ -205,7 +194,7 @@ impl BitcoinCoreRpcResultExt for Result { } pub struct Index { - client: Client, + pub(crate) client: Client, database: Database, durability: redb::Durability, event_sender: Option>, @@ -375,6 +364,49 @@ impl Index { Self::set_statistic(&mut statistics, Statistic::Schema, SCHEMA_VERSION)?; } + if settings.index_runes() && settings.chain() == Chain::Mainnet { + let rune = Rune(2055900680524219742); + + let id = RuneId { block: 1, tx: 0 }; + let etching = Txid::all_zeros(); + + tx.open_table(RUNE_TO_RUNE_ID)? + .insert(rune.store(), id.store())?; + + let mut statistics = tx.open_table(STATISTIC_TO_COUNT)?; + + Self::set_statistic(&mut statistics, Statistic::Runes, 1)?; + + tx.open_table(RUNE_ID_TO_RUNE_ENTRY)?.insert( + id.store(), + RuneEntry { + block: id.block, + burned: 0, + divisibility: 0, + etching, + terms: Some(Terms { + amount: Some(1), + cap: Some(u128::MAX), + height: ( + Some((SUBSIDY_HALVING_INTERVAL * 4).into()), + Some((SUBSIDY_HALVING_INTERVAL * 5).into()), + ), + offset: (None, None), + }), + mints: 0, + number: 0, + premine: 0, + spaced_rune: SpacedRune { rune, spacers: 128 }, + symbol: Some('\u{29C9}'), + timestamp: 0, + } + .store(), + )?; + + tx.open_table(TRANSACTION_ID_TO_RUNE)? + .insert(&etching.store(), rune.store())?; + } + tx.commit()?; database @@ -490,7 +522,7 @@ impl Index { inscriptions: blessed_inscriptions + cursed_inscriptions, lost_sats: statistic(Statistic::LostSats)?, minimum_rune_for_next_block: Rune::minimum_at_height( - self.settings.chain(), + self.settings.chain().network(), Height(next_height), ), rune_index: statistic(Statistic::IndexRunes)? != 0, @@ -615,14 +647,14 @@ impl Index { log::info!("{}", err.to_string()); match err.downcast_ref() { - Some(&ReorgError::Recoverable { height, depth }) => { + Some(&reorg::Error::Recoverable { height, depth }) => { Reorg::handle_reorg(self, height, depth)?; } - Some(&ReorgError::Unrecoverable) => { + Some(&reorg::Error::Unrecoverable) => { self .unrecoverably_reorged .store(true, atomic::Ordering::Relaxed); - return Err(anyhow!(ReorgError::Unrecoverable)); + return Err(anyhow!(reorg::Error::Unrecoverable)); } _ => return Err(err), }; @@ -632,7 +664,7 @@ impl Index { } pub(crate) fn export(&self, filename: &String, include_addresses: bool) -> Result { - let mut writer = BufWriter::new(File::create(filename)?); + let mut writer = BufWriter::new(fs::File::create(filename)?); let rtx = self.database.begin_read()?; let blocks_indexed = rtx @@ -831,7 +863,7 @@ impl Index { .begin_read()? .open_table(RUNE_ID_TO_RUNE_ENTRY)? .get(&id.store())? - .map(|entry| RuneEntry::load(entry.value()).rune), + .map(|entry| RuneEntry::load(entry.value()).spaced_rune.rune), ) } @@ -887,6 +919,27 @@ impl Index { Ok(entries) } + pub(crate) fn encode_rune_balance(id: RuneId, balance: u128, buffer: &mut Vec) { + varint::encode_to_vec(id.block.into(), buffer); + varint::encode_to_vec(id.tx.into(), buffer); + varint::encode_to_vec(balance, buffer); + } + + pub(crate) fn decode_rune_balance(buffer: &[u8]) -> Option<((RuneId, u128), usize)> { + let mut len = 0; + let (block, block_len) = varint::decode(&buffer[len..])?; + len += block_len; + let (tx, tx_len) = varint::decode(&buffer[len..])?; + len += tx_len; + let id = RuneId { + block: block.try_into().ok()?, + tx: tx.try_into().ok()?, + }; + let (balance, balance_len) = varint::decode(&buffer[len..])?; + len += balance_len; + Some(((id, balance), len)) + } + pub(crate) fn get_rune_balances_for_outpoint( &self, outpoint: OutPoint, @@ -906,17 +959,13 @@ impl Index { let mut balances = Vec::new(); let mut i = 0; while i < balances_buffer.len() { - let (id, length) = runes::varint::decode(&balances_buffer[i..]); - i += length; - let (amount, length) = runes::varint::decode(&balances_buffer[i..]); + let ((id, amount), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap(); i += length; - let id = RuneId::try_from(id).unwrap(); - let entry = RuneEntry::load(id_to_rune_entries.get(id.store())?.unwrap().value()); balances.push(( - entry.spaced_rune(), + entry.spaced_rune, Pile { amount, divisibility: entry.divisibility, @@ -928,33 +977,60 @@ impl Index { Ok(balances) } - pub(crate) fn get_rune_balance_map(&self) -> Result>> { + pub(crate) fn get_rune_balance_map( + &self, + ) -> Result>> { let outpoint_balances = self.get_rune_balances()?; let rtx = self.database.begin_read()?; let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; - let mut rune_balances: BTreeMap> = BTreeMap::new(); + let mut rune_balances_by_id: BTreeMap> = BTreeMap::new(); for (outpoint, balances) in outpoint_balances { for (rune_id, amount) in balances { - let rune = RuneEntry::load( - rune_id_to_rune_entry - .get(&rune_id.store())? - .unwrap() - .value(), - ) - .rune; - - *rune_balances - .entry(rune) + *rune_balances_by_id + .entry(rune_id) .or_default() .entry(outpoint) .or_default() += amount; } } + let mut rune_balances = BTreeMap::new(); + + for (rune_id, balances) in rune_balances_by_id { + let RuneEntry { + divisibility, + spaced_rune, + symbol, + .. + } = RuneEntry::load( + rune_id_to_rune_entry + .get(&rune_id.store())? + .unwrap() + .value(), + ); + + rune_balances.insert( + spaced_rune, + balances + .into_iter() + .map(|(outpoint, amount)| { + ( + outpoint, + Pile { + amount, + divisibility, + symbol, + }, + ) + }) + .collect(), + ); + } + Ok(rune_balances) } @@ -974,11 +1050,9 @@ impl Index { let mut balances = Vec::new(); let mut i = 0; while i < balances_buffer.len() { - let (id, length) = runes::varint::decode(&balances_buffer[i..]); - i += length; - let (balance, length) = runes::varint::decode(&balances_buffer[i..]); + let ((id, balance), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap(); i += length; - balances.push((RuneId::try_from(id)?, balance)); + balances.push((id, balance)); } result.push((outpoint, balances)); @@ -1070,10 +1144,10 @@ impl Index { } #[cfg(test)] - pub(crate) fn get_parent_by_inscription_id( + pub(crate) fn get_parents_by_inscription_id( &self, inscription_id: InscriptionId, - ) -> InscriptionId { + ) -> Vec { let rtx = self.database.begin_read().unwrap(); let sequence_number = rtx @@ -1088,25 +1162,28 @@ impl Index { .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY) .unwrap(); - let parent_sequence_number = InscriptionEntry::load( + let parent_sequences = InscriptionEntry::load( sequence_number_to_inscription_entry .get(sequence_number) .unwrap() .unwrap() .value(), ) - .parent - .unwrap(); + .parents; - let entry = InscriptionEntry::load( - sequence_number_to_inscription_entry - .get(parent_sequence_number) - .unwrap() - .unwrap() - .value(), - ); - - entry.id + parent_sequences + .into_iter() + .map(|parent_sequence_number| { + InscriptionEntry::load( + sequence_number_to_inscription_entry + .get(parent_sequence_number) + .unwrap() + .unwrap() + .value(), + ) + .id + }) + .collect() } pub(crate) fn get_children_by_sequence_number_paginated( @@ -1144,6 +1221,37 @@ impl Index { Ok((children, more)) } + pub(crate) fn get_parents_by_sequence_number_paginated( + &self, + parent_sequence_numbers: Vec, + page_index: usize, + ) -> Result<(Vec, bool)> { + const PAGE_SIZE: usize = 100; + let rtx = self.database.begin_read()?; + + let sequence_number_to_entry = rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?; + + let mut parents = parent_sequence_numbers + .iter() + .skip(page_index * PAGE_SIZE) + .take(PAGE_SIZE.saturating_add(1)) + .map(|sequence_number| { + sequence_number_to_entry + .get(sequence_number) + .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id) + .map_err(|err| err.into()) + }) + .collect::>>()?; + + let more_parents = parents.len() > PAGE_SIZE; + + if more_parents { + parents.pop(); + } + + Ok((parents, more_parents)) + } + pub(crate) fn get_etching(&self, txid: Txid) -> Result> { let rtx = self.database.begin_read()?; @@ -1158,7 +1266,7 @@ impl Index { let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; let entry = rune_id_to_rune_entry.get(&id.value())?.unwrap(); - Ok(Some(RuneEntry::load(entry.value()).spaced_rune())) + Ok(Some(RuneEntry::load(entry.value()).spaced_rune)) } pub(crate) fn get_inscription_ids_by_sat(&self, sat: Sat) -> Result> { @@ -1526,7 +1634,7 @@ impl Index { return Ok(false); } - if usize::try_from(outpoint.vout).unwrap() >= info.vout.len() { + if outpoint.vout.into_usize() >= info.vout.len() { return Ok(false); } @@ -1704,30 +1812,20 @@ impl Index { let rtx = index.database.begin_read()?; let sequence_number = match query { - query::Inscription::Id(id) => { - let inscription_id_to_sequence_number = - rtx.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?; - - let sequence_number = inscription_id_to_sequence_number - .get(&id.store())? - .map(|guard| guard.value()); - - drop(inscription_id_to_sequence_number); - - sequence_number - } - query::Inscription::Number(inscription_number) => { - let inscription_number_to_sequence_number = - rtx.open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?; - - let sequence_number = inscription_number_to_sequence_number - .get(inscription_number)? - .map(|guard| guard.value()); - - drop(inscription_number_to_sequence_number); - - sequence_number - } + query::Inscription::Id(id) => rtx + .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)? + .get(&id.store())? + .map(|guard| guard.value()), + query::Inscription::Number(inscription_number) => rtx + .open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)? + .get(inscription_number)? + .map(|guard| guard.value()), + query::Inscription::Sat(sat) => rtx + .open_multimap_table(SAT_TO_SEQUENCE_NUMBER)? + .get(sat.n())? + .next() + .transpose()? + .map(|guard| guard.value()), }; let Some(sequence_number) = sequence_number else { @@ -1817,23 +1915,27 @@ impl Index { { let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; let entry = rune_id_to_rune_entry.get(&rune_id.value())?.unwrap(); - Some(RuneEntry::load(entry.value()).spaced_rune()) + Some(RuneEntry::load(entry.value()).spaced_rune) } else { None }; - let parent = match entry.parent { - Some(parent) => Some( - InscriptionEntry::load( - sequence_number_to_inscription_entry - .get(parent)? - .unwrap() - .value(), + let parents = entry + .parents + .iter() + .take(4) + .map(|parent| { + Ok( + InscriptionEntry::load( + sequence_number_to_inscription_entry + .get(parent)? + .unwrap() + .value(), + ) + .id, ) - .id, - ), - None => None, - }; + }) + .collect::>>()?; let mut charms = entry.charms; @@ -1844,7 +1946,7 @@ impl Index { Ok(Some(InscriptionInfo { children, entry, - parent, + parents, output, satpoint, inscription, @@ -2037,13 +2139,13 @@ mod tests { let inscription = inscription("text/plain;charset=utf-8", "hello"); let template = TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }; { let context = Context::builder().build(); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(template.clone()); + let txid = context.core.broadcast_tx(template.clone()); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -2069,7 +2171,7 @@ mod tests { .arg("--first-inscription-height=3") .build(); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(template); + let txid = context.core.broadcast_tx(template); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -2088,13 +2190,13 @@ mod tests { let inscription = inscription("text/plain;charset=utf-8", "hello"); let template = TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }; { let context = Context::builder().build(); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(template.clone()); + let txid = context.core.broadcast_tx(template.clone()); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -2118,7 +2220,7 @@ mod tests { { let context = Context::builder().arg("--no-index-inscriptions").build(); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(template); + let txid = context.core.broadcast_tx(template); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -2168,9 +2270,9 @@ mod tests { inputs: &[(1, 0, 0, Default::default())], outputs: 2, fee: 0, - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(split_coinbase_output); + let txid = context.core.broadcast_tx(split_coinbase_output); context.mine_blocks(1); @@ -2193,10 +2295,10 @@ mod tests { let merge_coinbase_outputs = TransactionTemplate { inputs: &[(1, 0, 0, Default::default()), (2, 0, 0, Default::default())], fee: 0, - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(merge_coinbase_outputs); + let txid = context.core.broadcast_tx(merge_coinbase_outputs); context.mine_blocks(1); assert_eq!( @@ -2217,9 +2319,9 @@ mod tests { inputs: &[(1, 0, 0, Default::default())], outputs: 2, fee: 10, - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(fee_paying_tx); + let txid = context.core.broadcast_tx(fee_paying_tx); let coinbase_txid = context.mine_blocks(1)[0].txdata[0].txid(); assert_eq!( @@ -2250,15 +2352,15 @@ mod tests { let first_fee_paying_tx = TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 10, - ..Default::default() + ..default() }; let second_fee_paying_tx = TransactionTemplate { inputs: &[(2, 0, 0, Default::default())], fee: 10, - ..Default::default() + ..default() }; - context.rpc_server.broadcast_tx(first_fee_paying_tx); - context.rpc_server.broadcast_tx(second_fee_paying_tx); + context.core.broadcast_tx(first_fee_paying_tx); + context.core.broadcast_tx(second_fee_paying_tx); let coinbase_txid = context.mine_blocks(1)[0].txdata[0].txid(); @@ -2284,9 +2386,9 @@ mod tests { let no_value_output = TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(no_value_output); + let txid = context.core.broadcast_tx(no_value_output); context.mine_blocks(1); assert_eq!( @@ -2303,17 +2405,17 @@ mod tests { let no_value_output = TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }; - context.rpc_server.broadcast_tx(no_value_output); + context.core.broadcast_tx(no_value_output); context.mine_blocks(1); let no_value_input = TransactionTemplate { inputs: &[(2, 1, 0, Default::default())], fee: 0, - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(no_value_input); + let txid = context.core.broadcast_tx(no_value_input); context.mine_blocks(1); assert_eq!( @@ -2326,13 +2428,13 @@ mod tests { fn list_spent_output() { let context = Context::builder().arg("--index-sats").build(); context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 0, - ..Default::default() + ..default() }); context.mine_blocks(1); - let txid = context.rpc_server.tx(1, 0).txid(); + let txid = context.core.tx(1, 0).txid(); assert_matches!(context.index.list(OutPoint::new(txid, 0)).unwrap(), None); } @@ -2385,12 +2487,14 @@ mod tests { fn find_first_sat_of_second_block() { let context = Context::builder().arg("--index-sats").build(); context.mine_blocks(1); + let tx = context.core.tx(1, 0); assert_eq!( context.index.find(Sat(50 * COIN_VALUE)).unwrap().unwrap(), SatPoint { - outpoint: "84aca0d43f45ac753d4744f40b2f54edec3a496b298951735d450e601386089d:0" - .parse() - .unwrap(), + outpoint: OutPoint { + txid: tx.txid(), + vout: 0, + }, offset: 0, } ) @@ -2406,10 +2510,10 @@ mod tests { fn find_first_sat_spent_in_second_block() { let context = Context::builder().arg("--index-sats").build(); context.mine_blocks(1); - let spend_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let spend_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 0, - ..Default::default() + ..default() }); context.mine_blocks(1); assert_eq!( @@ -2426,9 +2530,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2450,17 +2554,17 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 50 * 100_000_000, - ..Default::default() + ..default() }); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2478,17 +2582,17 @@ mod tests { context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(4, 0, 0, Default::default())], fee: 50 * 100_000_000, - ..Default::default() + ..default() }); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(5, 1, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2511,9 +2615,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2528,9 +2632,9 @@ mod tests { Some(50 * COIN_VALUE), ); - let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let send_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2554,9 +2658,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(2); - let first_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let first_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let first_inscription_id = InscriptionId { @@ -2564,9 +2668,9 @@ mod tests { index: 0, }; - let second_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let second_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, inscription("text/png", [1; 100]).to_witness())], - ..Default::default() + ..default() }); let second_inscription_id = InscriptionId { txid: second_txid, @@ -2575,9 +2679,9 @@ mod tests { context.mine_blocks(1); - let merged_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let merged_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 1, 0, Default::default()), (3, 2, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2613,9 +2717,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2630,10 +2734,10 @@ mod tests { Some(50 * COIN_VALUE), ); - let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let send_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())], outputs: 2, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2661,9 +2765,9 @@ mod tests { let context = Context::builder().args(args).build(); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2678,9 +2782,9 @@ mod tests { Some(50 * COIN_VALUE), ); - let send_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let send_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2704,18 +2808,18 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, Default::default())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let coinbase_tx = context.mine_blocks(1)[0].txdata[0].txid(); @@ -2739,18 +2843,18 @@ mod tests { for context in Context::configurations() { context.mine_blocks(2); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Default::default()), (3, 1, 0, Default::default())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let coinbase_tx = context.mine_blocks(1)[0].txdata[0].txid(); @@ -2774,10 +2878,10 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2802,10 +2906,10 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -2827,10 +2931,10 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let first_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let first_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let first_inscription_id = InscriptionId { txid: first_txid, @@ -2840,10 +2944,10 @@ mod tests { context.mine_blocks_with_subsidy(1, 0); context.mine_blocks(1); - let second_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let second_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0, inscription("text/plain", "hello").to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let second_inscription_id = InscriptionId { txid: second_txid, @@ -2957,18 +3061,18 @@ mod tests { context.mine_blocks_with_subsidy(1, 0); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())], outputs: 2, - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 1, 1, Default::default()), (3, 1, 0, Default::default())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); context.mine_blocks_with_subsidy(1, 0); @@ -2988,11 +3092,11 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], outputs: 2, output_values: &[0, 50 * COIN_VALUE], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -3013,10 +3117,10 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks_with_subsidy(1, 0); @@ -3122,9 +3226,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3147,9 +3251,9 @@ mod tests { [inscription_id] ); - let send_id = context.rpc_server.broadcast_tx(TransactionTemplate { + let send_id = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3180,9 +3284,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let first = context.rpc_server.broadcast_tx(TransactionTemplate { + let first = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3215,9 +3319,9 @@ mod tests { Some(50 * COIN_VALUE), ); - let second = context.rpc_server.broadcast_tx(TransactionTemplate { + let second = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { @@ -3264,9 +3368,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3286,9 +3390,9 @@ mod tests { let mut ids = Vec::new(); for i in 0..101 { - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); ids.push(InscriptionId { txid, index: 0 }); @@ -3320,9 +3424,9 @@ mod tests { b"ord", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3357,9 +3461,9 @@ mod tests { b"ord", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3392,29 +3496,29 @@ mod tests { b"text/plain;charset=utf-8", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness.clone())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - assert_eq!(context.rpc_server.height(), 109); + assert_eq!(context.core.height(), 109); assert_eq!(context.index.inscription_number(inscription_id), -1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - assert_eq!(context.rpc_server.height(), 110); + assert_eq!(context.core.height(), 110); assert_eq!(context.index.inscription_number(inscription_id), 0); } @@ -3433,9 +3537,9 @@ mod tests { b"text/plain;charset=utf-8", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3453,9 +3557,9 @@ mod tests { let witness = envelope(&[b"ord", &[1]]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3482,9 +3586,9 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3512,9 +3616,9 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3531,17 +3635,17 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 50 * 100_000_000, - ..Default::default() + ..default() }); context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -3567,19 +3671,19 @@ mod tests { context.mine_blocks(1); // create zero value input - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 50 * 100_000_000, - ..Default::default() + ..default() }); context.mine_blocks(1); let witness = inscription("text/plain", "hello").to_witness(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, witness.clone()), (2, 1, 0, witness.clone())], - ..Default::default() + ..default() }); let second_inscription_id = InscriptionId { txid, index: 1 }; @@ -3608,13 +3712,13 @@ mod tests { let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, witness.clone()), (2, 0, 0, witness.clone()), (3, 0, 0, witness.clone()), ], - ..Default::default() + ..default() }); let first = InscriptionId { txid, index: 0 }; @@ -3659,7 +3763,7 @@ mod tests { #[test] fn multiple_inscriptions_same_input_are_cursed_reinscriptions() { for context in Context::configurations() { - context.rpc_server.mine_blocks(1); + context.core.mine_blocks(1); let script = script::Builder::new() .push_opcode(opcodes::OP_FALSE) @@ -3690,9 +3794,9 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let first = InscriptionId { txid, index: 0 }; @@ -3737,9 +3841,9 @@ mod tests { #[test] fn multiple_inscriptions_different_inputs_and_same_inputs() { for context in Context::configurations() { - context.rpc_server.mine_blocks(1); - context.rpc_server.mine_blocks(1); - context.rpc_server.mine_blocks(1); + context.core.mine_blocks(1); + context.core.mine_blocks(1); + context.core.mine_blocks(1); let script = script::Builder::new() .push_opcode(opcodes::OP_FALSE) @@ -3770,13 +3874,13 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, witness.clone()), (2, 0, 0, witness.clone()), (3, 0, 0, witness.clone()), ], - ..Default::default() + ..default() }); let first = InscriptionId { txid, index: 0 }; // normal @@ -3842,7 +3946,7 @@ mod tests { #[test] fn inscription_fee_distributed_evenly() { for context in Context::configurations() { - context.rpc_server.mine_blocks(1); + context.core.mine_blocks(1); let script = script::Builder::new() .push_opcode(opcodes::OP_FALSE) @@ -3873,10 +3977,10 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], fee: 33, - ..Default::default() + ..default() }); let first = InscriptionId { txid, index: 0 }; @@ -3914,10 +4018,10 @@ mod tests { let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); - let cursed_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let cursed_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness.clone()), (2, 0, 0, witness.clone())], outputs: 2, - ..Default::default() + ..default() }); let cursed = InscriptionId { @@ -3949,9 +4053,9 @@ mod tests { b"reinscription on cursed", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 1, 1, witness)], - ..Default::default() + ..default() }); let reinscription_on_cursed = InscriptionId { txid, index: 0 }; @@ -3979,10 +4083,10 @@ mod tests { let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); - let cursed_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let cursed_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness.clone()), (2, 0, 0, witness.clone())], outputs: 2, - ..Default::default() + ..default() }); let cursed = InscriptionId { @@ -4014,9 +4118,9 @@ mod tests { b"reinscription on cursed", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 1, 1, witness)], - ..Default::default() + ..default() }); let reinscription_on_cursed = InscriptionId { txid, index: 0 }; @@ -4042,9 +4146,9 @@ mod tests { b"second reinscription on cursed", ]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(4, 1, 0, witness)], - ..Default::default() + ..default() }); let second_reinscription_on_cursed = InscriptionId { txid, index: 0 }; @@ -4089,41 +4193,41 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let first = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 1, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let second = InscriptionId { txid, index: 0 }; context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 3, 1, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let third = InscriptionId { txid, index: 0 }; @@ -4150,16 +4254,16 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let mut inscription_ids = vec![]; + let mut inscription_ids = Vec::new(); for i in 1..=21 { - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( i, if i == 1 { 0 } else { 1 }, 0, inscription("text/plain;charset=utf-8", &format!("hello {}", i)).to_witness(), )], // for the first inscription use coinbase, otherwise use the previous tx - ..Default::default() + ..default() }); inscription_ids.push(InscriptionId { txid, index: 0 }); @@ -4201,14 +4305,14 @@ mod tests { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let first_id = InscriptionId { txid, index: 0 }; let first_location = SatPoint { @@ -4222,14 +4326,14 @@ mod tests { .index .assert_inscription_location(first_id, first_location, Some(50 * COIN_VALUE)); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let second_id = InscriptionId { txid, index: 0 }; let second_location = SatPoint { @@ -4243,7 +4347,7 @@ mod tests { .index .assert_inscription_location(second_id, second_location, Some(100 * COIN_VALUE)); - context.rpc_server.invalidate_tip(); + context.core.invalidate_tip(); context.mine_blocks(2); context @@ -4261,14 +4365,14 @@ mod tests { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let first_id = InscriptionId { txid, index: 0 }; let first_location = SatPoint { @@ -4278,14 +4382,14 @@ mod tests { context.mine_blocks(10); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let second_id = InscriptionId { txid, index: 0 }; let second_location = SatPoint { @@ -4299,15 +4403,15 @@ mod tests { .index .assert_inscription_location(second_id, second_location, Some(100 * COIN_VALUE)); - context.rpc_server.invalidate_tip(); - context.rpc_server.invalidate_tip(); - context.rpc_server.invalidate_tip(); + context.core.invalidate_tip(); + context.core.invalidate_tip(); + context.core.invalidate_tip(); context.mine_blocks(4); assert!(!context.index.inscription_exists(second_id).unwrap()); - context.rpc_server.invalidate_tip(); + context.core.invalidate_tip(); context.mine_blocks(2); @@ -4324,14 +4428,14 @@ mod tests { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); context.mine_blocks(11); @@ -4342,14 +4446,14 @@ mod tests { offset: 0, }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let second_id = InscriptionId { txid, index: 0 }; @@ -4365,7 +4469,7 @@ mod tests { .assert_inscription_location(second_id, second_location, Some(100 * COIN_VALUE)); for _ in 0..7 { - context.rpc_server.invalidate_tip(); + context.core.invalidate_tip(); } context.mine_blocks(9); @@ -4383,9 +4487,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4397,8 +4501,8 @@ mod tests { .get_inscription_entry(inscription_id) .unwrap() .unwrap() - .parent - .is_none()); + .parents + .is_empty()); } } @@ -4407,9 +4511,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4419,7 +4523,7 @@ mod tests { index: 0, }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 0, @@ -4427,12 +4531,12 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), )], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4444,8 +4548,8 @@ mod tests { .get_inscription_entry(inscription_id) .unwrap() .unwrap() - .parent - .is_none()); + .parents + .is_empty()); } } @@ -4454,9 +4558,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4466,7 +4570,7 @@ mod tests { index: 0, }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 1, @@ -4474,12 +4578,12 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), )], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4487,8 +4591,8 @@ mod tests { let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( - context.index.get_parent_by_inscription_id(inscription_id), - parent_inscription_id + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4501,14 +4605,375 @@ mod tests { } } + #[test] + fn inscription_with_two_parent_tags_and_parents_has_parent_entries() { + for context in Context::configurations() { + context.mine_blocks(2); + + let parent_txid_a = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + let parent_txid_b = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())], + ..default() + }); + + context.mine_blocks(1); + + let parent_inscription_id_a = InscriptionId { + txid: parent_txid_a, + index: 0, + }; + let parent_inscription_id_b = InscriptionId { + txid: parent_txid_b, + index: 0, + }; + + let multi_parent_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_inscription_id_a.value(), + parent_inscription_id_b.value(), + ], + ..default() + }; + let multi_parent_witness = multi_parent_inscription.to_witness(); + + let revelation_input = (3, 1, 0, multi_parent_witness); + + let parent_b_input = (3, 2, 0, Witness::new()); + + let txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[revelation_input, parent_b_input], + ..default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id_a, parent_inscription_id_b] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_a) + .unwrap(), + vec![inscription_id] + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_b) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_repeated_parent_tags_and_parents_has_singular_parent_entry() { + for context in Context::configurations() { + context.mine_blocks(1); + + let parent_txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + + context.mine_blocks(1); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + let txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[( + 2, + 1, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![parent_inscription_id.value(), parent_inscription_id.value()], + ..default() + } + .to_witness(), + )], + ..default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_distinct_parent_tag_encodings_for_same_parent_has_singular_parent_entry() { + for context in Context::configurations() { + context.mine_blocks(1); + + let parent_txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + + context.mine_blocks(1); + + let parent_inscription_id = InscriptionId { + txid: parent_txid, + index: 0, + }; + + let trailing_zero_inscription_id: Vec = parent_inscription_id + .value() + .into_iter() + .chain(vec![0, 0, 0, 0]) + .collect(); + + let txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[( + 2, + 1, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![parent_inscription_id.value(), trailing_zero_inscription_id], + ..default() + } + .to_witness(), + )], + ..default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_three_parent_tags_and_two_parents_has_two_parent_entries() { + for context in Context::configurations() { + context.mine_blocks(3); + + let parent_txid_a = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + let parent_txid_b = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())], + ..default() + }); + let parent_txid_c = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, inscription("text/plain", "wazzup").to_witness())], + ..default() + }); + + context.mine_blocks(1); + + let parent_inscription_id_a = InscriptionId { + txid: parent_txid_a, + index: 0, + }; + let parent_inscription_id_b = InscriptionId { + txid: parent_txid_b, + index: 0, + }; + let parent_inscription_id_c = InscriptionId { + txid: parent_txid_c, + index: 0, + }; + + let multi_parent_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_inscription_id_a.value(), + parent_inscription_id_b.value(), + parent_inscription_id_c.value(), + ], + ..default() + }; + let multi_parent_witness = multi_parent_inscription.to_witness(); + + let revealing_parent_a_input = (4, 1, 0, multi_parent_witness); + + let parent_c_input = (4, 3, 0, Witness::new()); + + let txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[revealing_parent_a_input, parent_c_input], + ..default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id_a, parent_inscription_id_c] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_a) + .unwrap(), + vec![inscription_id] + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_b) + .unwrap(), + Vec::new() + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_c) + .unwrap(), + vec![inscription_id] + ); + } + } + + #[test] + fn inscription_with_valid_and_malformed_parent_tags_only_lists_valid_entries() { + for context in Context::configurations() { + context.mine_blocks(3); + + let parent_txid_a = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + let parent_txid_b = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())], + ..default() + }); + let parent_txid_c = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, inscription("text/plain", "wazzup").to_witness())], + ..default() + }); + + context.mine_blocks(1); + + let parent_inscription_id_a = InscriptionId { + txid: parent_txid_a, + index: 0, + }; + let parent_inscription_id_b = InscriptionId { + txid: parent_txid_b, + index: 0, + }; + let parent_inscription_id_c = InscriptionId { + txid: parent_txid_c, + index: 0, + }; + + let malformed_inscription_id_b = parent_inscription_id_b + .value() + .into_iter() + .chain(iter::once(0)) + .collect(); + + let multi_parent_inscription = Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_inscription_id_a.value(), + malformed_inscription_id_b, + parent_inscription_id_c.value(), + ], + ..default() + }; + let multi_parent_witness = multi_parent_inscription.to_witness(); + + let revealing_parent_a_input = (4, 1, 0, multi_parent_witness); + let parent_b_input = (4, 2, 0, Witness::new()); + let parent_c_input = (4, 3, 0, Witness::new()); + + let txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[revealing_parent_a_input, parent_b_input, parent_c_input], + ..default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + assert_eq!( + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id_a, parent_inscription_id_c] + ); + + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_a) + .unwrap(), + vec![inscription_id] + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_b) + .unwrap(), + Vec::new() + ); + assert_eq!( + context + .index + .get_children_by_inscription_id(parent_inscription_id_c) + .unwrap(), + vec![inscription_id] + ); + } + } + #[test] fn parents_can_be_in_preceding_input() { for context in Context::configurations() { context.mine_blocks(1); - let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(2); @@ -4518,7 +4983,7 @@ mod tests { index: 0, }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ (2, 1, 0, Default::default()), ( @@ -4528,13 +4993,13 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), ], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4542,8 +5007,8 @@ mod tests { let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( - context.index.get_parent_by_inscription_id(inscription_id), - parent_inscription_id + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4561,9 +5026,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(2); @@ -4573,7 +5038,7 @@ mod tests { index: 0, }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ ( 3, @@ -4582,14 +5047,14 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), (2, 1, 0, Default::default()), ], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4597,8 +5062,8 @@ mod tests { let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( - context.index.get_parent_by_inscription_id(inscription_id), - parent_inscription_id + context.index.get_parents_by_inscription_id(inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4616,9 +5081,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4628,7 +5093,7 @@ mod tests { index: 0, }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 1, @@ -4636,18 +5101,16 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some( - parent_inscription_id - .value() - .into_iter() - .chain(iter::once(0)) - .collect(), - ), - ..Default::default() + parents: vec![parent_inscription_id + .value() + .into_iter() + .chain(iter::once(0)) + .collect()], + ..default() } .to_witness(), )], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4659,8 +5122,8 @@ mod tests { .get_inscription_entry(inscription_id) .unwrap() .unwrap() - .parent - .is_none()); + .parents + .is_empty()); } } @@ -4673,12 +5136,12 @@ mod tests { content_type: Some("text/plain".into()), body: Some("hello".into()), pointer: Some(100u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4705,12 +5168,12 @@ mod tests { content_type: Some("text/plain".into()), body: Some("hello".into()), pointer: Some((50 * COIN_VALUE).to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4737,13 +5200,13 @@ mod tests { content_type: Some("text/plain".into()), body: Some("hello".into()), pointer: Some((25 * COIN_VALUE).to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], fee: 25 * COIN_VALUE, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4770,12 +5233,12 @@ mod tests { content_type: Some("text/plain".into()), body: Some("pointer-child".into()), pointer: Some(0u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4800,9 +5263,9 @@ mod tests { for context in Context::configurations() { context.mine_blocks(1); - let parent_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "parent").to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4815,14 +5278,14 @@ mod tests { let child_inscription = Inscription { content_type: Some("text/plain".into()), body: Some("pointer-child".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], pointer: Some(0u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, child_inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4852,8 +5315,8 @@ mod tests { assert_eq!( context .index - .get_parent_by_inscription_id(child_inscription_id), - parent_inscription_id + .get_parents_by_inscription_id(child_inscription_id), + vec![parent_inscription_id] ); assert_eq!( @@ -4875,27 +5338,27 @@ mod tests { let builder = Inscription { pointer: Some(100u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); let builder = Inscription { pointer: Some(300_000u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); let builder = Inscription { pointer: Some(1_000_000u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4942,28 +5405,28 @@ mod tests { let builder = Inscription { pointer: Some(100u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); let builder = Inscription { pointer: Some(100_111u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); let builder = Inscription { pointer: Some(299_999u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], outputs: 3, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5010,31 +5473,31 @@ mod tests { content_type: Some("text/plain".into()), body: Some("hello jupiter".into()), pointer: Some((50 * COIN_VALUE).to_le_bytes().to_vec()), - ..Default::default() + ..default() }; let inscription_for_third_output = Inscription { content_type: Some("text/plain".into()), body: Some("hello mars".into()), pointer: Some((100 * COIN_VALUE).to_le_bytes().to_vec()), - ..Default::default() + ..default() }; let inscription_for_first_output = Inscription { content_type: Some("text/plain".into()), body: Some("hello world".into()), pointer: Some(0u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, inscription_for_second_output.to_witness()), (2, 0, 0, inscription_for_third_output.to_witness()), (3, 0, 0, inscription_for_first_output.to_witness()), ], outputs: 3, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5080,31 +5543,31 @@ mod tests { let first_inscription = Inscription { content_type: Some("text/plain".into()), body: Some("hello jupiter".into()), - ..Default::default() + ..default() }; let second_inscription = Inscription { content_type: Some("text/plain".into()), body: Some("hello mars".into()), pointer: Some(1u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; let third_inscription = Inscription { content_type: Some("text/plain".into()), body: Some("hello world".into()), pointer: Some(2u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, first_inscription.to_witness()), (2, 0, 0, second_inscription.to_witness()), (3, 0, 0, third_inscription.to_witness()), ], outputs: 1, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5150,23 +5613,23 @@ mod tests { let inscription = Inscription { content_type: Some("text/plain".into()), body: Some("hello jupiter".into()), - ..Default::default() + ..default() }; let cursed_reinscription = Inscription { content_type: Some("text/plain".into()), body: Some("hello mars".into()), pointer: Some(0u64.to_le_bytes().to_vec()), - ..Default::default() + ..default() }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, inscription.to_witness()), (2, 0, 0, cursed_reinscription.to_witness()), ], outputs: 2, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5208,10 +5671,10 @@ mod tests { let inscription = Inscription::default(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let blocks = context.mine_blocks(1); @@ -5239,10 +5702,10 @@ mod tests { let inscription = Inscription::default(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let blocks = context.mine_blocks_with_subsidy(1, 25 * COIN_VALUE); @@ -5282,9 +5745,9 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -5314,9 +5777,9 @@ mod tests { let inscription = Inscription::default(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5346,9 +5809,9 @@ mod tests { let inscription = Inscription::default(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 1, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5393,9 +5856,9 @@ mod tests { let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -5425,9 +5888,9 @@ mod tests { let inscription = Inscription::default(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(111, 1, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5457,9 +5920,9 @@ mod tests { let inscription = Inscription::default(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(112, 1, 0, inscription.to_witness())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5494,7 +5957,7 @@ mod tests { context.mine_blocks(1); let outpoint = OutPoint { - txid: context.rpc_server.tx(1, 0).into(), + txid: context.core.tx(1, 0).into(), vout: 0, }; @@ -5502,9 +5965,9 @@ mod tests { assert!(!ranges.is_empty()); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5523,7 +5986,7 @@ mod tests { context.mine_blocks(1); let outpoint = OutPoint { - txid: context.rpc_server.tx(1, 0).into(), + txid: context.core.tx(1, 0).into(), vout: 0, }; @@ -5533,9 +5996,9 @@ mod tests { assert_eq!(unspent_ranges, ranges); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5553,13 +6016,13 @@ mod tests { context.mine_blocks(1); let outpoint = OutPoint { - txid: context.rpc_server.tx(1, 0).into(), + txid: context.core.tx(1, 0).into(), vout: 0, }; - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5573,18 +6036,18 @@ mod tests { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks_with_update(1, false); let outpoint = OutPoint { txid, vout: 0 }; - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5607,14 +6070,14 @@ mod tests { assert!(!context .index .is_output_spent(OutPoint { - txid: context.rpc_server.tx(1, 0).txid(), + txid: context.core.tx(1, 0).txid(), vout: 0, }) .unwrap()); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5622,7 +6085,7 @@ mod tests { assert!(context .index .is_output_spent(OutPoint { - txid: context.rpc_server.tx(1, 0).txid(), + txid: context.core.tx(1, 0).txid(), vout: 0, }) .unwrap()); @@ -5647,7 +6110,7 @@ mod tests { assert!(context .index .is_output_in_active_chain(OutPoint { - txid: context.rpc_server.tx(1, 0).txid(), + txid: context.core.tx(1, 0).txid(), vout: 0, }) .unwrap()); @@ -5655,7 +6118,7 @@ mod tests { assert!(!context .index .is_output_in_active_chain(OutPoint { - txid: context.rpc_server.tx(1, 0).txid(), + txid: context.core.tx(1, 0).txid(), vout: 1, }) .unwrap()); @@ -5674,17 +6137,17 @@ mod tests { for context in Context::configurations() { context.mine_blocks(2); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); let a = InscriptionId { txid, index: 0 }; - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let b = InscriptionId { txid, index: 0 }; @@ -5704,11 +6167,11 @@ mod tests { context.mine_blocks(1); let inscription = Inscription::default(); - let create_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let create_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], fee: 0, outputs: 1, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5733,16 +6196,16 @@ mod tests { sequence_number: 0, block_height: 2, charms: expected_charms, - parent_inscription_id: None + parent_inscription_ids: Vec::new(), } ); // Transfer inscription - let transfer_txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let transfer_txid = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, Default::default())], fee: 0, outputs: 1, - ..Default::default() + ..default() }); context.mine_blocks(1); diff --git a/src/index/entry.rs b/src/index/entry.rs index 91923082fe..f3cae51f22 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -28,70 +28,146 @@ impl Entry for Header { } } +impl Entry for Rune { + type Value = u128; + + fn load(value: Self::Value) -> Self { + Self(value) + } + + fn store(self) -> Self::Value { + self.0 + } +} + #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub struct RuneEntry { + pub block: u64, pub burned: u128, pub divisibility: u8, pub etching: Txid, - pub mint: Option, - pub mints: u64, + pub mints: u128, pub number: u64, - pub rune: Rune, - pub spacers: u32, - pub supply: u128, + pub premine: u128, + pub spaced_rune: SpacedRune, pub symbol: Option, - pub timestamp: u32, + pub terms: Option, + pub timestamp: u64, } -pub(super) type RuneEntryValue = ( - u128, // burned - u8, // divisibility - (u128, u128), // etching - Option, // mint parameters - u64, // mints - u64, // number - u128, // rune - u32, // spacers - u128, // supply - Option, // symbol - u32, // timestamp -); +impl RuneEntry { + pub fn mintable(&self, height: u64) -> Result { + let Some(terms) = self.terms else { + return Err(MintError::Unmintable); + }; -#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Default)] -pub struct MintEntry { - pub deadline: Option, - pub end: Option, - pub limit: Option, -} + if let Some(start) = self.start() { + if height < start { + return Err(MintError::Start(start)); + } + } -type MintEntryValue = ( - Option, // deadline - Option, // end - Option, // limit -); + if let Some(end) = self.end() { + if height >= end { + return Err(MintError::End(end)); + } + } -impl RuneEntry { - pub(crate) fn spaced_rune(&self) -> SpacedRune { - SpacedRune { - rune: self.rune, - spacers: self.spacers, + let cap = terms.cap.unwrap_or_default(); + + if self.mints >= cap { + return Err(MintError::Cap(cap)); + } + + Ok(terms.amount.unwrap_or_default()) + } + + pub fn supply(&self) -> u128 { + self.premine + + self.mints + * self + .terms + .and_then(|terms| terms.amount) + .unwrap_or_default() + } + + pub fn pile(&self, amount: u128) -> Pile { + Pile { + amount, + divisibility: self.divisibility, + symbol: self.symbol, } } + + pub fn start(&self) -> Option { + let terms = self.terms?; + + let relative = terms + .offset + .0 + .map(|offset| self.block.saturating_add(offset)); + + let absolute = terms.height.0; + + relative + .zip(absolute) + .map(|(relative, absolute)| relative.max(absolute)) + .or(relative) + .or(absolute) + } + + pub fn end(&self) -> Option { + let terms = self.terms?; + + let relative = terms + .offset + .1 + .map(|offset| self.block.saturating_add(offset)); + + let absolute = terms.height.1; + + relative + .zip(absolute) + .map(|(relative, absolute)| relative.min(absolute)) + .or(relative) + .or(absolute) + } } +type TermsEntryValue = ( + Option, // cap + (Option, Option), // height + Option, // amount + (Option, Option), // offset +); + +pub(super) type RuneEntryValue = ( + u64, // block + u128, // burned + u8, // divisibility + (u128, u128), // etching + u128, // mints + u64, // number + u128, // premine + (u128, u32), // spaced rune + Option, // symbol + Option, // terms + u64, // timestamp +); + impl Default for RuneEntry { fn default() -> Self { Self { + block: 0, burned: 0, divisibility: 0, etching: Txid::all_zeros(), - mint: None, mints: 0, number: 0, - rune: Rune(0), - spacers: 0, - supply: 0, + premine: 0, + spaced_rune: SpacedRune::default(), symbol: None, + terms: None, timestamp: 0, } } @@ -102,20 +178,21 @@ impl Entry for RuneEntry { fn load( ( + block, burned, divisibility, etching, - mint, mints, number, - rune, - spacers, - supply, + premine, + (rune, spacers), symbol, + terms, timestamp, ): RuneEntryValue, ) -> Self { Self { + block, burned, divisibility, etching: { @@ -128,23 +205,27 @@ impl Entry for RuneEntry { high[14], high[15], ]) }, - mint: mint.map(|(deadline, end, limit)| MintEntry { - deadline, - end, - limit, - }), mints, number, - rune: Rune(rune), - spacers, - supply, + premine, + spaced_rune: SpacedRune { + rune: Rune(rune), + spacers, + }, symbol, + terms: terms.map(|(cap, height, amount, offset)| Terms { + cap, + height, + amount, + offset, + }), timestamp, } } fn store(self) -> Self::Value { ( + self.block, self.burned, self.divisibility, { @@ -160,46 +241,46 @@ impl Entry for RuneEntry { ]), ) }, - self.mint.map( - |MintEntry { - deadline, - end, - limit, - }| (deadline, end, limit), - ), self.mints, self.number, - self.rune.0, - self.spacers, - self.supply, + self.premine, + (self.spaced_rune.rune.0, self.spaced_rune.spacers), self.symbol, + self.terms.map( + |Terms { + cap, + height, + amount, + offset, + }| (cap, height, amount, offset), + ), self.timestamp, ) } } -pub(super) type RuneIdValue = (u32, u16); +pub(super) type RuneIdValue = (u64, u32); impl Entry for RuneId { type Value = RuneIdValue; - fn load((height, index): Self::Value) -> Self { - Self { height, index } + fn load((block, tx): Self::Value) -> Self { + Self { block, tx } } fn store(self) -> Self::Value { - (self.height, self.index) + (self.block, self.tx) } } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq, Clone)] pub(crate) struct InscriptionEntry { pub(crate) charms: u16, pub(crate) fee: u64, pub(crate) height: u32, pub(crate) id: InscriptionId, pub(crate) inscription_number: i32, - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) sat: Option, pub(crate) sequence_number: u32, pub(crate) timestamp: u32, @@ -211,7 +292,7 @@ pub(crate) type InscriptionEntryValue = ( u32, // height InscriptionIdValue, // inscription id i32, // inscription number - Option, // parent + Vec, // parents Option, // sat u32, // sequence number u32, // timestamp @@ -228,7 +309,7 @@ impl Entry for InscriptionEntry { height, id, inscription_number, - parent, + parents, sat, sequence_number, timestamp, @@ -240,7 +321,7 @@ impl Entry for InscriptionEntry { height, id: InscriptionId::load(id), inscription_number, - parent, + parents, sat: sat.map(Sat), sequence_number, timestamp, @@ -254,7 +335,7 @@ impl Entry for InscriptionEntry { self.height, self.id.store(), self.inscription_number, - self.parent, + self.parents, self.sat.map(Sat::n), self.sequence_number, self.timestamp, @@ -397,6 +478,30 @@ impl Entry for Txid { mod tests { use super::*; + #[test] + fn inscription_entry() { + let id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0" + .parse::() + .unwrap(); + + let entry = InscriptionEntry { + charms: 0, + fee: 1, + height: 2, + id, + inscription_number: 3, + parents: vec![4, 5, 6], + sat: Some(Sat(7)), + sequence_number: 8, + timestamp: 9, + }; + + let value = (0, 1, 2, id.store(), 3, vec![4, 5, 6], Some(7), 8, 9); + + assert_eq!(entry.clone().store(), value); + assert_eq!(InscriptionEntry::load(value), entry); + } + #[test] fn inscription_id_entry() { let inscription_id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0" @@ -444,6 +549,7 @@ mod tests { #[test] fn rune_entry() { let entry = RuneEntry { + block: 12, burned: 1, divisibility: 3, etching: Txid::from_byte_array([ @@ -451,34 +557,37 @@ mod tests { 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, ]), - mint: Some(MintEntry { - deadline: Some(2), - end: Some(4), - limit: Some(5), + terms: Some(Terms { + cap: Some(1), + height: (Some(2), Some(3)), + amount: Some(4), + offset: (Some(5), Some(6)), }), mints: 11, number: 6, - rune: Rune(7), - spacers: 8, - supply: 9, + premine: 12, + spaced_rune: SpacedRune { + rune: Rune(7), + spacers: 8, + }, symbol: Some('a'), timestamp: 10, }; let value = ( + 12, 1, 3, ( 0x0F0E0D0C0B0A09080706050403020100, 0x1F1E1D1C1B1A19181716151413121110, ), - Some((Some(2), Some(4), Some(5))), 11, 6, - 7, - 8, - 9, + 12, + (7, 8), Some('a'), + Some((Some(1), (Some(2), Some(3)), Some(4), (Some(5), Some(6)))), 10, ); @@ -488,22 +597,9 @@ mod tests { #[test] fn rune_id_entry() { - assert_eq!( - RuneId { - height: 1, - index: 2, - } - .store(), - (1, 2), - ); + assert_eq!(RuneId { block: 1, tx: 2 }.store(), (1, 2),); - assert_eq!( - RuneId { - height: 1, - index: 2, - }, - RuneId::load((1, 2)), - ); + assert_eq!(RuneId { block: 1, tx: 2 }, RuneId::load((1, 2)),); } #[test] @@ -520,4 +616,294 @@ mod tests { assert_eq!(actual, expected); } + + #[test] + fn mintable_default() { + assert_eq!(RuneEntry::default().mintable(0), Err(MintError::Unmintable)); + } + + #[test] + fn mintable_cap() { + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .mintable(0), + Ok(1000), + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + ..default() + }), + mints: 1, + ..default() + } + .mintable(0), + Err(MintError::Cap(1)), + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: None, + amount: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .mintable(0), + Err(MintError::Cap(0)), + ); + } + + #[test] + fn mintable_offset_start() { + assert_eq!( + RuneEntry { + block: 1, + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + offset: (Some(1), None), + ..default() + }), + mints: 0, + ..default() + } + .mintable(1), + Err(MintError::Start(2)), + ); + + assert_eq!( + RuneEntry { + block: 1, + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + offset: (Some(1), None), + ..default() + }), + mints: 0, + ..default() + } + .mintable(2), + Ok(1000), + ); + } + + #[test] + fn mintable_offset_end() { + assert_eq!( + RuneEntry { + block: 1, + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + offset: (None, Some(1)), + ..default() + }), + mints: 0, + ..default() + } + .mintable(1), + Ok(1000), + ); + + assert_eq!( + RuneEntry { + block: 1, + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + offset: (None, Some(1)), + ..default() + }), + mints: 0, + ..default() + } + .mintable(2), + Err(MintError::End(2)), + ); + } + + #[test] + fn mintable_height_start() { + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + height: (Some(1), None), + ..default() + }), + mints: 0, + ..default() + } + .mintable(0), + Err(MintError::Start(1)), + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + height: (Some(1), None), + ..default() + }), + mints: 0, + ..default() + } + .mintable(1), + Ok(1000), + ); + } + + #[test] + fn mintable_height_end() { + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + height: (None, Some(1)), + ..default() + }), + mints: 0, + ..default() + } + .mintable(0), + Ok(1000), + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + height: (None, Some(1)), + ..default() + }), + mints: 0, + ..default() + } + .mintable(1), + Err(MintError::End(1)), + ); + } + + #[test] + fn mintable_multiple_terms() { + let entry = RuneEntry { + terms: Some(Terms { + cap: Some(1), + amount: Some(1000), + height: (Some(10), Some(20)), + offset: (Some(0), Some(10)), + }), + block: 10, + mints: 0, + ..default() + }; + + assert_eq!(entry.mintable(10), Ok(1000)); + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().cap = None; + assert_eq!(entry.mintable(10), Err(MintError::Cap(0))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().height.0 = Some(11); + assert_eq!(entry.mintable(10), Err(MintError::Start(11))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().height.1 = Some(10); + assert_eq!(entry.mintable(10), Err(MintError::End(10))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().offset.0 = Some(1); + assert_eq!(entry.mintable(10), Err(MintError::Start(11))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().offset.1 = Some(0); + assert_eq!(entry.mintable(10), Err(MintError::End(10))); + } + } + + #[test] + fn supply() { + assert_eq!( + RuneEntry { + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .supply(), + 0 + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + mints: 1, + ..default() + } + .supply(), + 1000 + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + mints: 0, + premine: 1, + ..default() + } + .supply(), + 1 + ); + + assert_eq!( + RuneEntry { + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + mints: 1, + premine: 1, + ..default() + } + .supply(), + 1001 + ); + } } diff --git a/src/index/event.rs b/src/index/event.rs index c650691158..997a70ebb4 100644 --- a/src/index/event.rs +++ b/src/index/event.rs @@ -7,7 +7,7 @@ pub enum Event { charms: u16, inscription_id: InscriptionId, location: Option, - parent_inscription_id: Option, + parent_inscription_ids: Vec, sequence_number: u32, }, InscriptionTransferred { diff --git a/src/index/lot.rs b/src/index/lot.rs new file mode 100644 index 0000000000..0305c6e6f0 --- /dev/null +++ b/src/index/lot.rs @@ -0,0 +1,163 @@ +use { + super::*, + std::{ + cmp::{PartialEq, PartialOrd}, + ops::{Add, AddAssign, Div, Rem, Sub, SubAssign}, + }, +}; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Default, Serialize, Deserialize)] +pub(super) struct Lot(pub(super) u128); + +impl Lot { + #[cfg(test)] + const MAX: Self = Self(u128::MAX); + + pub(super) fn n(self) -> u128 { + self.0 + } + + fn checked_add(self, rhs: Self) -> Option { + Some(Self(self.0.checked_add(rhs.0)?)) + } + + fn checked_sub(self, rhs: Self) -> Option { + Some(Self(self.0.checked_sub(rhs.0)?)) + } +} + +impl TryFrom for usize { + type Error = >::Error; + fn try_from(lot: Lot) -> Result { + usize::try_from(lot.0) + } +} + +impl Add for Lot { + type Output = Self; + fn add(self, other: Self) -> Self::Output { + self.checked_add(other).expect("lot overflow") + } +} + +impl AddAssign for Lot { + fn add_assign(&mut self, other: Self) { + *self = *self + other; + } +} + +impl Add for Lot { + type Output = Self; + fn add(self, other: u128) -> Self::Output { + self + Lot(other) + } +} + +impl AddAssign for Lot { + fn add_assign(&mut self, other: u128) { + *self += Lot(other); + } +} + +impl Sub for Lot { + type Output = Self; + fn sub(self, other: Self) -> Self::Output { + self.checked_sub(other).expect("lot underflow") + } +} + +impl SubAssign for Lot { + fn sub_assign(&mut self, other: Self) { + *self = *self - other; + } +} + +impl Div for Lot { + type Output = Self; + fn div(self, other: u128) -> Self::Output { + Lot(self.0 / other) + } +} + +impl Rem for Lot { + type Output = Self; + fn rem(self, other: u128) -> Self::Output { + Lot(self.0 % other) + } +} + +impl PartialEq for Lot { + fn eq(&self, other: &u128) -> bool { + self.0 == *other + } +} + +impl PartialOrd for Lot { + fn partial_cmp(&self, other: &u128) -> Option { + self.0.partial_cmp(other) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "lot overflow")] + fn add() { + let _ = Lot::MAX + 1; + } + + #[test] + #[should_panic(expected = "lot overflow")] + fn add_assign() { + let mut l = Lot::MAX; + l += Lot(1); + } + + #[test] + #[should_panic(expected = "lot overflow")] + fn add_u128() { + let _ = Lot::MAX + 1; + } + + #[test] + #[should_panic(expected = "lot overflow")] + fn add_assign_u128() { + let mut l = Lot::MAX; + l += 1; + } + + #[test] + #[should_panic(expected = "lot underflow")] + fn sub() { + let _ = Lot(0) - Lot(1); + } + + #[test] + #[should_panic(expected = "lot underflow")] + fn sub_assign() { + let mut l = Lot(0); + l -= Lot(1); + } + + #[test] + fn div() { + assert_eq!(Lot(100) / 2, Lot(50)); + } + + #[test] + fn rem() { + assert_eq!(Lot(77) % 8, Lot(5)); + } + + #[test] + fn partial_eq() { + assert_eq!(Lot(100), 100); + } + + #[test] + fn partial_ord() { + assert!(Lot(100) > 10); + } +} diff --git a/src/index/reorg.rs b/src/index/reorg.rs index 4004226a71..1a7ed999cf 100644 --- a/src/index/reorg.rs +++ b/src/index/reorg.rs @@ -1,23 +1,23 @@ use {super::*, updater::BlockData}; #[derive(Debug, PartialEq)] -pub(crate) enum ReorgError { +pub(crate) enum Error { Recoverable { height: u32, depth: u32 }, Unrecoverable, } -impl Display for ReorgError { +impl Display for Error { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - ReorgError::Recoverable { height, depth } => { + Self::Recoverable { height, depth } => { write!(f, "{depth} block deep reorg detected at height {height}") } - ReorgError::Unrecoverable => write!(f, "unrecoverable reorg detected"), + Self::Unrecoverable => write!(f, "unrecoverable reorg detected"), } } } -impl std::error::Error for ReorgError {} +impl std::error::Error for Error {} const MAX_SAVEPOINTS: u32 = 2; const SAVEPOINT_INTERVAL: u32 = 10; @@ -43,11 +43,11 @@ impl Reorg { .into_option()?; if index_block_hash == bitcoind_block_hash { - return Err(anyhow!(ReorgError::Recoverable { height, depth })); + return Err(anyhow!(reorg::Error::Recoverable { height, depth })); } } - Err(anyhow!(ReorgError::Unrecoverable)) + Err(anyhow!(reorg::Error::Unrecoverable)) } _ => Ok(()), } diff --git a/src/index/rtx.rs b/src/index/rtx.rs index fb02a06453..776494ba5e 100644 --- a/src/index/rtx.rs +++ b/src/index/rtx.rs @@ -1,8 +1,8 @@ use super::*; -pub(crate) struct Rtx<'a>(pub(crate) redb::ReadTransaction<'a>); +pub(crate) struct Rtx(pub(crate) redb::ReadTransaction); -impl Rtx<'_> { +impl Rtx { pub(crate) fn block_height(&self) -> Result> { Ok( self diff --git a/src/index/testing.rs b/src/index/testing.rs index 7788480a02..6513b9c5ab 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -1,4 +1,4 @@ -use {super::*, std::ffi::OsString, tempfile::TempDir}; +use {super::*, bitcoin::script::PushBytes, std::ffi::OsString, tempfile::TempDir}; pub(crate) struct ContextBuilder { args: Vec, @@ -13,9 +13,7 @@ impl ContextBuilder { } pub(crate) fn try_build(self) -> Result { - let rpc_server = test_bitcoincore_rpc::builder() - .network(self.chain.network()) - .build(); + let core = mockcore::builder().network(self.chain.network()).build(); let tempdir = self.tempdir.unwrap_or_else(|| TempDir::new().unwrap()); let cookie_file = tempdir.path().join("cookie"); @@ -24,8 +22,8 @@ impl ContextBuilder { let command: Vec = vec![ "ord".into(), "--bitcoin-rpc-url".into(), - rpc_server.url().into(), - "--data-dir".into(), + core.url().into(), + "--datadir".into(), tempdir.path().into(), "--cookie-file".into(), cookie_file.into(), @@ -41,7 +39,7 @@ impl ContextBuilder { Ok(Context { index, - rpc_server, + core, tempdir, }) } @@ -74,7 +72,7 @@ impl ContextBuilder { pub(crate) struct Context { pub(crate) index: Index, - pub(crate) rpc_server: test_bitcoincore_rpc::Handle, + pub(crate) core: mockcore::Handle, #[allow(unused)] pub(crate) tempdir: TempDir, } @@ -89,12 +87,14 @@ impl Context { } } + #[track_caller] pub(crate) fn mine_blocks(&self, n: u64) -> Vec { self.mine_blocks_with_update(n, true) } + #[track_caller] pub(crate) fn mine_blocks_with_update(&self, n: u64, update: bool) -> Vec { - let blocks = self.rpc_server.mine_blocks(n); + let blocks = self.core.mine_blocks(n); if update { self.index.update().unwrap(); } @@ -102,7 +102,7 @@ impl Context { } pub(crate) fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec { - let blocks = self.rpc_server.mine_blocks_with_subsidy(n, subsidy); + let blocks = self.core.mine_blocks_with_subsidy(n, subsidy); self.index.update().unwrap(); blocks } @@ -145,8 +145,61 @@ impl Context { for (id, entry) in runes { pretty_assert_eq!( outstanding.get(id).copied().unwrap_or_default(), - entry.supply - entry.burned + entry.supply() - entry.burned ); } } + + pub(crate) fn etch(&self, runestone: Runestone, outputs: usize) -> (Txid, RuneId) { + let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap(); + + self.mine_blocks(1); + + self.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..default() + }); + + self.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + + let mut witness = Witness::new(); + + if let Some(etching) = runestone.etching { + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + etching + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + + witness.push(tapscript); + } else { + witness.push(ScriptBuf::new()); + } + + witness.push([]); + + let txid = self.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs, + ..default() + }); + + self.mine_blocks(1); + + ( + txid, + RuneId { + block: u64::try_from(block_count + usize::from(Runestone::COMMIT_INTERVAL) + 1).unwrap(), + tx: 1, + }, + ) + } } diff --git a/src/index/updater.rs b/src/index/updater.rs index aa3697d2b0..694934b04d 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -41,7 +41,7 @@ pub(crate) struct Updater<'index> { } impl<'index> Updater<'index> { - pub(crate) fn update_index<'a>(&'a mut self, mut wtx: WriteTransaction<'a>) -> Result { + pub(crate) fn update_index(&mut self, mut wtx: WriteTransaction) -> Result { let start = Instant::now(); let starting_height = u32::try_from(self.index.client.get_block_count()?).unwrap() + 1; let starting_index_height = self.height; @@ -321,7 +321,7 @@ impl<'index> Updater<'index> { log::info!( "Block {} at {} with {} transactions…", self.height, - timestamp(block.header.time), + timestamp(block.header.time.into()), block.txdata.len() ); @@ -591,38 +591,29 @@ impl<'index> Updater<'index> { .unwrap_or(0); let mut rune_updater = RuneUpdater { + block_time: block.header.time, + burned: HashMap::new(), + client: &self.index.client, height: self.height, id_to_entry: &mut rune_id_to_rune_entry, inscription_id_to_sequence_number: &mut inscription_id_to_sequence_number, - minimum: Rune::minimum_at_height(self.index.settings.chain(), Height(self.height)), + minimum: Rune::minimum_at_height( + self.index.settings.chain().network(), + Height(self.height), + ), outpoint_to_balances: &mut outpoint_to_rune_balances, rune_to_id: &mut rune_to_rune_id, runes, sequence_number_to_rune_id: &mut sequence_number_to_rune_id, statistic_to_count: &mut statistic_to_count, - timestamp: block.header.time, transaction_id_to_rune: &mut transaction_id_to_rune, - updates: HashMap::new(), }; for (i, (tx, txid)) in block.txdata.iter().enumerate() { - rune_updater.index_runes(i, tx, *txid)?; + rune_updater.index_runes(u32::try_from(i).unwrap(), tx, *txid)?; } - for (rune_id, update) in rune_updater.updates { - let mut entry = RuneEntry::load( - rune_id_to_rune_entry - .get(&rune_id.store())? - .unwrap() - .value(), - ); - - entry.burned += update.burned; - entry.mints += update.mints; - entry.supply += update.supply; - - rune_id_to_rune_entry.insert(&rune_id.store(), entry.store())?; - } + rune_updater.update()?; } height_to_block_header.insert(&self.height, &block.header.store())?; diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index ee10151f1e..41dd42c538 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -26,7 +26,7 @@ enum Origin { cursed: bool, fee: u64, hidden: bool, - parent: Option, + parents: Vec, pointer: Option, reinscription: bool, unbound: bool, @@ -37,39 +37,37 @@ enum Origin { }, } -pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { +pub(super) struct InscriptionUpdater<'a, 'tx> { pub(super) blessed_inscription_count: u64, pub(super) chain: Chain, - pub(super) content_type_to_count: &'a mut Table<'db, 'tx, Option<&'static [u8]>, u64>, + pub(super) content_type_to_count: &'a mut Table<'tx, Option<&'static [u8]>, u64>, pub(super) cursed_inscription_count: u64, pub(super) event_sender: Option<&'a Sender>, pub(super) flotsam: Vec, pub(super) height: u32, pub(super) home_inscription_count: u64, - pub(super) home_inscriptions: &'a mut Table<'db, 'tx, u32, InscriptionIdValue>, - pub(super) id_to_sequence_number: &'a mut Table<'db, 'tx, InscriptionIdValue, u32>, + pub(super) home_inscriptions: &'a mut Table<'tx, u32, InscriptionIdValue>, + pub(super) id_to_sequence_number: &'a mut Table<'tx, InscriptionIdValue, u32>, pub(super) index_transactions: bool, - pub(super) inscription_number_to_sequence_number: &'a mut Table<'db, 'tx, i32, u32>, + pub(super) inscription_number_to_sequence_number: &'a mut Table<'tx, i32, u32>, pub(super) lost_sats: u64, pub(super) next_sequence_number: u32, - pub(super) outpoint_to_value: &'a mut Table<'db, 'tx, &'static OutPointValue, u64>, + pub(super) outpoint_to_value: &'a mut Table<'tx, &'static OutPointValue, u64>, pub(super) reward: u64, pub(super) transaction_buffer: Vec, - pub(super) transaction_id_to_transaction: - &'a mut Table<'db, 'tx, &'static TxidValue, &'static [u8]>, - pub(super) sat_to_sequence_number: &'a mut MultimapTable<'db, 'tx, u64, u32>, - pub(super) satpoint_to_sequence_number: - &'a mut MultimapTable<'db, 'tx, &'static SatPointValue, u32>, - pub(super) sequence_number_to_children: &'a mut MultimapTable<'db, 'tx, u32, u32>, - pub(super) sequence_number_to_entry: &'a mut Table<'db, 'tx, u32, InscriptionEntryValue>, - pub(super) sequence_number_to_satpoint: &'a mut Table<'db, 'tx, u32, &'static SatPointValue>, + pub(super) transaction_id_to_transaction: &'a mut Table<'tx, &'static TxidValue, &'static [u8]>, + pub(super) sat_to_sequence_number: &'a mut MultimapTable<'tx, u64, u32>, + pub(super) satpoint_to_sequence_number: &'a mut MultimapTable<'tx, &'static SatPointValue, u32>, + pub(super) sequence_number_to_children: &'a mut MultimapTable<'tx, u32, u32>, + pub(super) sequence_number_to_entry: &'a mut Table<'tx, u32, InscriptionEntryValue>, + pub(super) sequence_number_to_satpoint: &'a mut Table<'tx, u32, &'static SatPointValue>, pub(super) timestamp: u32, pub(super) unbound_inscriptions: u64, pub(super) value_cache: &'a mut HashMap, pub(super) value_receiver: &'a mut Receiver, } -impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { +impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { pub(super) fn index_inscriptions( &mut self, tx: &Transaction, @@ -215,7 +213,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { cursed: curse.is_some() && !jubilant, fee: 0, hidden: inscription.payload.hidden(), - parent: inscription.payload.parent(), + parents: inscription.payload.parents(), pointer: inscription.payload.pointer(), reinscription: inscribed_offsets.get(&offset).is_some(), unbound: current_input_value == 0 @@ -253,15 +251,16 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { for flotsam in &mut floating_inscriptions { if let Flotsam { - origin: Origin::New { parent, .. }, + origin: Origin::New { + parents: purported_parents, + .. + }, .. } = flotsam { - if let Some(purported_parent) = parent { - if !potential_parents.contains(purported_parent) { - *parent = None; - } - } + let mut seen = HashSet::new(); + purported_parents + .retain(|parent| seen.insert(*parent) && potential_parents.contains(parent)); } } @@ -423,7 +422,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { cursed, fee, hidden, - parent, + parents, pointer: _, reinscription, unbound, @@ -463,21 +462,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } if let Some(sat) = sat { - if sat.nineball() { - Charm::Nineball.set(&mut charms); - } - - if sat.coin() { - Charm::Coin.set(&mut charms); - } - - match sat.rarity() { - Rarity::Common | Rarity::Mythic => {} - Rarity::Uncommon => Charm::Uncommon.set(&mut charms), - Rarity::Rare => Charm::Rare.set(&mut charms), - Rarity::Epic => Charm::Epic.set(&mut charms), - Rarity::Legendary => Charm::Legendary.set(&mut charms), - } + charms |= sat.charms(); } if new_satpoint.outpoint == OutPoint::null() { @@ -496,21 +481,22 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { self.sat_to_sequence_number.insert(&n, &sequence_number)?; } - let parent_sequence_number = match parent { - Some(parent_id) => { + let parent_sequence_numbers = parents + .iter() + .map(|parent| { let parent_sequence_number = self .id_to_sequence_number - .get(&parent_id.store())? + .get(&parent.store())? .unwrap() .value(); + self .sequence_number_to_children .insert(parent_sequence_number, sequence_number)?; - Some(parent_sequence_number) - } - None => None, - }; + Ok(parent_sequence_number) + }) + .collect::>>()?; if let Some(sender) = self.event_sender { sender.blocking_send(Event::InscriptionCreated { @@ -518,7 +504,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { charms, inscription_id, location: (!unbound).then_some(new_satpoint), - parent_inscription_id: parent, + parent_inscription_ids: parents, sequence_number, })?; } @@ -531,7 +517,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { height: self.height, id: inscription_id, inscription_number, - parent: parent_sequence_number, + parents: parent_sequence_numbers, sat, sequence_number, timestamp: self.timestamp, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 1a1096a1d4..744ff587b9 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -1,112 +1,67 @@ -use { - super::*, - crate::runes::{varint, Edict, Runestone}, -}; - -struct Claim { - id: u128, - limit: u128, -} - -struct Etched { - balance: u128, - divisibility: u8, - id: u128, - mint: Option, - rune: Rune, - spacers: u32, - symbol: Option, -} - -#[derive(Default)] -pub(crate) struct RuneUpdate { - pub(crate) burned: u128, - pub(crate) mints: u64, - pub(crate) supply: u128, -} +use super::*; -pub(super) struct RuneUpdater<'a, 'db, 'tx> { +pub(super) struct RuneUpdater<'a, 'tx, 'client> { + pub(super) block_time: u32, + pub(super) burned: HashMap, + pub(super) client: &'client Client, pub(super) height: u32, - pub(super) id_to_entry: &'a mut Table<'db, 'tx, RuneIdValue, RuneEntryValue>, - pub(super) inscription_id_to_sequence_number: &'a Table<'db, 'tx, InscriptionIdValue, u32>, + pub(super) id_to_entry: &'a mut Table<'tx, RuneIdValue, RuneEntryValue>, + pub(super) inscription_id_to_sequence_number: &'a Table<'tx, InscriptionIdValue, u32>, pub(super) minimum: Rune, - pub(super) outpoint_to_balances: &'a mut Table<'db, 'tx, &'static OutPointValue, &'static [u8]>, - pub(super) rune_to_id: &'a mut Table<'db, 'tx, u128, RuneIdValue>, + pub(super) outpoint_to_balances: &'a mut Table<'tx, &'static OutPointValue, &'static [u8]>, + pub(super) rune_to_id: &'a mut Table<'tx, u128, RuneIdValue>, pub(super) runes: u64, - pub(super) sequence_number_to_rune_id: &'a mut Table<'db, 'tx, u32, RuneIdValue>, - pub(super) statistic_to_count: &'a mut Table<'db, 'tx, u64, u64>, - pub(super) timestamp: u32, - pub(super) transaction_id_to_rune: &'a mut Table<'db, 'tx, &'static TxidValue, u128>, - pub(super) updates: HashMap, + pub(super) sequence_number_to_rune_id: &'a mut Table<'tx, u32, RuneIdValue>, + pub(super) statistic_to_count: &'a mut Table<'tx, u64, u64>, + pub(super) transaction_id_to_rune: &'a mut Table<'tx, &'static TxidValue, u128>, } -impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { - pub(super) fn index_runes(&mut self, index: usize, tx: &Transaction, txid: Txid) -> Result<()> { - let runestone = Runestone::from_transaction(tx); +impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { + pub(super) fn index_runes(&mut self, tx_index: u32, tx: &Transaction, txid: Txid) -> Result<()> { + let artifact = Runestone::decipher(tx); let mut unallocated = self.unallocated(tx)?; - let burn = runestone - .as_ref() - .map(|runestone| runestone.burn) - .unwrap_or_default(); + let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; - let default_output = runestone.as_ref().and_then(|runestone| { - runestone - .default_output - .and_then(|default| usize::try_from(default).ok()) - }); + if let Some(artifact) = &artifact { + if let Some(id) = artifact.mint() { + if let Some(amount) = self.mint(id)? { + *unallocated.entry(id).or_default() += amount; + } + } - let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; + let etched = self.etched(tx_index, tx, artifact)?; - if let Some(runestone) = runestone { - if let Some(claim) = runestone - .claim - .and_then(|id| self.claim(id).transpose()) - .transpose()? - { - *unallocated.entry(claim.id).or_default() += claim.limit; + if let Artifact::Runestone(runestone) = artifact { + if let Some((id, ..)) = etched { + *unallocated.entry(id).or_default() += + runestone.etching.unwrap().premine.unwrap_or_default(); + } - let update = self - .updates - .entry(RuneId::try_from(claim.id).unwrap()) - .or_default(); + for Edict { id, amount, output } in runestone.edicts.iter().copied() { + let amount = Lot(amount); - update.mints += 1; - update.supply += claim.limit; - } + // edicts with output values greater than the number of outputs + // should never be produced by the edict parser + let output = usize::try_from(output).unwrap(); + assert!(output <= tx.output.len()); - let mut etched = self.etched(index, &runestone)?; + let id = if id == RuneId::default() { + let Some((id, ..)) = etched else { + continue; + }; - if !burn { - for Edict { id, amount, output } in runestone.edicts { - let Ok(output) = usize::try_from(output) else { - continue; + id + } else { + id }; - // Skip edicts not referring to valid outputs - if output > tx.output.len() { + let Some(balance) = unallocated.get_mut(&id) else { continue; - } - - let (balance, id) = if id == 0 { - // If this edict allocates new issuance runes, skip it - // if no issuance was present, or if the issuance was invalid. - // Additionally, replace ID 0 with the newly assigned ID, and - // get the unallocated balance of the issuance. - match etched.as_mut() { - Some(Etched { balance, id, .. }) => (balance, *id), - None => continue, - } - } else { - // Get the unallocated balance of the given ID - match unallocated.get_mut(&id) { - Some(balance) => (balance, id), - None => continue, - } }; - let mut allocate = |balance: &mut u128, amount: u128, output: usize| { + let mut allocate = |balance: &mut Lot, amount: Lot, output: usize| { if amount > 0 { *balance -= amount; *allocated[output].entry(id).or_default() += amount; @@ -155,23 +110,31 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { } } - if let Some(etched) = etched { - self.create_rune_entry(txid, burn, etched)?; + if let Some((id, rune)) = etched { + self.create_rune_entry(txid, artifact, id, rune)?; } } - let mut burned: HashMap = HashMap::new(); + let mut burned: HashMap = HashMap::new(); - if burn { + if let Some(Artifact::Cenotaph(_)) = artifact { for (id, balance) in unallocated { *burned.entry(id).or_default() += balance; } } else { + let pointer = artifact + .map(|artifact| match artifact { + Artifact::Runestone(runestone) => runestone.pointer, + Artifact::Cenotaph(_) => unreachable!(), + }) + .unwrap_or_default(); + // assign all un-allocated runes to the default output, or the first non // OP_RETURN output if there is no default, or if the default output is // too large - if let Some(vout) = default_output - .filter(|vout| *vout < allocated.len()) + if let Some(vout) = pointer + .map(|pointer| pointer.into_usize()) + .inspect(|&pointer| assert!(pointer < allocated.len())) .or_else(|| { tx.output .iter() @@ -204,21 +167,20 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { // increment burned balances if tx.output[vout].script_pubkey.is_op_return() { for (id, balance) in &balances { - *burned.entry(*id).or_default() += balance; + *burned.entry(*id).or_default() += *balance; } continue; } buffer.clear(); - let mut balances = balances.into_iter().collect::>(); + let mut balances = balances.into_iter().collect::>(); // Sort balances by id so tests can assert balances in a fixed order balances.sort(); for (id, balance) in balances { - varint::encode_to_vec(id, &mut buffer); - varint::encode_to_vec(balance, &mut buffer); + Index::encode_rune_balance(id, balance.n(), &mut buffer); } self.outpoint_to_balances.insert( @@ -233,60 +195,85 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { // increment entries with burned runes for (id, amount) in burned { - self - .updates - .entry(RuneId::try_from(id).unwrap()) - .or_default() - .burned += amount; + *self.burned.entry(id).or_default() += amount; } Ok(()) } - fn create_rune_entry(&mut self, txid: Txid, burn: bool, etched: Etched) -> Result { - let Etched { - balance, - divisibility, - id, - mint, - rune, - spacers, - symbol, - } = etched; + pub(super) fn update(self) -> Result { + for (rune_id, burned) in self.burned { + let mut entry = RuneEntry::load(self.id_to_entry.get(&rune_id.store())?.unwrap().value()); + entry.burned = entry.burned.checked_add(burned.n()).unwrap(); + self.id_to_entry.insert(&rune_id.store(), entry.store())?; + } + + Ok(()) + } + + fn create_rune_entry( + &mut self, + txid: Txid, + artifact: &Artifact, + id: RuneId, + rune: Rune, + ) -> Result { + self.rune_to_id.insert(rune.store(), id.store())?; + self + .transaction_id_to_rune + .insert(&txid.store(), rune.store())?; - let id = RuneId::try_from(id).unwrap(); - self.rune_to_id.insert(rune.0, id.store())?; - self.transaction_id_to_rune.insert(&txid.store(), rune.0)?; let number = self.runes; self.runes += 1; + self .statistic_to_count .insert(&Statistic::Runes.into(), self.runes)?; - self.id_to_entry.insert( - id.store(), - RuneEntry { + + let entry = match artifact { + Artifact::Cenotaph(_) => RuneEntry { + block: id.block, burned: 0, - divisibility, + divisibility: 0, etching: txid, + terms: None, mints: 0, number, - mint: mint.and_then(|mint| (!burn).then_some(mint)), - rune, - spacers, - supply: if let Some(mint) = mint { - if mint.end == Some(self.height) { - 0 - } else { - mint.limit.unwrap_or(runes::MAX_LIMIT) - } - } else { - u128::MAX - } - balance, - symbol, - timestamp: self.timestamp, + premine: 0, + spaced_rune: SpacedRune { rune, spacers: 0 }, + symbol: None, + timestamp: self.block_time.into(), + }, + Artifact::Runestone(Runestone { etching, .. }) => { + let Etching { + divisibility, + terms, + premine, + spacers, + symbol, + .. + } = etching.unwrap(); + + RuneEntry { + block: id.block, + burned: 0, + divisibility: divisibility.unwrap_or_default(), + etching: txid, + terms, + mints: 0, + number, + premine: premine.unwrap_or_default(), + spaced_rune: SpacedRune { + rune, + spacers: spacers.unwrap_or_default(), + }, + symbol, + timestamp: self.block_time.into(), + } } - .store(), - )?; + }; + + self.id_to_entry.insert(id.store(), entry.store())?; let inscription_id = InscriptionId { txid, index: 0 }; @@ -302,25 +289,31 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { Ok(()) } - fn etched(&mut self, index: usize, runestone: &Runestone) -> Result> { - let Some(etching) = runestone.etching else { - return Ok(None); + fn etched( + &mut self, + tx_index: u32, + tx: &Transaction, + artifact: &Artifact, + ) -> Result> { + let rune = match artifact { + Artifact::Runestone(runestone) => match runestone.etching { + Some(etching) => etching.rune, + None => return Ok(None), + }, + Artifact::Cenotaph(cenotaph) => match cenotaph.etching { + Some(rune) => Some(rune), + None => return Ok(None), + }, }; - if etching - .rune - .map(|rune| rune < self.minimum || rune.is_reserved()) - .unwrap_or_default() - || etching - .rune - .and_then(|rune| self.rune_to_id.get(rune.0).transpose()) - .transpose()? - .is_some() - { - return Ok(None); - } - - let rune = if let Some(rune) = etching.rune { + let rune = if let Some(rune) = rune { + if rune < self.minimum + || rune.is_reserved() + || self.rune_to_id.get(rune.0)?.is_some() + || !self.tx_commits_to_rune(tx, rune)? + { + return Ok(None); + } rune } else { let reserved_runes = self @@ -333,76 +326,93 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { .statistic_to_count .insert(&Statistic::ReservedRunes.into(), reserved_runes + 1)?; - Rune::reserved(reserved_runes.into()) - }; - - // Nota bene: Because it would require constructing a block - // with 2**16 + 1 transactions, there is no test that checks that - // an eching in a transaction with an out-of-bounds index is - // ignored. - let Ok(index) = u16::try_from(index) else { - return Ok(None); + Rune::reserved(self.height.into(), tx_index) }; - Ok(Some(Etched { - balance: if let Some(mint) = etching.mint { - if mint.term == Some(0) { - 0 - } else { - mint.limit.unwrap_or(runes::MAX_LIMIT) - } - } else { - u128::MAX + Ok(Some(( + RuneId { + block: self.height.into(), + tx: tx_index, }, - divisibility: etching.divisibility, - id: u128::from(self.height) << 16 | u128::from(index), rune, - spacers: etching.spacers, - symbol: etching.symbol, - mint: etching.mint.map(|mint| MintEntry { - deadline: mint.deadline, - end: mint.term.map(|term| term + self.height), - limit: mint.limit.map(|limit| limit.min(runes::MAX_LIMIT)), - }), - })) + ))) } - fn claim(&self, id: u128) -> Result> { - let Ok(key) = RuneId::try_from(id) else { + fn mint(&mut self, id: RuneId) -> Result> { + let Some(entry) = self.id_to_entry.get(&id.store())? else { return Ok(None); }; - let Some(entry) = self.id_to_entry.get(&key.store())? else { + let mut rune_entry = RuneEntry::load(entry.value()); + + let Ok(amount) = rune_entry.mintable(self.height.into()) else { return Ok(None); }; - let entry = RuneEntry::load(entry.value()); + drop(entry); - let Some(mint) = entry.mint else { - return Ok(None); - }; + rune_entry.mints += 1; - if let Some(end) = mint.end { - if self.height >= end { - return Ok(None); - } - } + self.id_to_entry.insert(&id.store(), rune_entry.store())?; - if let Some(deadline) = mint.deadline { - if self.timestamp >= deadline { - return Ok(None); + Ok(Some(Lot(amount))) + } + + fn tx_commits_to_rune(&self, tx: &Transaction, rune: Rune) -> Result { + let commitment = rune.commitment(); + + for input in &tx.input { + // extracting a tapscript does not indicate that the input being spent + // was actually a taproot output. this is checked below, when we load the + // output's entry from the database + let Some(tapscript) = input.witness.tapscript() else { + continue; + }; + + for instruction in tapscript.instructions() { + // ignore errors, since the extracted script may not be valid + let Ok(instruction) = instruction else { + break; + }; + + let Some(pushbytes) = instruction.push_bytes() else { + continue; + }; + + if pushbytes.as_bytes() != commitment { + continue; + } + + let Some(tx_info) = self + .client + .get_raw_transaction_info(&input.previous_output.txid, None) + .into_option()? + else { + panic!("input not in UTXO set: {}", input.previous_output); + }; + + let taproot = tx_info.vout[input.previous_output.vout.into_usize()] + .script_pub_key + .script()? + .is_v1_p2tr(); + + let mature = tx_info + .confirmations + .map(|confirmations| confirmations >= Runestone::COMMIT_INTERVAL.into()) + .unwrap_or_default(); + + if taproot && mature { + return Ok(true); + } } } - Ok(Some(Claim { - id, - limit: mint.limit.unwrap_or(runes::MAX_LIMIT), - })) + Ok(false) } - fn unallocated(&mut self, tx: &Transaction) -> Result> { + fn unallocated(&mut self, tx: &Transaction) -> Result> { // map of rune ID to un-allocated balance of that rune - let mut unallocated: HashMap = HashMap::new(); + let mut unallocated: HashMap = HashMap::new(); // increment unallocated runes with the runes in tx inputs for input in &tx.input { @@ -413,9 +423,7 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { let buffer = guard.value(); let mut i = 0; while i < buffer.len() { - let (id, len) = varint::decode(&buffer[i..]); - i += len; - let (balance, len) = varint::decode(&buffer[i..]); + let ((id, balance), len) = Index::decode_rune_balance(&buffer[i..]).unwrap(); i += len; *unallocated.entry(id).or_default() += balance; } diff --git a/src/inscriptions.rs b/src/inscriptions.rs index 1c32ff8ae6..dd8f4e4ae4 100644 --- a/src/inscriptions.rs +++ b/src/inscriptions.rs @@ -2,11 +2,10 @@ use super::*; use tag::Tag; -pub(crate) use self::{charm::Charm, envelope::ParsedEnvelope, media::Media}; +pub(crate) use self::{envelope::ParsedEnvelope, media::Media}; pub use self::{envelope::Envelope, inscription::Inscription, inscription_id::InscriptionId}; -mod charm; mod envelope; mod inscription; pub(crate) mod inscription_id; diff --git a/src/inscriptions/charm.rs b/src/inscriptions/charm.rs deleted file mode 100644 index 9aea73e774..0000000000 --- a/src/inscriptions/charm.rs +++ /dev/null @@ -1,115 +0,0 @@ -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum Charm { - Coin = 0, - Cursed = 1, - Epic = 2, - Legendary = 3, - Lost = 4, - Nineball = 5, - Rare = 6, - Reinscription = 7, - Unbound = 8, - Uncommon = 9, - Vindicated = 10, -} - -impl Charm { - pub(crate) const ALL: [Charm; 11] = [ - Self::Coin, - Self::Uncommon, - Self::Rare, - Self::Epic, - Self::Legendary, - Self::Nineball, - Self::Reinscription, - Self::Cursed, - Self::Unbound, - Self::Lost, - Self::Vindicated, - ]; - - fn flag(self) -> u16 { - 1 << self as u16 - } - - pub(crate) fn set(self, charms: &mut u16) { - *charms |= self.flag(); - } - - pub(crate) fn is_set(self, charms: u16) -> bool { - charms & self.flag() != 0 - } - - pub(crate) fn unset(self, charms: u16) -> u16 { - charms & !self.flag() - } - - pub(crate) fn icon(self) -> &'static str { - match self { - Self::Coin => "🪙", - Self::Cursed => "👹", - Self::Epic => "🪻", - Self::Legendary => "🌝", - Self::Lost => "🤔", - Self::Nineball => "9️⃣", - Self::Rare => "🧿", - Self::Reinscription => "♻️", - Self::Unbound => "🔓", - Self::Uncommon => "🌱", - Self::Vindicated => "❤️‍🔥", - } - } - - pub(crate) fn title(self) -> &'static str { - match self { - Self::Coin => "coin", - Self::Cursed => "cursed", - Self::Epic => "epic", - Self::Legendary => "legendary", - Self::Lost => "lost", - Self::Nineball => "nineball", - Self::Rare => "rare", - Self::Reinscription => "reinscription", - Self::Unbound => "unbound", - Self::Uncommon => "uncommon", - Self::Vindicated => "vindicated", - } - } - - #[cfg(test)] - pub(crate) fn charms(charms: u16) -> Vec { - Self::ALL - .iter() - .filter(|charm| charm.is_set(charms)) - .cloned() - .collect() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn flag() { - assert_eq!(Charm::Coin.flag(), 0b1); - assert_eq!(Charm::Cursed.flag(), 0b10); - } - - #[test] - fn set() { - let mut flags = 0; - assert!(!Charm::Coin.is_set(flags)); - Charm::Coin.set(&mut flags); - assert!(Charm::Coin.is_set(flags)); - } - - #[test] - fn unset() { - let mut flags = 0; - Charm::Coin.set(&mut flags); - assert!(Charm::Coin.is_set(flags)); - let flags = Charm::Coin.unset(flags); - assert!(!Charm::Coin.is_set(flags)); - } -} diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index 0ef863c831..274bf5f9cd 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -3,7 +3,6 @@ use { bitcoin::blockdata::{ opcodes, script::{ - self, Instruction::{self, Op, PushBytes}, Instructions, }, @@ -48,13 +47,14 @@ impl From for ParsedEnvelope { let duplicate_field = fields.iter().any(|(_key, values)| values.len() > 1); - let content_encoding = Tag::ContentEncoding.remove_field(&mut fields); - let content_type = Tag::ContentType.remove_field(&mut fields); - let delegate = Tag::Delegate.remove_field(&mut fields); - let metadata = Tag::Metadata.remove_field(&mut fields); - let metaprotocol = Tag::Metaprotocol.remove_field(&mut fields); - let parent = Tag::Parent.remove_field(&mut fields); - let pointer = Tag::Pointer.remove_field(&mut fields); + let content_encoding = Tag::ContentEncoding.take(&mut fields); + let content_type = Tag::ContentType.take(&mut fields); + let delegate = Tag::Delegate.take(&mut fields); + let metadata = Tag::Metadata.take(&mut fields); + let metaprotocol = Tag::Metaprotocol.take(&mut fields); + let parents = Tag::Parent.take_array(&mut fields); + let pointer = Tag::Pointer.take(&mut fields); + let rune = Tag::Rune.take(&mut fields); let unrecognized_even_field = fields .keys() @@ -76,8 +76,9 @@ impl From for ParsedEnvelope { incomplete_field, metadata, metaprotocol, - parent, + parents, pointer, + rune, unrecognized_even_field, }, input: envelope.input, @@ -324,9 +325,7 @@ mod tests { .into_bytes(), Vec::new() ])]), - vec![ParsedEnvelope { - ..Default::default() - }] + vec![ParsedEnvelope { ..default() }] ); } @@ -363,17 +362,17 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::Nop.bytes(), + Tag::Nop.bytes().as_slice(), &[], - Tag::Nop.bytes(), + &Tag::Nop.bytes(), &[] ])]), vec![ParsedEnvelope { payload: Inscription { duplicate_field: true, - ..Default::default() + ..default() }, - ..Default::default() + ..default() }] ); } @@ -383,14 +382,14 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[], b"ord", ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "ord"), - ..Default::default() + ..default() }] ); } @@ -400,7 +399,7 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[9], b"br", @@ -412,7 +411,7 @@ mod tests { content_encoding: Some("br".as_bytes().to_vec()), ..inscription("text/plain;charset=utf-8", "ord") }, - ..Default::default() + ..default() }] ); } @@ -422,16 +421,16 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", - Tag::Nop.bytes(), + Tag::Nop.bytes().as_slice(), b"bar", &[], b"ord", ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "ord"), - ..Default::default() + ..default() }] ); } @@ -441,15 +440,15 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8" ])]), vec![ParsedEnvelope { payload: Inscription { content_type: Some(b"text/plain;charset=utf-8".to_vec()), - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -461,9 +460,9 @@ mod tests { vec![ParsedEnvelope { payload: Inscription { body: Some(b"foo".to_vec()), - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -473,7 +472,7 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[], b"foo", @@ -481,7 +480,7 @@ mod tests { ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "foobar"), - ..Default::default() + ..default() }], ); } @@ -491,13 +490,13 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[] ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", ""), - ..Default::default() + ..default() }] ); } @@ -507,7 +506,7 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[], &[], @@ -518,7 +517,7 @@ mod tests { ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", ""), - ..Default::default() + ..default() }], ); } @@ -541,7 +540,7 @@ mod tests { parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "ord"), - ..Default::default() + ..default() }], ); } @@ -564,7 +563,7 @@ mod tests { parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "ord"), - ..Default::default() + ..default() }], ); } @@ -595,12 +594,12 @@ mod tests { vec![ ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "foo"), - ..Default::default() + ..default() }, ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "bar"), offset: 1, - ..Default::default() + ..default() }, ], ); @@ -611,14 +610,14 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[], &[0b10000000] ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", [0b10000000]), - ..Default::default() + ..default() },], ); } @@ -666,14 +665,14 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"text/plain;charset=utf-8", &[], b"ord" ])]), vec![ParsedEnvelope { payload: inscription("text/plain;charset=utf-8", "ord"), - ..Default::default() + ..default() }], ); } @@ -685,7 +684,7 @@ mod tests { vec![ParsedEnvelope { payload: inscription("foo", [1; 1040]), input: 1, - ..Default::default() + ..default() }] ); } @@ -704,12 +703,12 @@ mod tests { vec![ ParsedEnvelope { payload: inscription("foo", [1; 100]), - ..Default::default() + ..default() }, ParsedEnvelope { payload: inscription("bar", [1; 100]), offset: 1, - ..Default::default() + ..default() } ] ); @@ -720,14 +719,14 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::ContentType.bytes(), + &Tag::ContentType.bytes(), b"image/png", &[], &[1; 100] ])]), vec![ParsedEnvelope { payload: inscription("image/png", [1; 100]), - ..Default::default() + ..default() }] ); } @@ -744,7 +743,7 @@ mod tests { parse(&[witness]), vec![ParsedEnvelope { payload: inscription("foo", [1; 1040]), - ..Default::default() + ..default() }] ); } @@ -761,7 +760,7 @@ mod tests { parse(&[witness]), vec![ParsedEnvelope { payload: Inscription::default(), - ..Default::default() + ..default() }], ); } @@ -769,10 +768,10 @@ mod tests { #[test] fn unknown_odd_fields_are_ignored() { assert_eq!( - parse(&[envelope(&[&PROTOCOL_ID, Tag::Nop.bytes(), &[0]])]), + parse(&[envelope(&[&PROTOCOL_ID, &Tag::Nop.bytes(), &[0]])]), vec![ParsedEnvelope { payload: Inscription::default(), - ..Default::default() + ..default() }], ); } @@ -784,9 +783,9 @@ mod tests { vec![ParsedEnvelope { payload: Inscription { unrecognized_even_field: true, - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -798,9 +797,9 @@ mod tests { vec![ParsedEnvelope { payload: Inscription { pointer: Some(vec![1]), - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -814,9 +813,9 @@ mod tests { pointer: Some(vec![1]), duplicate_field: true, unrecognized_even_field: true, - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -824,13 +823,13 @@ mod tests { #[test] fn tag_66_makes_inscriptions_unbound() { assert_eq!( - parse(&[envelope(&[&PROTOCOL_ID, Tag::Unbound.bytes(), &[1]])]), + parse(&[envelope(&[&PROTOCOL_ID, &Tag::Unbound.bytes(), &[1]])]), vec![ParsedEnvelope { payload: Inscription { unrecognized_even_field: true, - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -842,9 +841,9 @@ mod tests { vec![ParsedEnvelope { payload: Inscription { incomplete_field: true, - ..Default::default() + ..default() }, - ..Default::default() + ..default() }], ); } @@ -852,13 +851,13 @@ mod tests { #[test] fn metadata_is_parsed_correctly() { assert_eq!( - parse(&[envelope(&[&PROTOCOL_ID, Tag::Metadata.bytes(), &[]])]), + parse(&[envelope(&[&PROTOCOL_ID, &Tag::Metadata.bytes(), &[]])]), vec![ParsedEnvelope { payload: Inscription { - metadata: Some(vec![]), - ..Default::default() + metadata: Some(Vec::new()), + ..default() }, - ..Default::default() + ..default() }] ); } @@ -868,18 +867,18 @@ mod tests { assert_eq!( parse(&[envelope(&[ &PROTOCOL_ID, - Tag::Metadata.bytes(), + &Tag::Metadata.bytes(), &[0], - Tag::Metadata.bytes(), + &Tag::Metadata.bytes(), &[1] ])]), vec![ParsedEnvelope { payload: Inscription { metadata: Some(vec![0, 1]), duplicate_field: true, - ..Default::default() + ..default() }, - ..Default::default() + ..default() }] ); } @@ -921,10 +920,10 @@ mod tests { vec![ParsedEnvelope { payload: Inscription { body: Some(vec![value]), - ..Default::default() + ..default() }, pushnum: true, - ..Default::default() + ..default() }], ); } @@ -945,7 +944,7 @@ mod tests { vec![ParsedEnvelope { payload: Default::default(), stutter: true, - ..Default::default() + ..default() }], ); @@ -963,7 +962,7 @@ mod tests { vec![ParsedEnvelope { payload: Default::default(), stutter: true, - ..Default::default() + ..default() }], ); @@ -983,7 +982,7 @@ mod tests { vec![ParsedEnvelope { payload: Default::default(), stutter: true, - ..Default::default() + ..default() }], ); @@ -1002,7 +1001,7 @@ mod tests { vec![ParsedEnvelope { payload: Default::default(), stutter: false, - ..Default::default() + ..default() }], ); } diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index 9d34913160..5bc3014a43 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -1,13 +1,10 @@ use { super::*, anyhow::ensure, - bitcoin::{ - blockdata::{opcodes, script}, - ScriptBuf, - }, + bitcoin::blockdata::opcodes, brotli::enc::{writer::CompressorWriter, BrotliEncoderParams}, http::header::HeaderValue, - io::{Cursor, Read, Write}, + io::Write, std::str, }; @@ -21,8 +18,9 @@ pub struct Inscription { pub incomplete_field: bool, pub metadata: Option>, pub metaprotocol: Option>, - pub parent: Option>, + pub parents: Vec>, pub pointer: Option>, + pub rune: Option>, pub unrecognized_even_field: bool, } @@ -32,7 +30,7 @@ impl Inscription { Self { content_type, body, - ..Default::default() + ..default() } } @@ -42,9 +40,10 @@ impl Inscription { delegate: Option, metadata: Option>, metaprotocol: Option, - parent: Option, + parents: Vec, path: impl AsRef, pointer: Option, + rune: Option, ) -> Result { let path = path.as_ref(); @@ -65,7 +64,7 @@ impl Inscription { mode: compression_mode, quality: 11, size_hint: body.len(), - ..Default::default() + ..default() }, ) .write_all(&body)?; @@ -102,9 +101,10 @@ impl Inscription { delegate: delegate.map(|delegate| delegate.value()), metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), - parent: parent.map(|parent| parent.value()), + parents: parents.iter().map(|parent| parent.value()).collect(), pointer: pointer.map(Self::pointer_value), - ..Default::default() + rune: rune.map(|rune| rune.commitment()), + ..default() }) } @@ -127,13 +127,14 @@ impl Inscription { .push_opcode(opcodes::all::OP_IF) .push_slice(envelope::PROTOCOL_ID); - Tag::ContentType.encode(&mut builder, &self.content_type); - Tag::ContentEncoding.encode(&mut builder, &self.content_encoding); - Tag::Metaprotocol.encode(&mut builder, &self.metaprotocol); - Tag::Parent.encode(&mut builder, &self.parent); - Tag::Delegate.encode(&mut builder, &self.delegate); - Tag::Pointer.encode(&mut builder, &self.pointer); - Tag::Metadata.encode(&mut builder, &self.metadata); + Tag::ContentType.append(&mut builder, &self.content_type); + Tag::ContentEncoding.append(&mut builder, &self.content_encoding); + Tag::Metaprotocol.append(&mut builder, &self.metaprotocol); + Tag::Parent.append_array(&mut builder, &self.parents); + Tag::Delegate.append(&mut builder, &self.delegate); + Tag::Pointer.append(&mut builder, &self.pointer); + Tag::Metadata.append(&mut builder, &self.metadata); + Tag::Rune.append(&mut builder, &self.rune); if let Some(body) = &self.body { builder = builder.push_slice(envelope::BODY_TAG); @@ -168,7 +169,7 @@ impl Inscription { Inscription::append_batch_reveal_script_to_builder(inscriptions, builder).into_script() } - fn inscription_id_field(field: &Option>) -> Option { + fn inscription_id_field(field: Option<&[u8]>) -> Option { let value = field.as_ref()?; if value.len() < Txid::LEN { @@ -242,7 +243,7 @@ impl Inscription { } pub(crate) fn delegate(&self) -> Option { - Self::inscription_id_field(&self.delegate) + Self::inscription_id_field(self.delegate.as_deref()) } pub(crate) fn metadata(&self) -> Option { @@ -253,8 +254,12 @@ impl Inscription { str::from_utf8(self.metaprotocol.as_ref()?).ok() } - pub(crate) fn parent(&self) -> Option { - Self::inscription_id_field(&self.parent) + pub(crate) fn parents(&self) -> Vec { + self + .parents + .iter() + .filter_map(|parent| Self::inscription_id_field(Some(parent))) + .collect() } pub(crate) fn pointer(&self) -> Option { @@ -371,7 +376,7 @@ mod tests { assert_eq!( Inscription { metadata: None, - ..Default::default() + ..default() } .append_reveal_script(script::Builder::new()) .instructions() @@ -382,7 +387,7 @@ mod tests { assert_eq!( Inscription { metadata: Some(Vec::new()), - ..Default::default() + ..default() } .append_reveal_script(script::Builder::new()) .instructions() @@ -393,7 +398,7 @@ mod tests { assert_eq!( Inscription { metadata: Some(vec![0; 1]), - ..Default::default() + ..default() } .append_reveal_script(script::Builder::new()) .instructions() @@ -404,7 +409,7 @@ mod tests { assert_eq!( Inscription { metadata: Some(vec![0; 520]), - ..Default::default() + ..default() } .append_reveal_script(script::Builder::new()) .instructions() @@ -415,7 +420,7 @@ mod tests { assert_eq!( Inscription { metadata: Some(vec![0; 521]), - ..Default::default() + ..default() } .append_reveal_script(script::Builder::new()) .instructions() @@ -427,31 +432,31 @@ mod tests { #[test] fn inscription_with_no_parent_field_has_no_parent() { assert!(Inscription { - parent: None, - ..Default::default() + parents: Vec::new(), + ..default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] fn inscription_with_parent_field_shorter_than_txid_length_has_no_parent() { assert!(Inscription { - parent: Some(vec![]), - ..Default::default() + parents: vec![Vec::new()], + ..default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] fn inscription_with_parent_field_longer_than_txid_and_index_has_no_parent() { assert!(Inscription { - parent: Some(vec![1; 37]), - ..Default::default() + parents: vec![vec![1; 37]], + ..default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] @@ -460,12 +465,12 @@ mod tests { parent[35] = 0; - assert!(Inscription { - parent: Some(parent), - ..Default::default() + assert!(!Inscription { + parents: vec![parent], + ..default() } - .parent() - .is_some()); + .parents() + .is_empty()); } #[test] @@ -475,11 +480,11 @@ mod tests { parent[34] = 0; assert!(Inscription { - parent: Some(parent), - ..Default::default() + parents: vec![parent], + ..default() } - .parent() - .is_none()); + .parents() + .is_empty()); } #[test] @@ -491,7 +496,7 @@ mod tests { 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, ]), - ..Default::default() + ..default() } .delegate() .unwrap() @@ -506,19 +511,19 @@ mod tests { fn inscription_parent_txid_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - ]), - ..Default::default() + ]], + ..default() } - .parent() - .unwrap() - .txid, - "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100" - .parse() - .unwrap() + .parents(), + [ + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100i0" + .parse() + .unwrap() + ], ); } @@ -526,13 +531,15 @@ mod tests { fn inscription_parent_with_zero_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![1; 32]), - ..Default::default() + parents: vec![vec![1; 32]], + ..default() } - .parent() - .unwrap() - .index, - 0 + .parents(), + [ + "0101010101010101010101010101010101010101010101010101010101010101i0" + .parse() + .unwrap() + ], ); } @@ -540,17 +547,19 @@ mod tests { fn inscription_parent_with_one_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01 - ]), - ..Default::default() + ]], + ..default() } - .parent() - .unwrap() - .index, - 1 + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi1" + .parse() + .unwrap() + ], ); } @@ -558,17 +567,19 @@ mod tests { fn inscription_parent_with_two_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x02 - ]), - ..Default::default() + ]], + ..default() } - .parent() - .unwrap() - .index, - 0x0201, + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi513" + .parse() + .unwrap() + ], ); } @@ -576,17 +587,19 @@ mod tests { fn inscription_parent_with_three_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03 - ]), - ..Default::default() + ]], + ..default() } - .parent() - .unwrap() - .index, - 0x030201, + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi197121" + .parse() + .unwrap() + ], ); } @@ -594,17 +607,49 @@ mod tests { fn inscription_parent_with_four_byte_index_field_is_deserialized_correctly() { assert_eq!( Inscription { - parent: Some(vec![ + parents: vec![vec![ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04, - ]), - ..Default::default() + ]], + ..default() } - .parent() - .unwrap() - .index, - 0x04030201, + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985" + .parse() + .unwrap() + ], + ); + } + + #[test] + fn inscription_parent_returns_multiple_parents() { + assert_eq!( + Inscription { + parents: vec![ + vec![ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04, + ], + vec![ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0x00, 0x02, 0x03, 0x04, + ] + ], + ..default() + } + .parents(), + [ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985" + .parse() + .unwrap(), + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305984" + .parse() + .unwrap() + ], ); } @@ -613,7 +658,7 @@ mod tests { assert_eq!( Inscription { metadata: Some(vec![0x44, 0, 1, 2, 3]), - ..Default::default() + ..default() } .metadata() .unwrap(), @@ -626,7 +671,7 @@ mod tests { assert_eq!( Inscription { metadata: None, - ..Default::default() + ..default() } .metadata(), None, @@ -638,7 +683,7 @@ mod tests { assert_eq!( Inscription { metadata: Some(vec![0x44]), - ..Default::default() + ..default() } .metadata(), None, @@ -650,7 +695,7 @@ mod tests { assert_eq!( Inscription { pointer: None, - ..Default::default() + ..default() } .pointer(), None @@ -658,7 +703,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![0]), - ..Default::default() + ..default() } .pointer(), Some(0), @@ -666,7 +711,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8]), - ..Default::default() + ..default() } .pointer(), Some(0x0807060504030201), @@ -674,7 +719,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![1, 2, 3, 4, 5, 6]), - ..Default::default() + ..default() } .pointer(), Some(0x0000060504030201), @@ -682,7 +727,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0]), - ..Default::default() + ..default() } .pointer(), Some(0x0807060504030201), @@ -690,7 +735,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 1]), - ..Default::default() + ..default() } .pointer(), None, @@ -698,7 +743,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 1]), - ..Default::default() + ..default() } .pointer(), None, @@ -710,7 +755,7 @@ mod tests { assert_eq!( Inscription { pointer: None, - ..Default::default() + ..default() } .to_witness(), envelope(&[b"ord"]), @@ -719,7 +764,7 @@ mod tests { assert_eq!( Inscription { pointer: Some(vec![1, 2, 3]), - ..Default::default() + ..default() } .to_witness(), envelope(&[b"ord", &[2], &[1, 2, 3]]), @@ -738,9 +783,10 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), None, + None, ) .unwrap(); @@ -752,9 +798,10 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), Some(0), + None, ) .unwrap(); @@ -766,9 +813,10 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), Some(1), + None, ) .unwrap(); @@ -780,9 +828,10 @@ mod tests { None, None, None, - None, + Vec::new(), file.path(), Some(256), + None, ) .unwrap(); @@ -797,7 +846,7 @@ mod tests { Inscription { content_type: content_type.map(|content_type| content_type.as_bytes().into()), body: body.map(|content_type| content_type.as_bytes().into()), - ..Default::default() + ..default() } .hidden(), expected @@ -854,7 +903,7 @@ mod tests { assert!(Inscription { content_type: Some("text/plain".as_bytes().into()), body: Some(b"{\xc3\x28}".as_slice().into()), - ..Default::default() + ..default() } .hidden()); @@ -862,7 +911,7 @@ mod tests { content_type: Some("text/html".as_bytes().into()), body: Some("hello".as_bytes().into()), metaprotocol: Some(Vec::new()), - ..Default::default() + ..default() } .hidden()); } diff --git a/src/inscriptions/inscription_id.rs b/src/inscriptions/inscription_id.rs index 773b0ac715..92fc0a844f 100644 --- a/src/inscriptions/inscription_id.rs +++ b/src/inscriptions/inscription_id.rs @@ -1,6 +1,8 @@ use super::*; -#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq, PartialOrd, Ord)] +#[derive( + Debug, PartialEq, Copy, Clone, Hash, Eq, PartialOrd, Ord, DeserializeFromStr, SerializeDisplay, +)] pub struct InscriptionId { pub txid: Txid, pub index: u32, @@ -34,24 +36,6 @@ impl InscriptionId { } } -impl<'de> Deserialize<'de> for InscriptionId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) - } -} - -impl Serialize for InscriptionId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - impl Display for InscriptionId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{}i{}", self.txid, self.index) diff --git a/src/inscriptions/tag.rs b/src/inscriptions/tag.rs index 6f186c13f2..ad86f0ebdf 100644 --- a/src/inscriptions/tag.rs +++ b/src/inscriptions/tag.rs @@ -1,58 +1,48 @@ use super::*; #[derive(Copy, Clone)] +#[repr(u8)] pub(crate) enum Tag { - Pointer, + Pointer = 2, #[allow(unused)] - Unbound, - - ContentType, - Parent, - Metadata, - Metaprotocol, - ContentEncoding, - Delegate, + Unbound = 66, + + ContentType = 1, + Parent = 3, + Metadata = 5, + Metaprotocol = 7, + ContentEncoding = 9, + Delegate = 11, + Rune = 13, #[allow(unused)] - Note, + Note = 15, #[allow(unused)] - Nop, + Nop = 255, } impl Tag { - fn is_chunked(self) -> bool { + fn chunked(self) -> bool { matches!(self, Self::Metadata) } - pub(crate) fn bytes(self) -> &'static [u8] { - match self { - Self::Pointer => &[2], - Self::Unbound => &[66], - - Self::ContentType => &[1], - Self::Parent => &[3], - Self::Metadata => &[5], - Self::Metaprotocol => &[7], - Self::ContentEncoding => &[9], - Self::Delegate => &[11], - Self::Note => &[15], - Self::Nop => &[255], - } + pub(crate) fn bytes(self) -> [u8; 1] { + [self as u8] } - pub(crate) fn encode(self, builder: &mut script::Builder, value: &Option>) { + pub(crate) fn append(self, builder: &mut script::Builder, value: &Option>) { if let Some(value) = value { let mut tmp = script::Builder::new(); mem::swap(&mut tmp, builder); - if self.is_chunked() { + if self.chunked() { for chunk in value.chunks(MAX_SCRIPT_ELEMENT_SIZE) { tmp = tmp - .push_slice::<&script::PushBytes>(self.bytes().try_into().unwrap()) + .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap()) .push_slice::<&script::PushBytes>(chunk.try_into().unwrap()); } } else { tmp = tmp - .push_slice::<&script::PushBytes>(self.bytes().try_into().unwrap()) + .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap()) .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); } @@ -60,9 +50,22 @@ impl Tag { } } - pub(crate) fn remove_field(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Option> { - if self.is_chunked() { - let value = fields.remove(self.bytes())?; + pub(crate) fn append_array(self, builder: &mut script::Builder, values: &Vec>) { + let mut tmp = script::Builder::new(); + mem::swap(&mut tmp, builder); + + for value in values { + tmp = tmp + .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap()) + .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); + } + + mem::swap(&mut tmp, builder); + } + + pub(crate) fn take(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Option> { + if self.chunked() { + let value = fields.remove(self.bytes().as_slice())?; if value.is_empty() { None @@ -70,7 +73,7 @@ impl Tag { Some(value.into_iter().flatten().cloned().collect()) } } else { - let values = fields.get_mut(self.bytes())?; + let values = fields.get_mut(self.bytes().as_slice())?; if values.is_empty() { None @@ -78,11 +81,20 @@ impl Tag { let value = values.remove(0).to_vec(); if values.is_empty() { - fields.remove(self.bytes()); + fields.remove(self.bytes().as_slice()); } Some(value) } } } + + pub(crate) fn take_array(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Vec> { + fields + .remove(self.bytes().as_slice()) + .unwrap_or_default() + .into_iter() + .map(|v| v.to_vec()) + .collect() + } } diff --git a/src/into_usize.rs b/src/into_usize.rs new file mode 100644 index 0000000000..5d773b9515 --- /dev/null +++ b/src/into_usize.rs @@ -0,0 +1,19 @@ +pub(crate) trait IntoUsize { + fn into_usize(self) -> usize; +} + +impl IntoUsize for u32 { + fn into_usize(self) -> usize { + self.try_into().unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn into_usize() { + u32::MAX.into_usize(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8501bba765..1725bd5d70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,13 +16,15 @@ use { arguments::Arguments, blocktime::Blocktime, decimal::Decimal, + deserialize_from_str::DeserializeFromStr, + index::BitcoinCoreRpcResultExt, inscriptions::{ inscription_id, media::{self, ImageRendering, Media}, - teleburn, Charm, ParsedEnvelope, + teleburn, ParsedEnvelope, }, + into_usize::IntoUsize, representation::Representation, - runes::{Etching, Pile, SpacedRune}, settings::Settings, subcommand::{Subcommand, SubcommandResult}, tally::Tally, @@ -38,27 +40,30 @@ use { consensus::{self, Decodable, Encodable}, hash_types::{BlockHash, TxMerkleNode}, hashes::Hash, - opcodes, - script::{self, Instruction}, - Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, - Witness, + script, Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, + TxOut, Txid, Witness, }, bitcoincore_rpc::{Client, RpcApi}, chrono::{DateTime, TimeZone, Utc}, ciborium::Value, clap::{ArgGroup, Parser}, html_escaper::{Escape, Trusted}, + http::HeaderMap, lazy_static::lazy_static, - ordinals::{DeserializeFromStr, Epoch, Height, Rarity, Sat, SatPoint}, + ordinals::{ + varint, Artifact, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone, + Sat, SatPoint, SpacedRune, Terms, + }, regex::Regex, reqwest::Url, - serde::{Deserialize, Deserializer, Serialize, Serializer}, + serde::{Deserialize, Deserializer, Serialize}, + serde_with::{DeserializeFromStr, SerializeDisplay}, std::{ cmp::{self, Reverse}, collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, env, fmt::{self, Display, Formatter}, - fs::{self, File}, + fs, io::{self, Cursor, Read}, mem, net::ToSocketAddrs, @@ -79,11 +84,10 @@ use { pub use self::{ chain::Chain, fee_rate::FeeRate, - index::{Index, MintEntry, RuneEntry}, + index::{Index, RuneEntry}, inscriptions::{Envelope, Inscription, InscriptionId}, object::Object, options::Options, - runes::{Edict, Rune, RuneId, Runestone}, wallet::transaction_builder::{Target, TransactionBuilder}, }; @@ -94,44 +98,41 @@ mod test; #[cfg(test)] use self::test::*; -macro_rules! tprintln { - ($($arg:tt)*) => { - if cfg!(test) { - eprint!("==> "); - eprintln!($($arg)*); - } - }; -} - pub mod api; pub mod arguments; mod blocktime; pub mod chain; mod decimal; +mod deserialize_from_str; mod fee_rate; pub mod index; mod inscriptions; +mod into_usize; +mod macros; mod object; -mod options; -mod ordzaar; +pub mod options; pub mod outgoing; +mod re; mod representation; pub mod runes; -mod server_config; mod settings; pub mod subcommand; mod tally; pub mod templates; pub mod wallet; +// ---- Ordzaar ---- +mod ordzaar; +// ---- Ordzaar ---- + type Result = std::result::Result; +const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); + static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); static LISTENERS: Mutex> = Mutex::new(Vec::new()); static INDEXER: Mutex>> = Mutex::new(None); -const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn fund_raw_transaction( client: &Client, @@ -159,16 +160,30 @@ fn fund_raw_transaction( // by 1000. fee_rate: Some(Amount::from_sat((fee_rate.n() * 1000.0).ceil() as u64)), change_position: Some(unfunded_transaction.output.len().try_into()?), - ..Default::default() + ..default() }), Some(false), - )? + ) + .map_err(|err| { + if matches!( + err, + bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc( + bitcoincore_rpc::jsonrpc::error::RpcError { code: -6, .. } + )) + ) { + anyhow!("not enough cardinal utxos") + } else { + err.into() + } + })? .hex, ) } -pub fn timestamp(seconds: u32) -> DateTime { - Utc.timestamp_opt(seconds.into(), 0).unwrap() +pub fn timestamp(seconds: u64) -> DateTime { + Utc + .timestamp_opt(seconds.try_into().unwrap_or(i64::MAX), 0) + .unwrap() } fn target_as_block_hash(target: bitcoin::Target) -> BlockHash { @@ -182,6 +197,14 @@ fn unbound_outpoint() -> OutPoint { } } +fn uncheck(address: &Address) -> Address { + address.to_string().parse().unwrap() +} + +fn default() -> T { + Default::default() +} + pub fn parse_ord_server_args(args: &str) -> (Settings, subcommand::server::Server) { match Arguments::try_parse_from(args.split_whitespace()) { Ok(arguments) => match arguments.subcommand { @@ -214,13 +237,12 @@ fn gracefully_shutdown_indexer() { pub fn main() { env_logger::init(); - ctrlc::set_handler(move || { if SHUTTING_DOWN.fetch_or(true, atomic::Ordering::Relaxed) { process::exit(1); } - println!("Shutting down gracefully. Press again to shutdown immediately."); + eprintln!("Shutting down gracefully. Press again to shutdown immediately."); LISTENERS .lock() diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000000..873847ae3e --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,51 @@ +#[macro_export] +macro_rules! define_table { + ($name:ident, $key:ty, $value:ty) => { + const $name: TableDefinition<$key, $value> = TableDefinition::new(stringify!($name)); + }; +} + +#[macro_export] +macro_rules! define_multimap_table { + ($name:ident, $key:ty, $value:ty) => { + const $name: MultimapTableDefinition<$key, $value> = + MultimapTableDefinition::new(stringify!($name)); + }; +} + +#[macro_export] +macro_rules! tprintln { + ($($arg:tt)*) => { + if cfg!(test) { + eprint!("==> "); + eprintln!($($arg)*); + } + }; +} + +#[macro_export] +macro_rules! assert_regex_match { + ($value:expr, $pattern:expr $(,)?) => { + let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap(); + let string = $value.to_string(); + + if !regex.is_match(string.as_ref()) { + eprintln!("Regex did not match:"); + pretty_assert_eq!(regex.as_str(), string); + } + }; +} + +#[macro_export] +macro_rules! assert_matches { + ($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => { + match $expression { + $( $pattern )|+ $( if $guard )? => {} + left => panic!( + "assertion failed: (left ~= right)\n left: `{:?}`\n right: `{}`", + left, + stringify!($($pattern)|+ $(if $guard)?) + ), + } + } +} diff --git a/src/object.rs b/src/object.rs index 611f592934..159acb16e2 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, DeserializeFromStr, SerializeDisplay)] pub enum Object { Address(Address), Hash([u8; 32]), @@ -53,24 +53,6 @@ impl Display for Object { } } -impl Serialize for Object { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for Object { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/options.rs b/src/options.rs index 9a93442f4e..b77002d6d6 100644 --- a/src/options.rs +++ b/src/options.rs @@ -34,7 +34,7 @@ pub struct Options { pub(crate) config_dir: Option, #[arg(long, help = "Load Bitcoin Core RPC cookie file from .")] pub(crate) cookie_file: Option, - #[arg(long, help = "Store index in .")] + #[arg(long, alias = "datadir", help = "Store index in .")] pub(crate) data_dir: Option, #[arg( long, diff --git a/src/ordzaar/mod.rs b/src/ordzaar/mod.rs index 7c01e31dcb..bf2718e0d0 100644 --- a/src/ordzaar/mod.rs +++ b/src/ordzaar/mod.rs @@ -2,3 +2,4 @@ use super::*; pub mod inscriptions; pub mod ordinals; +pub mod runes; diff --git a/src/ordzaar/ordinals.rs b/src/ordzaar/ordinals.rs index 6b02522f75..7d8c83a10f 100644 --- a/src/ordzaar/ordinals.rs +++ b/src/ordzaar/ordinals.rs @@ -30,41 +30,41 @@ pub struct Output { } pub fn get_ordinals(index: &Index, outpoint: OutPoint) -> Result> { - let index_list = index.list(outpoint)?; - if let Some(ranges) = index_list { - let mut ordinals = Vec::new(); - for Output { - output, - start, - end, - size, - offset, - rarity, - name, - } in list(outpoint, ranges) - { - let sat = Sat(start); - ordinals.push(OrdinalJson { - number: sat.n(), - decimal: sat.decimal().to_string(), - degree: sat.degree().to_string(), - name, - height: sat.height().0, - cycle: sat.cycle(), - epoch: sat.epoch().0, - period: sat.period(), - offset, - rarity, + match index.list(outpoint)? { + Some(ranges) => { + let mut ordinals = Vec::new(); + for Output { output, start, end, size, - }); + offset, + rarity, + name, + } in list(outpoint, ranges) + { + let sat = Sat(start); + ordinals.push(OrdinalJson { + number: sat.n(), + decimal: sat.decimal().to_string(), + degree: sat.degree().to_string(), + name, + height: sat.height().0, + cycle: sat.cycle(), + epoch: sat.epoch().0, + period: sat.period(), + offset, + rarity, + output, + start, + end, + size, + }); + } + Ok(ordinals) } - return Ok(ordinals); - }; - - return Ok(Vec::new()); + None => Ok(Vec::new()), + } } fn list(outpoint: OutPoint, ranges: Vec<(u64, u64)>) -> Vec { diff --git a/src/ordzaar/runes.rs b/src/ordzaar/runes.rs new file mode 100644 index 0000000000..3082a6a2a2 --- /dev/null +++ b/src/ordzaar/runes.rs @@ -0,0 +1,117 @@ +use super::*; + +// Custom Ordzaar Rune Response +// one of the reasons to create a custom response is to +// convert some of the bigint values into string +// and also to make the response consistent +// (prevent broken responses when bumping to the latest Ord version) + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct RuneOutpoint { + pub spaced_rune: SpacedRune, + pub amount: String, + pub divisibility: u8, + pub symbol: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct RuneTerms { + pub amount: Option, + pub cap: Option, + pub height: (Option, Option), + pub offset: (Option, Option), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct RuneDetail { + pub rune_id: RuneId, + pub mintable: bool, + pub rune: Rune, + pub block: String, + pub burned: String, + pub divisibility: u8, + pub etching: Txid, + pub mints: String, + pub number: String, + pub premine: String, + pub spaced_rune: SpacedRune, + pub symbol: Option, + pub terms: Option, + pub timestamp: String, +} + +impl RuneOutpoint { + pub fn from_spaced_rune_pile(spaced_rune_piled: (SpacedRune, Pile)) -> Self { + Self { + spaced_rune: spaced_rune_piled.0, + amount: spaced_rune_piled.1.amount.to_string(), + divisibility: spaced_rune_piled.1.divisibility, + symbol: spaced_rune_piled.1.symbol, + } + } +} + +impl RuneDetail { + pub fn from_rune(rune_id: RuneId, entry: RuneEntry, mintable: bool) -> Self { + let mut terms: Option = None; + + if let Some(terms_value) = entry.terms { + terms = Some(RuneTerms { + amount: match terms_value.amount { + Some(v) => Some(v.to_string()), + None => None, + }, + cap: match terms_value.cap { + Some(v) => Some(v.to_string()), + None => None, + }, + height: ( + match terms_value.height.0 { + Some(v) => Some(v.to_string()), + None => None, + }, + match terms_value.height.1 { + Some(v) => Some(v.to_string()), + None => None, + }, + ), + offset: ( + match terms_value.offset.0 { + Some(v) => Some(v.to_string()), + None => None, + }, + match terms_value.offset.1 { + Some(v) => Some(v.to_string()), + None => None, + }, + ), + }) + } + + Self { + block: entry.block.to_string(), + mintable, + rune_id, + rune: entry.spaced_rune.rune, + spaced_rune: entry.spaced_rune, + burned: entry.burned.to_string(), + divisibility: entry.divisibility, + etching: entry.etching, + terms, + mints: entry.mints.to_string(), + premine: entry.premine.to_string(), + number: entry.number.to_string(), + symbol: entry.symbol, + timestamp: entry.timestamp.to_string(), + } + } +} + +pub fn str_coma_to_array(str_coma: &str) -> Vec { + str_coma.split(',').map(|s| s.trim().to_string()).collect() +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct RuneOutputBulkQuery { + pub outpoints: String, +} diff --git a/src/outgoing.rs b/src/outgoing.rs index 72d00a1064..f1cad9acde 100644 --- a/src/outgoing.rs +++ b/src/outgoing.rs @@ -1,11 +1,12 @@ use super::*; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, DeserializeFromStr, SerializeDisplay)] pub enum Outgoing { Amount(Amount), InscriptionId(InscriptionId), - SatPoint(SatPoint), Rune { decimal: Decimal, rune: SpacedRune }, + Sat(Sat), + SatPoint(SatPoint), } impl Display for Outgoing { @@ -13,8 +14,9 @@ impl Display for Outgoing { match self { Self::Amount(amount) => write!(f, "{}", amount.to_string().to_lowercase()), Self::InscriptionId(inscription_id) => inscription_id.fmt(f), + Self::Rune { decimal, rune } => write!(f, "{decimal}:{rune}"), + Self::Sat(sat) => write!(f, "{}", sat.name()), Self::SatPoint(satpoint) => satpoint.fmt(f), - Self::Rune { decimal, rune } => write!(f, "{decimal} {rune}"), } } } @@ -24,8 +26,6 @@ impl FromStr for Outgoing { fn from_str(s: &str) -> Result { lazy_static! { - static ref SATPOINT: Regex = Regex::new(r"^[[:xdigit:]]{64}:\d+:\d+$").unwrap(); - static ref INSCRIPTION_ID: Regex = Regex::new(r"^[[:xdigit:]]{64}i\d+$").unwrap(); static ref AMOUNT: Regex = Regex::new( r"(?x) ^ @@ -36,7 +36,7 @@ impl FromStr for Outgoing { | \d+\.\d+ ) - \ * + \ ? (bit|btc|cbtc|mbtc|msat|nbtc|pbtc|sat|satoshi|ubtc) (s)? $ @@ -53,7 +53,7 @@ impl FromStr for Outgoing { | \d+\.\d+ ) - \ * + \s*:\s* ( [A-Z•.]+ ) @@ -63,9 +63,11 @@ impl FromStr for Outgoing { .unwrap(); } - Ok(if SATPOINT.is_match(s) { + Ok(if re::SAT_NAME.is_match(s) { + Self::Sat(s.parse()?) + } else if re::SATPOINT.is_match(s) { Self::SatPoint(s.parse()?) - } else if INSCRIPTION_ID.is_match(s) { + } else if re::INSCRIPTION_ID.is_match(s) { Self::InscriptionId(s.parse()?) } else if AMOUNT.is_match(s) { Self::Amount(s.parse()?) @@ -80,24 +82,6 @@ impl FromStr for Outgoing { } } -impl Serialize for Outgoing { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(self) - } -} - -impl<'de> Deserialize<'de> for Outgoing { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - DeserializeFromStr::with(deserializer) - } -} - #[cfg(test)] mod tests { use super::*; @@ -109,6 +93,9 @@ mod tests { assert_eq!(s.parse::().unwrap(), outgoing); } + case("nvtdijuwxlp", Outgoing::Sat("nvtdijuwxlp".parse().unwrap())); + case("a", Outgoing::Sat("a".parse().unwrap())); + case( "0000000000000000000000000000000000000000000000000000000000000000i0", Outgoing::InscriptionId( @@ -133,7 +120,7 @@ mod tests { case(".0btc", Outgoing::Amount("0 btc".parse().unwrap())); case( - "0 XYZ", + "0 : XYZ", Outgoing::Rune { rune: "XYZ".parse().unwrap(), decimal: "0".parse().unwrap(), @@ -141,7 +128,7 @@ mod tests { ); case( - "0XYZ", + "0:XYZ", Outgoing::Rune { rune: "XYZ".parse().unwrap(), decimal: "0".parse().unwrap(), @@ -149,7 +136,7 @@ mod tests { ); case( - "0.0XYZ", + "0.0:XYZ", Outgoing::Rune { rune: "XYZ".parse().unwrap(), decimal: "0.0".parse().unwrap(), @@ -157,7 +144,7 @@ mod tests { ); case( - ".0XYZ", + ".0:XYZ", Outgoing::Rune { rune: "XYZ".parse().unwrap(), decimal: ".0".parse().unwrap(), @@ -165,7 +152,7 @@ mod tests { ); case( - "1.1XYZ", + "1.1:XYZ", Outgoing::Rune { rune: "XYZ".parse().unwrap(), decimal: "1.1".parse().unwrap(), @@ -173,14 +160,12 @@ mod tests { ); case( - "1.1X.Y.Z", + "1.1:X.Y.Z", Outgoing::Rune { rune: "X.Y.Z".parse().unwrap(), decimal: "1.1".parse().unwrap(), }, ); - - assert!("0".parse::().is_err()); } #[test] @@ -191,6 +176,9 @@ mod tests { assert_eq!(s, outgoing.to_string()); } + case("nvtdijuwxlp", Outgoing::Sat("nvtdijuwxlp".parse().unwrap())); + case("a", Outgoing::Sat("a".parse().unwrap())); + case( "0000000000000000000000000000000000000000000000000000000000000000i0", Outgoing::InscriptionId( @@ -213,7 +201,7 @@ mod tests { case("1.2 btc", Outgoing::Amount("1.2 btc".parse().unwrap())); case( - "0 XY•Z", + "0:XY•Z", Outgoing::Rune { rune: "XY•Z".parse().unwrap(), decimal: "0".parse().unwrap(), @@ -221,7 +209,7 @@ mod tests { ); case( - "1.1 XYZ", + "1.1:XYZ", Outgoing::Rune { rune: "XYZ".parse().unwrap(), decimal: "1.1".parse().unwrap(), @@ -238,6 +226,13 @@ mod tests { assert_eq!(serde_json::from_str::(j).unwrap(), o); } + case( + "nvtdijuwxlp", + "\"nvtdijuwxlp\"", + Outgoing::Sat("nvtdijuwxlp".parse().unwrap()), + ); + case("a", "\"a\"", Outgoing::Sat("a".parse().unwrap())); + case( "0000000000000000000000000000000000000000000000000000000000000000i0", "\"0000000000000000000000000000000000000000000000000000000000000000i0\"", @@ -265,8 +260,8 @@ mod tests { ); case( - "6.66 HELL.MONEY", - "\"6.66 HELL•MONEY\"", + "6.66:HELL.MONEY", + "\"6.66:HELL•MONEY\"", Outgoing::Rune { rune: "HELL•MONEY".parse().unwrap(), decimal: "6.66".parse().unwrap(), diff --git a/src/re.rs b/src/re.rs new file mode 100644 index 0000000000..ad2d50288b --- /dev/null +++ b/src/re.rs @@ -0,0 +1,27 @@ +use super::*; + +fn re(s: &'static str) -> Regex { + Regex::new(&format!("^{s}$")).unwrap() +} + +lazy_static! { + pub(crate) static ref HASH: Regex = re(r"[[:xdigit:]]{64}"); + pub(crate) static ref INSCRIPTION_ID: Regex = re(r"[[:xdigit:]]{64}i\d+"); + pub(crate) static ref INSCRIPTION_NUMBER: Regex = re(r"-?[0-9]+"); + pub(crate) static ref OUTPOINT: Regex = re(r"[[:xdigit:]]{64}:\d+"); + pub(crate) static ref RUNE_ID: Regex = re(r"[0-9]+:[0-9]+"); + pub(crate) static ref SATPOINT: Regex = re(r"[[:xdigit:]]{64}:\d+:\d+"); + pub(crate) static ref SAT_NAME: Regex = re(r"[a-z]{1,11}"); + pub(crate) static ref SPACED_RUNE: Regex = re(r"[A-Z•.]+"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sat_name() { + assert!(SAT_NAME.is_match(&Sat(0).name())); + assert!(SAT_NAME.is_match(&Sat::LAST.name())); + } +} diff --git a/src/runes.rs b/src/runes.rs index 1de3fb8544..3919273fb3 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1,29 +1,23 @@ -use { - self::{flag::Flag, tag::Tag}, - super::*, -}; - -pub use {edict::Edict, rune::Rune, rune_id::RuneId, runestone::Runestone}; - -pub(crate) use {etching::Etching, mint::Mint, pile::Pile, spaced_rune::SpacedRune}; - -pub const MAX_DIVISIBILITY: u8 = 38; -pub(crate) const MAX_LIMIT: u128 = 1 << 64; -const RESERVED: u128 = 6402364363415443603228541259936211926; - -mod edict; -mod etching; -mod flag; -mod mint; -mod pile; -mod rune; -mod rune_id; -mod runestone; -mod spaced_rune; -mod tag; -pub mod varint; +use super::*; + +#[derive(Debug, PartialEq)] +pub enum MintError { + Cap(u128), + End(u64), + Start(u64), + Unmintable, +} -type Result = std::result::Result; +impl Display for MintError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + MintError::Cap(cap) => write!(f, "limited to {cap} mints"), + MintError::End(end) => write!(f, "mint ended on block {end}"), + MintError::Start(start) => write!(f, "mint starts on block {start}"), + MintError::Unmintable => write!(f, "not mintable"), + } + } +} #[cfg(test)] mod tests { @@ -43,27 +37,16 @@ mod tests { context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes([], []); } @@ -74,10 +57,12 @@ mod tests { context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.etch(Default::default(), 1); + + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], op_return: Some(Runestone::default().encipher()), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -89,38 +74,29 @@ mod tests { fn etching_with_no_edicts_creates_rune() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + ..default() }, )], [], @@ -131,44 +107,36 @@ mod tests { fn etching_with_edict_creates_rune() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -177,7 +145,11 @@ mod tests { #[test] fn runes_must_be_greater_than_or_equal_to_minimum_for_height() { - let block_two_minimum: u128 = Rune::minimum_at_height(Chain::Regtest, Height(2)).0; + let minimum = Rune::minimum_at_height( + Chain::Regtest.network(), + Height((Runestone::COMMIT_INTERVAL + 2).into()), + ) + .0; { let context = Context::builder() @@ -185,74 +157,62 @@ mod tests { .arg("--index-runes") .build(); - context.mine_blocks(1); - - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(block_two_minimum - 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(minimum - 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes([], []); } { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(block_two_minimum)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + let context = Context::builder() + .chain(Chain::Regtest) + .arg("--index-runes") + .build(); - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(minimum)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(block_two_minimum), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(minimum), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -265,29 +225,21 @@ mod tests { { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RESERVED)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune::reserved(0, 0)), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes([], []); } @@ -295,44 +247,36 @@ mod tests { { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RESERVED - 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(Rune::reserved(0, 0).n() - 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RESERVED - 1), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(Rune::reserved(0, 0).n() - 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -346,31 +290,29 @@ mod tests { context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid0 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![Edict { - id: 0, + id: RuneId::default(), amount: u128::MAX, output: 0, }], etching: Some(Etching { rune: None, - ..Default::default() + premine: Some(u128::MAX), + ..default() }), - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); - let id0 = RuneId { - height: 2, - index: 1, - }; + let id0 = RuneId { block: 2, tx: 1 }; context.mine_blocks(1); @@ -378,11 +320,15 @@ mod tests { [( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RESERVED), - supply: u128::MAX, + spaced_rune: SpacedRune { + rune: Rune::reserved(id0.block, id0.tx), + spacers: 0, + }, + premine: u128::MAX, timestamp: 2, - ..Default::default() + ..default() }, )], [( @@ -396,54 +342,60 @@ mod tests { context.mine_blocks(1); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: 0, + id: RuneId::default(), amount: u128::MAX, output: 0, }], etching: Some(Etching { + premine: Some(u128::MAX), rune: None, - ..Default::default() + ..default() }), - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id1 = RuneId { - height: 4, - index: 1, - }; + let id1 = RuneId { block: 4, tx: 1 }; context.assert_runes( [ ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RESERVED), - supply: u128::MAX, + spaced_rune: SpacedRune { + rune: Rune::reserved(id0.block, id0.tx), + spacers: 0, + }, + premine: u128::MAX, timestamp: 2, - ..Default::default() + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RESERVED + 1), - supply: u128::MAX, + spaced_rune: SpacedRune { + rune: Rune::reserved(id1.block, id0.tx), + spacers: 0, + }, + premine: u128::MAX, timestamp: 4, number: 1, - ..Default::default() + ..default() }, ), ], @@ -470,46 +422,40 @@ mod tests { fn etching_with_non_zero_divisibility_and_rune() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - divisibility: 1, - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + divisibility: Some(1), + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, RuneEntry { - rune: Rune(RUNE), + block: id.block, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, etching: txid, divisibility: 1, - supply: u128::MAX, - timestamp: 2, - ..Default::default() + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -520,51 +466,43 @@ mod tests { fn allocations_over_max_supply_are_ignored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -575,52 +513,44 @@ mod tests { fn allocations_partially_over_max_supply_are_honored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX / 2, - output: 0, - }, - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: u128::MAX / 2, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, symbol: None, - timestamp: 2, - ..Default::default() + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -633,43 +563,37 @@ mod tests { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 100, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: 100, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(100), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: 100, - timestamp: 2, - ..Default::default() - }, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 100, + timestamp: id.block, + ..default() + }, )], [(OutPoint { txid, vout: 0 }, vec![(id, 100)])], ); @@ -679,52 +603,44 @@ mod tests { fn etching_may_allocate_to_multiple_outputs() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 100, - output: 0, - }, - Edict { - id: 0, - amount: 100, - output: 1, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 100, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: 100, + output: 1, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(200), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, burned: 100, etching: txid, - rune: Rune(RUNE), - supply: 200, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 200, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, 100)])], @@ -732,57 +648,48 @@ mod tests { } #[test] - fn allocations_to_invalid_outputs_are_ignored() { + fn allocations_to_invalid_outputs_produce_cenotaph() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 100, - output: 0, - }, - Edict { - id: 0, - amount: 100, - output: 3, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 100, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: 100, + output: 3, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: 100, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() }, )], - [(OutPoint { txid, vout: 0 }, vec![(id, 100)])], + [], ); } @@ -790,44 +697,36 @@ mod tests { fn input_runes_may_be_allocated() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -839,20 +738,20 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: id.into(), + id, amount: u128::MAX, output: 0, }], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -861,11 +760,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -879,48 +782,38 @@ mod tests { } #[test] - fn etched_rune_is_allocated_with_zero_supply_for_burned_runestone() { + fn etched_rune_is_allocated_with_zero_supply_for_cenotaph() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - default_output: None, - burn: true, - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..default() + }), + pointer: Some(10), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + ..default() }, )], [], @@ -928,61 +821,53 @@ mod tests { } #[test] - fn etched_rune_open_etching_parameters_are_unset_for_burned_runestone() { + fn etched_rune_parameters_are_unset_for_cenotaph() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - deadline: Some(1), - limit: Some(1), - term: Some(1), - }), - divisibility: 1, - symbol: Some('$'), - spacers: 1, + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + premine: Some(u128::MAX), + rune: Some(Rune(RUNE)), + terms: Some(Terms { + cap: Some(1), + amount: Some(1), + offset: (Some(1), Some(1)), + height: (None, None), }), - burn: true, - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + divisibility: Some(1), + symbol: Some('$'), + spacers: Some(1), + }), + pointer: Some(10), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, burned: 0, - divisibility: 1, + divisibility: 0, etching: txid0, - mint: None, + terms: None, mints: 0, number: 0, - rune: Rune(RUNE), - spacers: 1, - supply: 0, - symbol: Some('$'), - timestamp: 2, + premine: 0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + symbol: None, + timestamp: id.block, }, )], [], @@ -990,92 +875,68 @@ mod tests { } #[test] - fn etched_reserved_rune_is_allocated_with_zero_supply_for_burned_runestone() { + fn reserved_runes_are_not_allocated_in_cenotaph() { let context = Context::builder().arg("--index-runes").build(); context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: 0, + id: RuneId::default(), amount: u128::MAX, output: 0, }], etching: Some(Etching::default()), - burn: true, - ..Default::default() + pointer: Some(10), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RESERVED), - timestamp: 2, - ..Default::default() - }, - )], - [], - ); + context.assert_runes([], []); } #[test] fn input_runes_are_burned_if_an_unrecognized_even_tag_is_encountered() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1087,16 +948,16 @@ mod tests { )], ); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { - burn: true, - ..Default::default() + pointer: Some(10), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -1105,12 +966,16 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: u128::MAX, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [], @@ -1121,44 +986,36 @@ mod tests { fn unallocated_runes_are_assigned_to_first_non_op_return_output() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1170,10 +1027,10 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some(Runestone::default().encipher()), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -1182,11 +1039,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1203,44 +1064,36 @@ mod tests { fn unallocated_runes_are_burned_if_no_non_op_return_output_is_present() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1252,11 +1105,11 @@ mod tests { )], ); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some(Runestone::default().encipher()), outputs: 0, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -1265,12 +1118,16 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, burned: u128::MAX, - ..Default::default() + ..default() }, )], [], @@ -1281,44 +1138,36 @@ mod tests { fn unallocated_runes_are_assigned_to_default_output() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1330,17 +1179,17 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { - default_output: Some(1), - ..Default::default() + pointer: Some(1), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -1349,11 +1198,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1366,137 +1219,40 @@ mod tests { ); } - #[test] - fn unallocated_runes_are_assigned_to_first_non_op_return_output_if_default_is_too_large() { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; - - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() - }, - )], - [( - OutPoint { - txid: txid0, - vout: 0, - }, - vec![(id, u128::MAX)], - )], - ); - - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], - outputs: 2, - op_return: Some( - Runestone { - default_output: Some(3), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() - }, - )], - [( - OutPoint { - txid: txid1, - vout: 0, - }, - vec![(id, u128::MAX)], - )], - ); - } - #[test] fn unallocated_runes_are_burned_if_default_output_is_op_return() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1508,17 +1264,17 @@ mod tests { )], ); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { - default_output: Some(2), - ..Default::default() + pointer: Some(2), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -1527,12 +1283,16 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, burned: u128::MAX, - timestamp: 2, - ..Default::default() + timestamp: id.block, + ..default() }, )], [], @@ -1544,44 +1304,36 @@ mod tests { ) { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1593,10 +1345,10 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: None, - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -1605,11 +1357,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -1626,68 +1382,56 @@ mod tests { fn duplicate_runes_are_forbidden() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], ); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..default() + }), + ..default() + }, + 1, + ); context.mine_blocks(1); @@ -1695,11 +1439,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -1707,47 +1455,39 @@ mod tests { } #[test] - fn outpoint_may_hold_multiple_runes() { + fn output_may_hold_multiple_runes() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, )], [( @@ -1759,54 +1499,52 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [ ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -1828,9 +1566,12 @@ mod tests { ], ); - let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new()), (3, 1, 0, Witness::new())], - ..Default::default() + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[ + (id0.block.try_into().unwrap(), 1, 0, Witness::new()), + (id1.block.try_into().unwrap(), 1, 0, Witness::new()), + ], + ..default() }); context.mine_blocks(1); @@ -1840,22 +1581,30 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -1873,44 +1622,36 @@ mod tests { fn multiple_input_runes_on_the_same_input_may_be_allocated() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, )], [( @@ -1922,54 +1663,52 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [ ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -1991,9 +1730,12 @@ mod tests { ], ); - let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new()), (3, 1, 0, Witness::new())], - ..Default::default() + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[ + (id0.block.try_into().unwrap(), 1, 0, Witness::new()), + (id1.block.try_into().unwrap(), 1, 0, Witness::new()), + ], + ..default() }); context.mine_blocks(1); @@ -2003,22 +1745,30 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2031,28 +1781,28 @@ mod tests { )], ); - let txid3 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(4, 1, 0, Witness::new())], + let txid3 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[((id1.block + 1).try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![ Edict { - id: id0.into(), + id: id0, amount: u128::MAX / 2, output: 1, }, Edict { - id: id1.into(), + id: id1, amount: u128::MAX / 2, output: 1, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2062,22 +1812,30 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2104,44 +1862,36 @@ mod tests { fn multiple_input_runes_on_different_inputs_may_be_allocated() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, )], [( @@ -2153,54 +1903,52 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [ ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2222,27 +1970,30 @@ mod tests { ], ); - let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new()), (3, 1, 0, Witness::new())], + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[ + (id0.block.try_into().unwrap(), 1, 0, Witness::new()), + (id1.block.try_into().unwrap(), 1, 0, Witness::new()), + ], op_return: Some( Runestone { edicts: vec![ Edict { - id: id0.into(), + id: id0, amount: u128::MAX, output: 0, }, Edict { - id: id1.into(), + id: id1, amount: u128::MAX, output: 0, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2252,22 +2003,30 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2286,44 +2045,36 @@ mod tests { ) { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -2335,15 +2086,15 @@ mod tests { )], ); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .into_script(), ), op_return_index: Some(0), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2352,11 +2103,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 1 }, vec![(id, u128::MAX)])], @@ -2364,84 +2119,72 @@ mod tests { } #[test] - fn rune_rarity_is_assigned_correctly() { + fn multiple_runes_may_be_etched_in_one_block() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(2); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - let id0 = RuneId { - height: 3, - index: 1, - }; - - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); - let id1 = RuneId { - height: 3, - index: 2, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [ ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 3, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2468,44 +2211,36 @@ mod tests { fn edicts_with_id_zero_are_skipped() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -2517,27 +2252,27 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![ Edict { - id: 0, + id: RuneId::default(), amount: 100, output: 0, }, Edict { - id: id.into(), + id, amount: u128::MAX, output: 0, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2546,11 +2281,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -2567,44 +2306,36 @@ mod tests { fn edicts_which_refer_to_input_rune_with_no_balance_are_skipped() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, )], [( @@ -2616,54 +2347,52 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [ ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2685,27 +2414,27 @@ mod tests { ], ); - let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id0.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![ Edict { - id: id0.into(), + id: id0, amount: u128::MAX, output: 0, }, Edict { - id: id1.into(), + id: id1, amount: u128::MAX, output: 0, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2715,22 +2444,30 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id0.block, + ..default() }, ), ( id1, RuneEntry { + block: id1.block, etching: txid1, - rune: Rune(RUNE + 1), - supply: u128::MAX, - timestamp: 3, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id1.block, number: 1, - ..Default::default() + ..default() }, ), ], @@ -2757,44 +2494,36 @@ mod tests { fn edicts_over_max_inputs_are_ignored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX / 2, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX / 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX / 2), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX / 2, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX / 2, + timestamp: id.block, + ..default() }, )], [( @@ -2806,20 +2535,20 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: id.into(), + id, amount: u128::MAX, output: 0, }], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -2828,11 +2557,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX / 2, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX / 2, + timestamp: id.block, + ..default() }, )], [( @@ -2849,45 +2582,37 @@ mod tests { fn edicts_may_transfer_runes_to_op_return_outputs() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 1, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 1, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, burned: u128::MAX, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [], @@ -2898,45 +2623,36 @@ mod tests { fn outputs_with_no_runes_have_no_balance() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 2, - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -2947,52 +2663,43 @@ mod tests { fn edicts_which_transfer_no_runes_to_output_create_no_balance_entry() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 2, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - Edict { - id: 0, - amount: 0, - output: 1, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: 0, + output: 1, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -3003,45 +2710,36 @@ mod tests { fn split_in_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 0, - output: 5, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: 0, + output: 5, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 4, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3057,52 +2755,43 @@ mod tests { fn split_in_etching_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 1000, - output: 0, - }, - Edict { - id: 0, - amount: 0, - output: 5, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 1000, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: 0, + output: 5, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 4, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3130,52 +2819,43 @@ mod tests { fn split_in_etching_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 0, - output: 5, - }, - Edict { - id: 0, - amount: 1000, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 0, + output: 5, + }, + Edict { + id: RuneId::default(), + amount: 1000, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 4, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3191,45 +2871,36 @@ mod tests { fn split_with_amount_in_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 1000, - output: 5, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: 1000, + output: 5, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(4000), + ..default() + }), + ..default() + }, + 4, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: 4000, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 4000, + timestamp: id.block, + ..default() }, )], [ @@ -3245,52 +2916,43 @@ mod tests { fn split_in_etching_with_amount_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX - 3000, - output: 0, - }, - Edict { - id: 0, - amount: 1000, - output: 5, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: u128::MAX - 3000, + output: 0, + }, + Edict { + id: RuneId::default(), + amount: 1000, + output: 5, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 4, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3305,52 +2967,43 @@ mod tests { fn split_in_etching_with_amount_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 1000, - output: 5, - }, - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: RuneId::default(), + amount: 1000, + output: 5, + }, + Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 4, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3369,44 +3022,36 @@ mod tests { fn split() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -3418,21 +3063,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![Edict { - id: id.into(), + id, amount: 0, output: 3, }], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3441,11 +3086,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3471,44 +3120,36 @@ mod tests { fn split_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -3520,28 +3161,28 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![ Edict { - id: id.into(), + id, amount: 1000, output: 0, }, Edict { - id: id.into(), + id, amount: 0, output: 3, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3550,11 +3191,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3580,44 +3225,36 @@ mod tests { fn split_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -3629,28 +3266,28 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![ Edict { - id: id.into(), + id, amount: 0, output: 3, }, Edict { - id: id.into(), + id, amount: 1000, output: 1, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3659,11 +3296,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3689,44 +3330,36 @@ mod tests { fn split_with_amount() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -3738,21 +3371,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![Edict { - id: id.into(), + id, amount: 1000, output: 3, }], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3761,11 +3394,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3791,44 +3428,36 @@ mod tests { fn split_with_amount_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -3840,28 +3469,28 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 4, op_return: Some( Runestone { edicts: vec![ Edict { - id: id.into(), + id, amount: u128::MAX - 2000, output: 0, }, Edict { - id: id.into(), + id, amount: 1000, output: 5, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3870,11 +3499,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -3900,44 +3533,36 @@ mod tests { fn split_with_amount_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -3949,28 +3574,28 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 4, op_return: Some( Runestone { edicts: vec![ Edict { - id: id.into(), + id, amount: 1000, output: 5, }, Edict { - id: id.into(), + id, amount: u128::MAX, output: 0, }, ], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -3979,11 +3604,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [ @@ -4023,46 +3652,38 @@ mod tests { fn etching_may_specify_symbol() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - symbol: Some('$'), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + symbol: Some('$'), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, symbol: Some('$'), - timestamp: 2, - ..Default::default() + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -4073,44 +3694,36 @@ mod tests { fn allocate_all_remaining_runes_in_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 0, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: 0, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], @@ -4121,44 +3734,36 @@ mod tests { fn allocate_all_remaining_runes_in_inputs() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -4170,21 +3775,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { edicts: vec![Edict { - id: id.into(), + id, amount: 0, output: 1, }], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4193,11 +3798,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() }, )], [( @@ -4210,75 +3819,59 @@ mod tests { ); } - #[test] - fn max_limit() { - MAX_LIMIT - .checked_mul(u128::from(u16::MAX) * 144 * 365 * 1_000_000_000) - .unwrap(); - } - #[test] fn rune_can_be_minted_without_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - // etch the rune - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - timestamp: 2, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, mints: 0, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() + ..default() }, )], [], ); - // claim the rune - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid1 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4287,16 +3880,21 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), mints: 1, - rune: Rune(RUNE), - supply: 1000, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() }, )], [( @@ -4310,72 +3908,64 @@ mod tests { } #[test] - fn etching_with_limit_can_be_minted() { + fn etching_with_amount_can_be_minted() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - // etch the rune - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + cap: Some(100), + amount: Some(1000), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - timestamp: 2, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + premine: 0, mints: 0, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() + ..default() }, )], [], ); - // claim the rune - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: u128::from(id), + id, amount: 1000, output: 0, }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4384,16 +3974,21 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), mints: 1, - rune: Rune(RUNE), - supply: 1000, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() }, )], [( @@ -4406,21 +4001,21 @@ mod tests { ); // claim the rune - let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(3, 0, 0, Witness::new())], + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(4, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: u128::from(id), + id, amount: 1000, output: 0, }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4429,16 +4024,21 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), mints: 2, - rune: Rune(RUNE), - supply: 2000, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() }, )], [ @@ -4460,22 +4060,22 @@ mod tests { ); // claim the rune in a burn runestone - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(4, 0, 0, Witness::new())], + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(5, 0, 0, Witness::new())], op_return: Some( Runestone { - burn: true, - claim: Some(u128::from(id)), + pointer: Some(10), + mint: Some(id), edicts: vec![Edict { - id: u128::from(id), + id, amount: 1000, output: 0, }], - ..Default::default() + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4484,17 +4084,22 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: 1000, etching: txid0, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), mints: 3, - rune: Rune(RUNE), - supply: 3000, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() }, )], [ @@ -4517,92 +4122,63 @@ mod tests { } #[test] - fn open_etchings_can_be_limited_to_term() { + fn open_mints_can_be_limited_with_offset_end() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - term: Some(2), - ..Default::default() - }), - ..Default::default() + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + offset: (None, Some(2)), + ..default() }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + ..default() + }), + ..default() + }, + 1, + ); - let id = RuneId { - height: 2, - index: 1, + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + offset: (None, Some(2)), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() }; - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - end: Some(4), - ..Default::default() - }), - timestamp: 2, - ..Default::default() - }, - )], - [], - ); + context.assert_runes([(id, entry)], []); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid1 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - end: Some(4), - ..Default::default() - }), - supply: 1000, - timestamp: 2, - mints: 1, - ..Default::default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4612,42 +4188,22 @@ mod tests { )], ); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - supply: 1000, - timestamp: 2, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(4), - ..Default::default() - }), - mints: 1, - ..Default::default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4659,187 +4215,231 @@ mod tests { } #[test] - fn open_etchings_with_term_zero_cannot_be_minted() { + fn open_mints_can_be_limited_with_offset_start() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + offset: (Some(2), None), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + offset: (Some(2), None), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: 0, - amount: 1000, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - term: Some(0), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - - context.assert_runes( - [( - id, - RuneEntry { - etching: txid, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - end: Some(2), - ..Default::default() - }), - timestamp: 2, - ..Default::default() - }, - )], - [], - ); + context.assert_runes([(id, entry)], []); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - outputs: 2, + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1, - output: 3, - }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( + [(id, entry)], [( - id, - RuneEntry { - etching: txid, - rune: Rune(RUNE), - timestamp: 2, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(2), - ..Default::default() - }), - ..Default::default() + OutPoint { + txid: txid1, + vout: 0, }, + vec![(id, 1000)], )], - [], ); } #[test] - fn open_etchings_can_be_limited_to_deadline() { + fn open_mints_can_be_limited_with_height_start() { let context = Context::builder().arg("--index-runes").build(); + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + height: (Some(10), None), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); + + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + height: (Some(10), None), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], + op_return: Some( + Runestone { + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + context.assert_runes([(id, entry)], []); + + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - deadline: Some(4), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; + entry.mints += 1; context.assert_runes( + [(id, entry)], [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - timestamp: 2, - mint: Some(MintEntry { - deadline: Some(4), - limit: Some(1000), - ..Default::default() - }), - ..Default::default() + OutPoint { + txid: txid1, + vout: 0, }, + vec![(id, 1000)], )], - [], ); + } + + #[test] + fn open_mints_can_be_limited_with_height_end() { + let context = Context::builder().arg("--index-runes").build(); + + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + height: (None, Some(10)), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); + + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + height: (None, Some(10)), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + context.assert_runes([(id, entry)], []); + + let txid1 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( - [( - id, - RuneEntry { - rune: Rune(RUNE), - supply: 1000, - timestamp: 2, - mints: 1, - etching: txid0, - mint: Some(MintEntry { - deadline: Some(4), - limit: Some(1000), - ..Default::default() - }), - ..Default::default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4849,42 +4449,22 @@ mod tests { )], ); - context.rpc_server.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - supply: 1000, - timestamp: 2, - mint: Some(MintEntry { - limit: Some(1000), - deadline: Some(4), - ..Default::default() - }), - mints: 1, - ..Default::default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4896,70 +4476,150 @@ mod tests { } #[test] - fn open_etching_claims_can_use_split() { + fn open_mints_with_offset_end_zero_can_be_premined() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: 1111, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(1111), + terms: Some(Terms { + amount: Some(1000), + offset: (None, Some(0)), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + offset: (None, Some(0)), + ..default() + }), + timestamp: id.block, + premine: 1111, + ..default() + }, + )], + [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])], + ); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], + outputs: 2, op_return: Some( Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + terms: Some(Terms { + amount: Some(1000), + offset: (None, Some(0)), + ..default() + }), + premine: 1111, + ..default() + }, + )], + [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])], + ); + } + + #[test] + fn open_mints_can_be_limited_to_cap() { + let context = Context::builder().arg("--index-runes").build(); + + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(2), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { + block: id.block, etching: txid0, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + terms: Some(Terms { + amount: Some(1000), + cap: Some(2), + ..default() }), - timestamp: 2, - ..Default::default() + ..default() }, )], [], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid1 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], - outputs: 2, op_return: Some( Runestone { edicts: vec![Edict { - id: u128::from(id), - amount: 0, - output: 3, + id, + amount: 1000, + output: 0, }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -4968,158 +4628,204 @@ mod tests { [( id, RuneEntry { + block: id.block, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mints: 1, etching: txid0, - rune: Rune(RUNE), - supply: 1000, - timestamp: 2, - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + terms: Some(Terms { + cap: Some(2), + amount: Some(1000), + ..default() }), - mints: 1, - ..Default::default() + ..default() }, )], - [ - ( - OutPoint { - txid: txid1, - vout: 0, - }, - vec![(id, 500)], - ), + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 0, + }], + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + terms: Some(Terms { + amount: Some(1000), + cap: Some(2), + ..default() + }), + mints: 2, + ..default() + }, + )], + [ ( OutPoint { txid: txid1, - vout: 1, + vout: 0, }, - vec![(id, 500)], + vec![(id, 1000)], + ), + ( + OutPoint { + txid: txid2, + vout: 0, + }, + vec![(id, 1000)], ), ], ); - } - #[test] - fn runes_can_be_etched_and_claimed_in_the_same_transaction() { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(4, 0, 0, Witness::new())], op_return: Some( Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() - }), edicts: vec![Edict { - id: 0, - amount: 2000, + id, + amount: 1000, output: 0, }], - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, RuneEntry { - etching: txid, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + terms: Some(Terms { + amount: Some(1000), + cap: Some(2), + ..default() }), - timestamp: 2, - supply: 1000, - ..Default::default() + mints: 2, + ..default() }, )], - [(OutPoint { txid, vout: 0 }, vec![(id, 1000)])], + [ + ( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + ), + ( + OutPoint { + txid: txid2, + vout: 0, + }, + vec![(id, 1000)], + ), + ], ); } #[test] - fn limit_over_max_is_clamped() { + fn open_mint_claims_can_use_split() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(MAX_LIMIT + 1), - ..Default::default() - }), - ..Default::default() + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, - rune: Rune(RUNE), - timestamp: 2, - mint: Some(MintEntry { - limit: Some(MAX_LIMIT), - deadline: None, - end: None, + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() + timestamp: id.block, + ..default() }, )], [], ); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + outputs: 2, op_return: Some( Runestone { edicts: vec![Edict { - id: u128::from(id), - amount: MAX_LIMIT + 1, - output: 0, + id, + amount: 0, + output: 3, }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); @@ -5128,68 +4834,125 @@ mod tests { [( id, RuneEntry { - etching, - rune: Rune(RUNE), - timestamp: 2, - mints: 1, - supply: MAX_LIMIT, - mint: Some(MintEntry { - limit: Some(MAX_LIMIT), - deadline: None, - end: None, + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() + mints: 1, + ..default() }, )], - [(OutPoint { txid, vout: 0 }, vec![(id, MAX_LIMIT)])], + [ + ( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 500)], + ), + ( + OutPoint { + txid: txid1, + vout: 1, + }, + vec![(id, 500)], + ), + ], ); } #[test] - fn omitted_limit_defaults_to_max_limit() { + fn runes_can_be_etched_and_premined_in_the_same_transaction() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(2000), + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + ..default() + }), + edicts: vec![Edict { + id: RuneId::default(), + amount: 2000, + output: 0, + }], + ..default() + }, + 1, + ); - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - term: Some(1), - ..Default::default() - }), - ..Default::default() + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + ..default() }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + timestamp: id.block, + premine: 2000, + ..default() + }, + )], + [(OutPoint { txid, vout: 0 }, vec![(id, 2000)])], + ); + } - context.mine_blocks(1); + #[test] + fn omitted_edicts_defaults_to_mint_amount() { + let context = Context::builder().arg("--index-runes").build(); - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + offset: (None, Some(1)), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: None, - end: Some(3), - ..Default::default() + block: id.block, + etching: txid, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: None, + offset: (None, Some(1)), + ..default() }), - timestamp: 2, - ..Default::default() + timestamp: id.block, + ..default() }, )], [], @@ -5197,233 +4960,473 @@ mod tests { } #[test] - fn transactions_cannot_claim_more_than_limit() { + fn premines_can_claim_over_mint_amount() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(2000), + terms: Some(Terms { + amount: Some(1000), + cap: Some(1), + ..default() }), - edicts: vec![Edict { - id: 0, - amount: 2000, - output: 0, - }], - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ..default() + }), + edicts: vec![Edict { + id: RuneId::default(), + amount: 2000, + output: 0, + }], + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + block: id.block, + etching: txid, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(1), + ..default() }), - timestamp: 2, - supply: 1000, - ..Default::default() - }, - )], - [( - OutPoint { - txid: etching, - vout: 0, + timestamp: id.block, + premine: 2000, + mints: 0, + ..default() }, - vec![(id, 1000)], )], + [(OutPoint { txid, vout: 0 }, vec![(id, 2000)])], + ); + } + + #[test] + fn transactions_cannot_claim_more_than_mint_amount() { + let context = Context::builder().arg("--index-runes").build(); + + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() + }), + ..default() + }), + ..default() + }, + 1, ); - let edict = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { - id: u128::from(id), + id, amount: 2000, output: 0, }], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, RuneEntry { - etching, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - timestamp: 2, - supply: 2000, + timestamp: id.block, mints: 1, - ..Default::default() + ..default() }, )], - [ - ( - OutPoint { - txid: etching, - vout: 0, - }, - vec![(id, 1000)], - ), - ( - OutPoint { - txid: edict, - vout: 0, - }, - vec![(id, 1000)], - ), - ], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], ); } #[test] - fn multiple_edicts_in_one_transaction_may_claim_open_etching() { + fn multiple_edicts_in_one_transaction_may_claim_open_mint() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ..default() + }), + ..default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - timestamp: 2, - ..Default::default() + timestamp: id.block, + ..default() }, )], [], ); - let edict = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![ Edict { - id: u128::from(id), + id, amount: 500, output: 0, }, Edict { - id: u128::from(id), + id, amount: 500, output: 0, }, Edict { - id: u128::from(id), + id, amount: 500, output: 0, }, ], - claim: Some(u128::from(id)), - ..Default::default() + mint: Some(id), + ..default() } .encipher(), ), - ..Default::default() + ..default() }); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, RuneEntry { - etching, - rune: Rune(RUNE), - mint: Some(MintEntry { - limit: Some(1000), - ..Default::default() + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() }), - timestamp: 2, - supply: 1000, + timestamp: id.block, mints: 1, - ..Default::default() + ..default() }, )], [( OutPoint { - txid: edict, + txid: txid1, vout: 0, }, vec![(id, 1000)], )], ); } + + #[test] + fn commits_are_not_valid_in_non_taproot_witnesses() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = context.index.block_count().unwrap().into_usize(); + + context.mine_blocks(1); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: false, + ..default() + }); + + context.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + + let mut witness = Witness::new(); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + ..default() + }), + ..default() + }; + + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + runestone + .etching + .unwrap() + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + + witness.push(tapscript); + + witness.push([]); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + #[test] + fn immature_commits_are_not_valid() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = context.index.block_count().unwrap().into_usize(); + + context.mine_blocks(1); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..default() + }); + + context.mine_blocks((Runestone::COMMIT_INTERVAL - 1).into()); + + let mut witness = Witness::new(); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + ..default() + }), + ..default() + }; + + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + runestone + .etching + .unwrap() + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + + witness.push(tapscript); + + witness.push([]); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + #[test] + fn etchings_are_not_valid_without_commitment() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = context.index.block_count().unwrap().into_usize(); + + context.mine_blocks(1); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..default() + }); + + context.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + + let mut witness = Witness::new(); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + ..default() + }), + ..default() + }; + + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>([].as_slice().try_into().unwrap()) + .into_script(); + + witness.push(tapscript); + + witness.push([]); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + #[test] + fn tx_commits_to_rune_ignores_invalid_script() { + let context = Context::builder().arg("--index-runes").build(); + + context.mine_blocks(1); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + terms: Some(Terms { + amount: Some(1000), + ..default() + }), + ..default() + }), + ..default() + }; + + let mut witness = Witness::new(); + + witness.push([opcodes::all::OP_PUSHDATA4.to_u8()]); + witness.push([]); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + #[test] + fn genesis_rune() { + assert_eq!( + Chain::Mainnet.first_rune_height(), + SUBSIDY_HALVING_INTERVAL * 4, + ); + + Context::builder() + .chain(Chain::Mainnet) + .arg("--index-runes") + .build() + .assert_runes( + [( + RuneId { block: 1, tx: 0 }, + RuneEntry { + block: 1, + burned: 0, + divisibility: 0, + etching: Txid::all_zeros(), + mints: 0, + number: 0, + premine: 0, + spaced_rune: SpacedRune { + rune: Rune(2055900680524219742), + spacers: 128, + }, + symbol: Some('\u{29C9}'), + terms: Some(Terms { + amount: Some(1), + cap: Some(u128::MAX), + height: ( + Some((SUBSIDY_HALVING_INTERVAL * 4).into()), + Some((SUBSIDY_HALVING_INTERVAL * 5).into()), + ), + offset: (None, None), + }), + timestamp: 0, + }, + )], + [], + ); + } } diff --git a/src/runes/edict.rs b/src/runes/edict.rs deleted file mode 100644 index 4caa4477a7..0000000000 --- a/src/runes/edict.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Debug, PartialEq, Copy, Clone)] -pub struct Edict { - pub id: u128, - pub amount: u128, - pub output: u128, -} diff --git a/src/runes/etching.rs b/src/runes/etching.rs deleted file mode 100644 index 1f051148d8..0000000000 --- a/src/runes/etching.rs +++ /dev/null @@ -1,10 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Debug, PartialEq, Copy, Clone)] -pub struct Etching { - pub divisibility: u8, - pub mint: Option, - pub rune: Option, - pub spacers: u32, - pub symbol: Option, -} diff --git a/src/runes/mint.rs b/src/runes/mint.rs deleted file mode 100644 index 5632d9b023..0000000000 --- a/src/runes/mint.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Debug, PartialEq, Copy, Clone)] -pub struct Mint { - pub deadline: Option, - pub limit: Option, - pub term: Option, -} diff --git a/src/runes/rune_id.rs b/src/runes/rune_id.rs deleted file mode 100644 index 2ba260207f..0000000000 --- a/src/runes/rune_id.rs +++ /dev/null @@ -1,132 +0,0 @@ -use {super::*, std::num::TryFromIntError}; - -#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq, Ord, PartialOrd)] -pub struct RuneId { - pub height: u32, - pub index: u16, -} - -impl TryFrom for RuneId { - type Error = TryFromIntError; - - fn try_from(n: u128) -> Result { - Ok(Self { - height: u32::try_from(n >> 16)?, - index: u16::try_from(n & 0xFFFF).unwrap(), - }) - } -} - -impl From for u128 { - fn from(id: RuneId) -> Self { - u128::from(id.height) << 16 | u128::from(id.index) - } -} - -impl Display for RuneId { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}:{}", self.height, self.index,) - } -} - -impl FromStr for RuneId { - type Err = Error; - - fn from_str(s: &str) -> Result { - let (height, index) = s - .split_once(':') - .ok_or_else(|| anyhow!("invalid rune ID: {s}"))?; - - Ok(Self { - height: height.parse()?, - index: index.parse()?, - }) - } -} - -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>, - { - DeserializeFromStr::with(deserializer) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rune_id_to_128() { - assert_eq!( - 0b11_0000_0000_0000_0001u128, - RuneId { - height: 3, - index: 1, - } - .into() - ); - } - - #[test] - fn display() { - assert_eq!( - RuneId { - height: 1, - index: 2 - } - .to_string(), - "1:2" - ); - } - - #[test] - fn from_str() { - assert!(":".parse::().is_err()); - assert!("1:".parse::().is_err()); - assert!(":2".parse::().is_err()); - assert!("a:2".parse::().is_err()); - assert!("1:a".parse::().is_err()); - assert_eq!( - "1:2".parse::().unwrap(), - RuneId { - height: 1, - index: 2 - } - ); - } - - #[test] - fn try_from() { - assert_eq!( - RuneId::try_from(0x060504030201).unwrap(), - RuneId { - height: 0x06050403, - index: 0x0201 - } - ); - - 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 deleted file mode 100644 index 3c4f289772..0000000000 --- a/src/runes/runestone.rs +++ /dev/null @@ -1,1678 +0,0 @@ -use super::*; - -const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; - -#[derive(Default, Serialize, Debug, PartialEq)] -pub struct Runestone { - pub burn: bool, - pub claim: Option, - pub default_output: Option, - pub edicts: Vec, - pub etching: Option, -} - -struct Message { - fields: HashMap, - edicts: Vec, -} - -impl Message { - fn from_integers(payload: &[u128]) -> Self { - let mut edicts = Vec::new(); - let mut fields = HashMap::new(); - - for i in (0..payload.len()).step_by(2) { - let tag = payload[i]; - - if Tag::Body == tag { - let mut id = 0u128; - for chunk in payload[i + 1..].chunks_exact(3) { - id = id.saturating_add(chunk[0]); - edicts.push(Edict { - id, - amount: chunk[1], - output: chunk[2], - }); - } - break; - } - - let Some(&value) = payload.get(i + 1) else { - break; - }; - - fields.entry(tag).or_insert(value); - } - - Self { fields, edicts } - } -} - -impl Runestone { - pub fn from_transaction(transaction: &Transaction) -> Option { - Self::decipher(transaction).ok().flatten() - } - - fn decipher(transaction: &Transaction) -> Result, script::Error> { - let Some(payload) = Runestone::payload(transaction)? else { - return Ok(None); - }; - - let integers = Runestone::integers(&payload); - - let Message { mut fields, edicts } = Message::from_integers(&integers); - - let claim = Tag::Claim.take(&mut fields); - - let deadline = Tag::Deadline - .take(&mut fields) - .and_then(|deadline| u32::try_from(deadline).ok()); - - let default_output = Tag::DefaultOutput - .take(&mut fields) - .and_then(|default| u32::try_from(default).ok()); - - let divisibility = Tag::Divisibility - .take(&mut fields) - .and_then(|divisibility| u8::try_from(divisibility).ok()) - .and_then(|divisibility| (divisibility <= MAX_DIVISIBILITY).then_some(divisibility)) - .unwrap_or_default(); - - let limit = Tag::Limit - .take(&mut fields) - .map(|limit| limit.min(MAX_LIMIT)); - - let rune = Tag::Rune.take(&mut fields).map(Rune); - - let spacers = Tag::Spacers - .take(&mut fields) - .and_then(|spacers| u32::try_from(spacers).ok()) - .and_then(|spacers| (spacers <= MAX_SPACERS).then_some(spacers)) - .unwrap_or_default(); - - let symbol = Tag::Symbol - .take(&mut fields) - .and_then(|symbol| u32::try_from(symbol).ok()) - .and_then(char::from_u32); - - let term = Tag::Term - .take(&mut fields) - .and_then(|term| u32::try_from(term).ok()); - - let mut flags = Tag::Flags.take(&mut fields).unwrap_or_default(); - - let etch = Flag::Etch.take(&mut flags); - - let mint = Flag::Mint.take(&mut flags); - - let etching = if etch { - Some(Etching { - divisibility, - rune, - spacers, - symbol, - mint: mint.then_some(Mint { - deadline, - limit, - term, - }), - }) - } else { - None - }; - - Ok(Some(Self { - burn: flags != 0 || fields.keys().any(|tag| tag % 2 == 0), - claim, - default_output, - edicts, - etching, - })) - } - - pub(crate) fn encipher(&self) -> ScriptBuf { - let mut payload = Vec::new(); - - if let Some(etching) = self.etching { - let mut flags = 0; - Flag::Etch.set(&mut flags); - - if etching.mint.is_some() { - Flag::Mint.set(&mut flags); - } - - Tag::Flags.encode(flags, &mut payload); - - if let Some(rune) = etching.rune { - Tag::Rune.encode(rune.0, &mut payload); - } - - if etching.divisibility != 0 { - Tag::Divisibility.encode(etching.divisibility.into(), &mut payload); - } - - if etching.spacers != 0 { - Tag::Spacers.encode(etching.spacers.into(), &mut payload); - } - - if let Some(symbol) = etching.symbol { - Tag::Symbol.encode(symbol.into(), &mut payload); - } - - if let Some(mint) = etching.mint { - if let Some(deadline) = mint.deadline { - Tag::Deadline.encode(deadline.into(), &mut payload); - } - - if let Some(limit) = mint.limit { - Tag::Limit.encode(limit, &mut payload); - } - - if let Some(term) = mint.term { - Tag::Term.encode(term.into(), &mut payload); - } - } - } - - if let Some(claim) = self.claim { - Tag::Claim.encode(claim, &mut payload); - } - - if let Some(default_output) = self.default_output { - Tag::DefaultOutput.encode(default_output.into(), &mut payload); - } - - if self.burn { - Tag::Burn.encode(0, &mut payload); - } - - if !self.edicts.is_empty() { - varint::encode_to_vec(Tag::Body.into(), &mut payload); - - let mut edicts = self.edicts.clone(); - edicts.sort_by_key(|edict| edict.id); - - let mut id = 0; - for edict in edicts { - varint::encode_to_vec(edict.id - id, &mut payload); - varint::encode_to_vec(edict.amount, &mut payload); - varint::encode_to_vec(edict.output, &mut payload); - id = edict.id; - } - } - - let mut builder = script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST"); - - for chunk in payload.chunks(MAX_SCRIPT_ELEMENT_SIZE) { - let push: &script::PushBytes = chunk.try_into().unwrap(); - builder = builder.push_slice(push); - } - - builder.into_script() - } - - fn payload(transaction: &Transaction) -> Result>, script::Error> { - for output in &transaction.output { - let mut instructions = output.script_pubkey.instructions(); - - if instructions.next().transpose()? != Some(Instruction::Op(opcodes::all::OP_RETURN)) { - continue; - } - - if instructions.next().transpose()? != Some(Instruction::PushBytes(b"RUNE_TEST".into())) { - continue; - } - - let mut payload = Vec::new(); - - for result in instructions { - if let Instruction::PushBytes(push) = result? { - payload.extend_from_slice(push.as_bytes()); - } - } - - return Ok(Some(payload)); - } - - Ok(None) - } - - fn integers(payload: &[u8]) -> Vec { - 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; - } - - integers - } -} - -#[cfg(test)] -mod tests { - use {super::*, bitcoin::script::PushBytes}; - - fn decipher(integers: &[u128]) -> Runestone { - let payload = payload(integers); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }) - .unwrap() - .unwrap() - } - - fn payload(integers: &[u128]) -> Vec { - let mut payload = Vec::new(); - - for integer in integers { - payload.extend(varint::encode(*integer)); - } - - payload - } - - #[test] - fn from_transaction_returns_none_if_decipher_returns_error() { - assert_eq!( - Runestone::from_transaction(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: ScriptBuf::from_bytes(vec![opcodes::all::OP_PUSHBYTES_4.to_u8()]), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }), - None - ); - } - - #[test] - fn deciphering_transaction_with_no_outputs_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: Vec::new(), - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_transaction_with_non_op_return_output_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new().push_slice([]).into_script(), - value: 0 - }], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_transaction_with_bare_op_return_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .into_script(), - value: 0 - }], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_transaction_with_non_matching_op_return_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"FOOO") - .into_script(), - value: 0 - }], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_valid_runestone_with_invalid_script_returns_script_error() { - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: ScriptBuf::from_bytes(vec![opcodes::all::OP_PUSHBYTES_4.to_u8()]), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }) - .unwrap_err(); - } - - #[test] - fn deciphering_valid_runestone_with_invalid_script_postfix_returns_script_error() { - let mut script_pubkey = script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .into_script() - .into_bytes(); - - script_pubkey.push(opcodes::all::OP_PUSHBYTES_4.to_u8()); - - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: ScriptBuf::from_bytes(script_pubkey), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }) - .unwrap_err(); - } - - #[test] - fn deciphering_runestone_with_truncated_varint_succeeds() { - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice([128]) - .into_script(), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }) - .unwrap(); - } - - #[test] - fn non_push_opcodes_in_runestone_are_ignored() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice([0, 1]) - .push_opcode(opcodes::all::OP_VERIFY) - .push_slice([2, 3]) - .into_script(), - value: 0, - }], - lock_time: LockTime::ZERO, - version: 2, - }) - .unwrap() - .unwrap(), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - }, - ); - } - - #[test] - fn deciphering_empty_runestone_is_successful() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .into_script(), - value: 0 - }], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(Some(Runestone::default())) - ); - } - - #[test] - fn error_in_input_aborts_search_for_runestone() { - let payload = payload(&[0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - let mut script_pubkey = Vec::new(); - script_pubkey.push(opcodes::all::OP_RETURN.to_u8()); - script_pubkey.push(opcodes::all::OP_PUSHBYTES_9.to_u8()); - script_pubkey.extend_from_slice(b"RUNE_TEST"); - script_pubkey.push(opcodes::all::OP_PUSHBYTES_4.to_u8()); - - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![ - TxOut { - script_pubkey: ScriptBuf::from_bytes(script_pubkey), - value: 0, - }, - TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0, - }, - ], - lock_time: LockTime::ZERO, - version: 2, - }) - .unwrap_err(); - } - - #[test] - fn deciphering_non_empty_runestone_is_successful() { - assert_eq!( - decipher(&[Tag::Body.into(), 1, 2, 3]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - } - ); - } - - #[test] - fn decipher_etching() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Body.into(), - 1, - 2, - 3 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching::default()), - ..Default::default() - } - ); - } - - #[test] - fn decipher_etching_with_rune() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Body.into(), - 1, - 2, - 3 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn etch_flag_is_required_to_etch_rune_even_if_mint_is_set() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Mint.mask(), - Tag::Term.into(), - 4, - Tag::Body.into(), - 1, - 2, - 3 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - }, - ); - } - - #[test] - fn decipher_etching_with_term() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), - Tag::Term.into(), - 4, - Tag::Body.into(), - 1, - 2, - 3 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - mint: Some(Mint { - term: Some(4), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn decipher_etching_with_limit() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), - Tag::Limit.into(), - 4, - Tag::Body.into(), - 1, - 2, - 3 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - mint: Some(Mint { - limit: Some(4), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn duplicate_tags_are_ignored() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Rune.into(), - 5, - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - ..Default::default() - }), - ..Default::default() - } - ); - } - - #[test] - fn unrecognized_odd_tag_is_ignored() { - assert_eq!( - decipher(&[Tag::Nop.into(), 100, Tag::Body.into(), 1, 2, 3]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - }, - ); - } - - #[test] - fn unrecognized_even_tag_is_burn() { - assert_eq!( - decipher(&[Tag::Burn.into(), 0, Tag::Body.into(), 1, 2, 3]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - burn: true, - ..Default::default() - }, - ); - } - - #[test] - fn unrecognized_flag_is_burn() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Burn.mask(), - Tag::Body.into(), - 1, - 2, - 3 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - burn: true, - ..Default::default() - }, - ); - } - - #[test] - fn tag_with_no_value_is_ignored() { - assert_eq!( - decipher(&[Tag::Flags.into(), 1, Tag::Flags.into()]), - Runestone { - etching: Some(Etching::default()), - ..Default::default() - }, - ); - } - - #[test] - fn additional_integers_in_body_are_ignored() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Body.into(), - 1, - 2, - 3, - 4, - 5 - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn decipher_etching_with_divisibility() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Divisibility.into(), - 5, - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - divisibility: 5, - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn divisibility_above_max_is_ignored() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Divisibility.into(), - (MAX_DIVISIBILITY + 1).into(), - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn symbol_above_max_is_ignored() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Symbol.into(), - u128::from(u32::from(char::MAX) + 1), - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching::default()), - ..Default::default() - }, - ); - } - - #[test] - fn decipher_etching_with_symbol() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Symbol.into(), - 'a'.into(), - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - symbol: Some('a'), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn decipher_etching_with_all_etching_tags() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), - Tag::Rune.into(), - 4, - Tag::Deadline.into(), - 7, - Tag::Divisibility.into(), - 1, - Tag::Spacers.into(), - 5, - Tag::Symbol.into(), - 'a'.into(), - Tag::Term.into(), - 2, - Tag::Limit.into(), - 3, - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - mint: Some(Mint { - deadline: Some(7), - term: Some(2), - limit: Some(3), - }), - divisibility: 1, - symbol: Some('a'), - spacers: 5, - }), - ..Default::default() - }, - ); - } - - #[test] - fn recognized_even_etching_fields_in_non_etching_are_ignored() { - assert_eq!( - decipher(&[ - Tag::Rune.into(), - 4, - Tag::Divisibility.into(), - 1, - Tag::Symbol.into(), - 'a'.into(), - Tag::Term.into(), - 2, - Tag::Limit.into(), - 3, - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - }, - ); - } - - #[test] - fn decipher_etching_with_divisibility_and_symbol() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Rune.into(), - 4, - Tag::Divisibility.into(), - 1, - Tag::Symbol.into(), - 'a'.into(), - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - rune: Some(Rune(4)), - divisibility: 1, - symbol: Some('a'), - ..Default::default() - }), - ..Default::default() - }, - ); - } - - #[test] - fn tag_values_are_not_parsed_as_tags() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Divisibility.into(), - Tag::Body.into(), - Tag::Body.into(), - 1, - 2, - 3, - ]), - Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching::default()), - ..Default::default() - }, - ); - } - - #[test] - fn runestone_may_contain_multiple_edicts() { - assert_eq!( - decipher(&[Tag::Body.into(), 1, 2, 3, 3, 5, 6]), - Runestone { - edicts: vec![ - Edict { - id: 1, - amount: 2, - output: 3, - }, - Edict { - id: 4, - amount: 5, - output: 6, - }, - ], - ..Default::default() - }, - ); - } - - #[test] - fn id_deltas_saturate_to_max() { - assert_eq!( - decipher(&[Tag::Body.into(), 1, 2, 3, u128::MAX, 5, 6]), - Runestone { - edicts: vec![ - Edict { - id: 1, - amount: 2, - output: 3, - }, - Edict { - id: u128::MAX, - amount: 5, - output: 6, - }, - ], - ..Default::default() - }, - ); - } - - #[test] - fn payload_pushes_are_concatenated() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice::<&PushBytes>( - varint::encode(Tag::Flags.into()) - .as_slice() - .try_into() - .unwrap() - ) - .push_slice::<&PushBytes>( - varint::encode(Flag::Etch.mask()) - .as_slice() - .try_into() - .unwrap() - ) - .push_slice::<&PushBytes>( - varint::encode(Tag::Divisibility.into()) - .as_slice() - .try_into() - .unwrap() - ) - .push_slice::<&PushBytes>(varint::encode(5).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>( - varint::encode(Tag::Body.into()) - .as_slice() - .try_into() - .unwrap() - ) - .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(3).as_slice().try_into().unwrap()) - .into_script(), - value: 0 - }], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(Some(Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - etching: Some(Etching { - divisibility: 5, - ..Default::default() - }), - ..Default::default() - })) - ); - } - - #[test] - fn runestone_may_be_in_second_output() { - let payload = payload(&[0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![ - TxOut { - script_pubkey: ScriptBuf::new(), - value: 0, - }, - TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - } - ], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(Some(Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - })) - ); - } - - #[test] - fn runestone_may_be_after_non_matching_op_return() { - let payload = payload(&[0, 1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![ - TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"FOO") - .into_script(), - value: 0, - }, - TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - } - ], - lock_time: LockTime::ZERO, - version: 2, - }), - Ok(Some(Runestone { - edicts: vec![Edict { - id: 1, - amount: 2, - output: 3, - }], - ..Default::default() - })) - ); - } - - #[test] - fn runestone_size() { - #[track_caller] - fn case(edicts: Vec, etching: Option, size: usize) { - assert_eq!( - Runestone { - edicts, - etching, - ..Default::default() - } - .encipher() - .len() - - 1 - - b"RUNE_TEST".len(), - size - ); - } - - case(Vec::new(), None, 1); - - case( - Vec::new(), - Some(Etching { - rune: Some(Rune(0)), - ..Default::default() - }), - 6, - ); - - case( - Vec::new(), - Some(Etching { - divisibility: MAX_DIVISIBILITY, - rune: Some(Rune(0)), - ..Default::default() - }), - 8, - ); - - case( - Vec::new(), - Some(Etching { - divisibility: MAX_DIVISIBILITY, - mint: Some(Mint { - deadline: Some(10000), - limit: Some(1), - term: Some(1), - }), - rune: Some(Rune(0)), - symbol: Some('$'), - spacers: 1, - }), - 19, - ); - - case( - Vec::new(), - Some(Etching { - rune: Some(Rune(u128::MAX)), - ..Default::default() - }), - 24, - ); - - case( - vec![Edict { - amount: 0, - id: RuneId { - height: 0, - index: 0, - } - .into(), - output: 0, - }], - Some(Etching { - divisibility: MAX_DIVISIBILITY, - rune: Some(Rune(u128::MAX)), - ..Default::default() - }), - 30, - ); - - case( - vec![Edict { - amount: u128::MAX, - id: RuneId { - height: 0, - index: 0, - } - .into(), - output: 0, - }], - Some(Etching { - divisibility: MAX_DIVISIBILITY, - rune: Some(Rune(u128::MAX)), - ..Default::default() - }), - 48, - ); - - case( - vec![Edict { - amount: 0, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }], - None, - 11, - ); - - case( - vec![Edict { - amount: u128::MAX, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }], - None, - 29, - ); - - case( - vec![ - Edict { - amount: u128::MAX, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }, - Edict { - amount: u128::MAX, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }, - ], - None, - 50, - ); - - case( - vec![ - Edict { - amount: u128::MAX, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }, - Edict { - amount: u128::MAX, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }, - Edict { - amount: u128::MAX, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }, - ], - None, - 71, - ); - - case( - vec![ - Edict { - amount: u64::MAX.into(), - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }; - 4 - ], - None, - 56, - ); - - case( - vec![ - Edict { - amount: u64::MAX.into(), - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }; - 5 - ], - None, - 68, - ); - - case( - vec![ - Edict { - amount: u64::MAX.into(), - id: RuneId { - height: 0, - index: u16::MAX, - } - .into(), - output: 0, - }; - 5 - ], - None, - 65, - ); - - case( - vec![ - Edict { - amount: 1_000_000_000_000_000_000, - id: RuneId { - height: 1_000_000, - index: u16::MAX, - } - .into(), - output: 0, - }; - 5 - ], - None, - 63, - ); - } - - #[test] - fn etching_with_term_greater_than_maximum_is_ignored() { - assert_eq!( - decipher(&[ - Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Term.into(), - u128::from(u64::MAX) + 1, - ]), - Runestone { - etching: Some(Etching::default()), - ..Default::default() - }, - ); - } - - #[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::ZERO, - version: 2, - }; - - let payload = Runestone::payload(&transaction).unwrap().unwrap(); - - assert_eq!(Runestone::integers(&payload), 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, - mint: Some(Mint { - deadline: Some(2), - limit: Some(3), - term: Some(5), - }), - symbol: Some('@'), - rune: Some(Rune(4)), - spacers: 6, - }), - edicts: vec![ - Edict { - amount: 8, - id: 9, - output: 10, - }, - Edict { - amount: 5, - id: 6, - output: 7, - }, - ], - default_output: Some(11), - burn: true, - claim: Some(12), - }, - &[ - Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), - Tag::Rune.into(), - 4, - Tag::Divisibility.into(), - 1, - Tag::Spacers.into(), - 6, - Tag::Symbol.into(), - '@'.into(), - Tag::Deadline.into(), - 2, - Tag::Limit.into(), - 3, - Tag::Term.into(), - 5, - Tag::Claim.into(), - 12, - Tag::DefaultOutput.into(), - 11, - Tag::Burn.into(), - 0, - Tag::Body.into(), - 6, - 5, - 7, - 3, - 8, - 10, - ], - ); - - case( - Runestone { - etching: Some(Etching { - divisibility: 0, - mint: None, - symbol: None, - rune: Some(Rune(3)), - spacers: 0, - }), - burn: false, - ..Default::default() - }, - &[Tag::Flags.into(), Flag::Etch.mask(), Tag::Rune.into(), 3], - ); - - case( - Runestone { - etching: Some(Etching { - divisibility: 0, - mint: None, - symbol: None, - rune: None, - spacers: 0, - }), - burn: false, - ..Default::default() - }, - &[Tag::Flags.into(), Flag::Etch.mask()], - ); - - case( - Runestone { - burn: true, - ..Default::default() - }, - &[Tag::Burn.into(), 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); - } - - #[test] - fn max_spacers() { - let mut rune = String::new(); - - for (i, c) in Rune(u128::MAX).to_string().chars().enumerate() { - if i > 0 { - rune.push('•'); - } - - rune.push(c); - } - - assert_eq!(MAX_SPACERS, rune.parse::().unwrap().spacers); - } -} diff --git a/src/runes/tag.rs b/src/runes/tag.rs deleted file mode 100644 index 9cb95bd9a5..0000000000 --- a/src/runes/tag.rs +++ /dev/null @@ -1,96 +0,0 @@ -use super::*; - -#[derive(Copy, Clone, Debug)] -pub(super) enum Tag { - Body = 0, - Flags = 2, - Rune = 4, - Limit = 6, - Term = 8, - Deadline = 10, - DefaultOutput = 12, - Claim = 14, - #[allow(unused)] - Burn = 126, - - Divisibility = 1, - Spacers = 3, - Symbol = 5, - #[allow(unused)] - Nop = 127, -} - -impl Tag { - pub(super) fn take(self, fields: &mut HashMap) -> Option { - fields.remove(&self.into()) - } - - pub(super) fn encode(self, value: u128, payload: &mut Vec) { - varint::encode_to_vec(self.into(), payload); - varint::encode_to_vec(value, payload); - } -} - -impl From for u128 { - fn from(tag: Tag) -> Self { - tag as u128 - } -} - -impl PartialEq for Tag { - fn eq(&self, other: &u128) -> bool { - u128::from(*self) == *other - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn from_u128() { - assert_eq!(0u128, Tag::Body.into()); - assert_eq!(2u128, Tag::Flags.into()); - } - - #[test] - fn partial_eq() { - assert_eq!(Tag::Body, 0); - assert_eq!(Tag::Flags, 2); - } - - #[test] - fn take() { - let mut fields = vec![(2, 3)].into_iter().collect::>(); - - assert_eq!(Tag::Flags.take(&mut fields), Some(3)); - - assert!(fields.is_empty()); - - assert_eq!(Tag::Flags.take(&mut fields), None); - } - - #[test] - fn encode() { - let mut payload = Vec::new(); - - Tag::Flags.encode(3, &mut payload); - - assert_eq!(payload, [2, 3]); - - Tag::Rune.encode(5, &mut payload); - - assert_eq!(payload, [2, 3, 4, 5]); - } - - #[test] - fn burn_and_nop_are_one_byte() { - let mut payload = Vec::new(); - Tag::Burn.encode(0, &mut payload); - assert_eq!(payload.len(), 2); - - let mut payload = Vec::new(); - Tag::Nop.encode(0, &mut payload); - assert_eq!(payload.len(), 2); - } -} diff --git a/src/runes/varint.rs b/src/runes/varint.rs deleted file mode 100644 index 438673a836..0000000000 --- a/src/runes/varint.rs +++ /dev/null @@ -1,144 +0,0 @@ -#[cfg(test)] -pub fn encode(n: u128) -> Vec { - let mut v = Vec::new(); - encode_to_vec(n, &mut v); - v -} - -pub fn encode_to_vec(mut n: u128, v: &mut Vec) { - let mut out = [0; 19]; - let mut i = 18; - - out[i] = n.to_le_bytes()[0] & 0b0111_1111; - - while n > 0b0111_1111 { - n = n / 128 - 1; - i -= 1; - out[i] = n.to_le_bytes()[0] | 0b1000_0000; - } - - v.extend_from_slice(&out[i..]); -} - -pub fn decode(buffer: &[u8]) -> (u128, usize) { - let mut n = 0; - let mut i = 0; - - loop { - let b = match buffer.get(i) { - Some(b) => u128::from(*b), - None => return (n, i), - }; - - n = n.saturating_mul(128); - - if b < 128 { - return (n.saturating_add(b), i + 1); - } - - n = n.saturating_add(b - 127); - - i += 1; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn u128_max_round_trips_successfully() { - let n = u128::MAX; - let encoded = encode(n); - let (decoded, length) = decode(&encoded); - assert_eq!(decoded, n); - assert_eq!(length, encoded.len()); - } - - #[test] - fn powers_of_two_round_trip_successfully() { - for i in 0..128 { - let n = 1 << i; - let encoded = encode(n); - let (decoded, length) = decode(&encoded); - assert_eq!(decoded, n); - assert_eq!(length, encoded.len()); - } - } - - #[test] - fn alternating_bit_strings_round_trip_successfully() { - let mut n = 0; - - for i in 0..129 { - n = n << 1 | (i % 2); - let encoded = encode(n); - let (decoded, length) = decode(&encoded); - assert_eq!(decoded, n); - assert_eq!(length, encoded.len()); - } - } - - #[test] - fn large_varints_saturate_to_maximum() { - assert_eq!( - decode(&[ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 255, - 0, - ]), - (u128::MAX, 19) - ); - } - - #[test] - fn truncated_varints_with_large_final_byte_saturate_to_maximum() { - assert_eq!( - decode(&[ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 255, - 255, - ]), - (u128::MAX, 19) - ); - } - - #[test] - fn varints_with_large_final_byte_saturate_to_maximum() { - assert_eq!( - decode(&[ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 255, - 127, - ]), - (u128::MAX, 19) - ); - } - - #[test] - fn taproot_annex_format_bip_test_vectors_round_trip_successfully() { - const TEST_VECTORS: &[(u128, &[u8])] = &[ - (0, &[0x00]), - (1, &[0x01]), - (127, &[0x7F]), - (128, &[0x80, 0x00]), - (255, &[0x80, 0x7F]), - (256, &[0x81, 0x00]), - (16383, &[0xFE, 0x7F]), - (16384, &[0xFF, 0x00]), - (16511, &[0xFF, 0x7F]), - (65535, &[0x82, 0xFE, 0x7F]), - (1 << 32, &[0x8E, 0xFE, 0xFE, 0xFF, 0x00]), - ]; - - for (n, encoding) in TEST_VECTORS { - let actual = encode(*n); - assert_eq!(actual, *encoding); - let (actual, length) = decode(encoding); - assert_eq!(actual, *n); - assert_eq!(length, encoding.len()); - } - } - - #[test] - fn varints_may_be_truncated() { - assert_eq!(decode(&[128]), (1, 1)); - } -} diff --git a/src/server_config.rs b/src/server_config.rs deleted file mode 100644 index 57cd3e0dd1..0000000000 --- a/src/server_config.rs +++ /dev/null @@ -1,12 +0,0 @@ -use super::*; - -#[derive(Default)] -pub(crate) struct ServerConfig { - pub(crate) chain: Chain, - pub(crate) content_proxy: Option, - pub(crate) csp_origin: Option, - pub(crate) decompress: bool, - pub(crate) domain: Option, - pub(crate) index_sats: bool, - pub(crate) json_api_enabled: bool, -} diff --git a/src/settings.rs b/src/settings.rs index 34d8e41309..a0a487002a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -73,7 +73,7 @@ impl Settings { }; let config = if let Some(config_path) = config_path { - serde_yaml::from_reader(File::open(&config_path).context(anyhow!( + serde_yaml::from_reader(fs::File::open(&config_path).context(anyhow!( "failed to open config file `{}`", config_path.display() ))?) @@ -589,7 +589,7 @@ mod tests { Settings::merge( Options { bitcoin_rpc_username: Some("foo".into()), - ..Default::default() + ..default() }, Default::default(), ) @@ -605,7 +605,7 @@ mod tests { Settings::merge( Options { bitcoin_rpc_password: Some("foo".into()), - ..Default::default() + ..default() }, Default::default(), ) @@ -649,15 +649,13 @@ mod tests { #[test] fn rpc_server_chain_must_match() { - let rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Testnet) - .build(); + let core = mockcore::builder().network(Network::Testnet).build(); let settings = parse(&[ "--cookie-file", - rpc_server.cookie_file().to_str().unwrap(), + core.cookie_file().to_str().unwrap(), "--bitcoin-rpc-url", - &rpc_server.url(), + &core.url(), ]); assert_eq!( @@ -780,7 +778,7 @@ mod tests { #[test] fn network_is_joined_with_data_dir() { - let data_dir = parse(&["--chain=signet", "--data-dir=foo"]) + let data_dir = parse(&["--chain=signet", "--datadir=foo"]) .data_dir() .display() .to_string(); @@ -899,7 +897,7 @@ mod tests { let config = Settings { bitcoin_rpc_username: Some("config_user".into()), bitcoin_rpc_password: Some("config_pass".into()), - ..Default::default() + ..default() }; let tempdir = TempDir::new().unwrap(); @@ -914,7 +912,7 @@ mod tests { bitcoin_rpc_username: Some("option_user".into()), bitcoin_rpc_password: Some("option_pass".into()), config: Some(config_path.clone()), - ..Default::default() + ..default() }, vec![ ("BITCOIN_RPC_USERNAME".into(), "env_user".into()), @@ -933,7 +931,7 @@ mod tests { Settings::merge( Options { config: Some(config_path.clone()), - ..Default::default() + ..default() }, vec![ ("BITCOIN_RPC_USERNAME".into(), "env_user".into()), @@ -952,7 +950,7 @@ mod tests { Settings::merge( Options { config: Some(config_path), - ..Default::default() + ..default() }, Default::default(), ) @@ -973,7 +971,7 @@ mod tests { #[test] fn example_config_file_is_valid() { - let _: Settings = serde_yaml::from_reader(File::open("ord.yaml").unwrap()).unwrap(); + let _: Settings = serde_yaml::from_reader(fs::File::open("ord.yaml").unwrap()).unwrap(); } #[test] @@ -1065,7 +1063,7 @@ mod tests { "--config=config", "--config-dir=config dir", "--cookie-file=cookie file", - "--data-dir=/data/dir", + "--datadir=/data/dir", "--first-inscription-height=2", "--height-limit=3", "--index-cache-size=4", @@ -1119,7 +1117,7 @@ mod tests { let config = Settings { index: Some("config".into()), - ..Default::default() + ..default() }; let tempdir = TempDir::new().unwrap(); diff --git a/src/subcommand/balances.rs b/src/subcommand/balances.rs index 8643c926ac..b7550a4eac 100644 --- a/src/subcommand/balances.rs +++ b/src/subcommand/balances.rs @@ -2,7 +2,7 @@ use super::*; #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Output { - pub runes: BTreeMap>, + pub runes: BTreeMap>, } pub(crate) fn run(settings: Settings) -> SubcommandResult { diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index f0562a126a..a9480f79c4 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -3,32 +3,29 @@ use super::*; #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] pub struct CompactOutput { pub inscriptions: Vec, + pub runestone: Option, } #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] pub struct RawOutput { pub inscriptions: Vec, + pub runestone: Option, } #[derive(Serialize, Eq, PartialEq, Deserialize, Debug)] +#[serde_with::skip_serializing_none] pub struct CompactInscription { - #[serde(default, skip_serializing_if = "Option::is_none")] pub body: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub content_encoding: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub content_type: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub duplicate_field: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub incomplete_field: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] pub metadata: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub metaprotocol: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub parents: Vec, pub pointer: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub unrecognized_even_field: bool, @@ -45,7 +42,7 @@ impl TryFrom for CompactInscription { .transpose()?, content_type: inscription.content_type().map(str::to_string), metaprotocol: inscription.metaprotocol().map(str::to_string), - parent: inscription.parent(), + parents: inscription.parents(), pointer: inscription.pointer(), body: inscription.body.map(hex::encode), duplicate_field: inscription.duplicate_field, @@ -80,13 +77,15 @@ impl Decode { .bitcoin_rpc_client(None)? .get_raw_transaction(&txid, None)? } else if let Some(file) = self.file { - Transaction::consensus_decode(&mut File::open(file)?)? + Transaction::consensus_decode(&mut fs::File::open(file)?)? } else { Transaction::consensus_decode(&mut io::stdin())? }; let inscriptions = ParsedEnvelope::from_transaction(&transaction); + let runestone = Runestone::decipher(&transaction); + if self.compact { Ok(Some(Box::new(CompactOutput { inscriptions: inscriptions @@ -94,9 +93,13 @@ impl Decode { .into_iter() .map(|inscription| inscription.payload.try_into()) .collect::>>()?, + runestone, }))) } else { - Ok(Some(Box::new(RawOutput { inscriptions }))) + Ok(Some(Box::new(RawOutput { + inscriptions, + runestone, + }))) } } } diff --git a/src/subcommand/env.rs b/src/subcommand/env.rs index 293a11295e..9e2a4058f3 100644 --- a/src/subcommand/env.rs +++ b/src/subcommand/env.rs @@ -91,7 +91,7 @@ rpcport={bitcoind_port} let _ord = KillOnDrop( Command::new(&ord) - .arg("--data-dir") + .arg("--datadir") .arg(&absolute) .arg("server") .arg("--polling-interval=100ms") @@ -104,7 +104,7 @@ rpcport={bitcoind_port} if !absolute.join("regtest/wallets/ord").try_exists()? { let status = Command::new(&ord) - .arg("--data-dir") + .arg("--datadir") .arg(&absolute) .arg("wallet") .arg("create") @@ -113,7 +113,7 @@ rpcport={bitcoind_port} ensure!(status.success(), "failed to create wallet: {status}"); let output = Command::new(&ord) - .arg("--data-dir") + .arg("--datadir") .arg(&absolute) .arg("wallet") .arg("receive") @@ -124,43 +124,57 @@ rpcport={bitcoind_port} "failed to generate receive address: {status}" ); - let receive = - serde_json::from_slice::(&output.stdout)?; - - let address = receive.address.require_network(Network::Regtest)?; + let receive = serde_json::from_slice::(&output.stdout)?; let status = Command::new("bitcoin-cli") .arg(format!("-datadir={relative}")) .arg("generatetoaddress") .arg("200") - .arg(address.to_string()) + .arg( + receive + .addresses + .first() + .cloned() + .unwrap() + .require_network(Network::Regtest)? + .to_string(), + ) .status()?; ensure!(status.success(), "failed to create wallet: {status}"); } serde_json::to_writer_pretty( - File::create(self.directory.join("env.json"))?, + fs::File::create(self.directory.join("env.json"))?, &Info { bitcoind_port, ord_port, bitcoin_cli_command: vec!["bitcoin-cli".into(), format!("-datadir={relative}")], ord_wallet_command: vec![ ord.to_str().unwrap().into(), - "--data-dir".into(), + "--datadir".into(), absolute.to_str().unwrap().into(), "wallet".into(), ], }, )?; + let datadir = if relative + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') + { + relative + } else { + format!("'{relative}'") + }; + eprintln!( "{} {server_url} {} -bitcoin-cli -datadir='{relative}' getblockchaininfo +bitcoin-cli -datadir={datadir} getblockchaininfo {} -{} --data-dir '{relative}' wallet balance", +{} --datadir {datadir} wallet balance", "`ord` server URL:".blue().bold(), "Example `bitcoin-cli` command:".blue().bold(), "Example `ord` command:".blue().bold(), diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index 602710f656..7715cfefd0 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -32,7 +32,10 @@ impl Find { match self.end { Some(end) => match index.find_range(self.sat, end)? { - Some(result) => Ok(Some(Box::new(result))), + Some(mut results) => { + results.sort_by_key(|find_range_output| find_range_output.start); + Ok(Some(Box::new(results))) + } None => Err(anyhow!("range has not been mined as of index height")), }, None => match index.find(self.sat)? { diff --git a/src/subcommand/runes.rs b/src/subcommand/runes.rs index dd99e79eab..645cce331c 100644 --- a/src/subcommand/runes.rs +++ b/src/subcommand/runes.rs @@ -7,20 +7,20 @@ pub struct Output { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct RuneInfo { + pub block: u64, pub burned: u128, pub divisibility: u8, pub etching: Txid, - pub height: u32, pub id: RuneId, - pub index: u16, - pub mint: Option, - pub mints: u64, + pub mints: u128, pub number: u64, - pub rune: Rune, - pub spacers: u32, + pub premine: u128, + pub rune: SpacedRune, pub supply: u128, pub symbol: Option, + pub terms: Option, pub timestamp: DateTime, + pub tx: u32, } pub(crate) fn run(settings: Settings) -> SubcommandResult { @@ -40,37 +40,37 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { .map( |( id, - RuneEntry { + entry @ RuneEntry { + block, burned, divisibility, etching, - mint, mints, number, - rune, - spacers, - supply, + premine, + spaced_rune, symbol, + terms, timestamp, }, )| { ( - rune, + spaced_rune.rune, RuneInfo { + block, burned, divisibility, etching, - height: id.height, id, - index: id.index, - mint, mints, number, - rune, - spacers, - supply, + premine, + rune: spaced_rune, + supply: entry.supply(), symbol, + terms, timestamp: crate::timestamp(timestamp), + tx: id.tx, }, ) }, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index e269845dc4..0b1155427d 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -5,22 +5,21 @@ use { error::{OptionExt, ServerError, ServerResult}, }, super::*, - crate::{ - ordzaar::inscriptions::{InscriptionData, InscriptionIds}, - ordzaar::ordinals::get_ordinals, - server_config::ServerConfig, - templates::{ - BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, InputHtml, - InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml, - PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, - PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, - RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, TransactionHtml, - }, + // ---- Ordzaar ---- + crate::ordzaar, + // ---- Ordzaar ---- + crate::templates::{ + BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, InputHtml, + InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml, + ParentsHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, + PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, + PreviewVideoHtml, RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, + TransactionHtml, }, axum::{ body, extract::{Extension, Json, Path, Query}, - http::{header, HeaderMap, HeaderValue, StatusCode, Uri}, + http::{header, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Redirect, Response}, routing::{get, post}, Router, @@ -34,7 +33,7 @@ use { caches::DirCache, AcmeConfig, }, - std::{cmp::Ordering, io::Read, str, sync::Arc}, + std::{cmp::Ordering, str, sync::Arc}, tokio_stream::StreamExt, tower_http::{ compression::CompressionLayer, @@ -44,10 +43,13 @@ use { }, }; +pub(crate) use server_config::ServerConfig; + mod accept_encoding; mod accept_json; mod error; pub(crate) mod query; +mod server_config; enum SpawnConfig { Https(AxumAcceptor), @@ -217,6 +219,11 @@ impl Server { .route("/install.sh", get(Self::install_script)) .route("/ordinal/:sat", get(Self::ordinal)) .route("/output/:output", get(Self::output)) + .route("/parents/:inscription_id", get(Self::parents)) + .route( + "/parents/:inscription_id/:page", + get(Self::parents_paginated), + ) .route("/preview/:inscription_id", get(Self::preview)) .route("/r/blockhash", get(Self::block_hash_json)) .route( @@ -256,10 +263,20 @@ impl Server { .route("/static/*path", get(Self::static_asset)) .route("/status", get(Self::status)) .route("/tx/:txid", get(Self::transaction)) + .route("/update", get(Self::update)) // ---- Ordzaar routes ---- + // Deprecated: Ordzaar custom routes should use"ordzar" prefix/path + // to prevent duplication with the Ord server paths. .route("/inscriptions", post(Self::ordzaar_inscriptions_from_ids)) .route("/ordinals/:outpoint", get(Self::ordzaar_ordinals_from_outpoint)) + + .route("/ordzaar/inscriptions", post(Self::ordzaar_inscriptions_from_ids)) + .route("/ordzaar/ordinals/:outpoint", get(Self::ordzaar_ordinals_from_outpoint)) + .route("/ordzaar/rune/:rune", get(Self::ordzaar_rune_detail)) + .route("/ordzaar/rune/output/:outpoint", get(Self::ordzaar_rune_output)) + .route("/ordzaar/rune/outputs/bulk", get(Self::ordzaar_rune_output_bulk)) // ---- Ordzaar routes ---- + .fallback(Self::fallback) .layer(Extension(index)) .layer(Extension(server_config.clone())) .layer(Extension(settings.clone())) @@ -350,14 +367,14 @@ impl Server { .into_iter() .nth(outpoint.vout as usize) .ok_or_not_found(|| format!("output {outpoint}"))?; - Ok(Json(get_ordinals(&index, outpoint)?).into_response()) + Ok(Json(ordzaar::ordinals::get_ordinals(&index, outpoint)?).into_response()) } async fn ordzaar_inscriptions_from_ids( Extension(index): Extension>, - Json(payload): Json, + Json(payload): Json, ) -> ServerResult { - let mut inscriptions: Vec = Vec::new(); + let mut inscriptions: Vec = Vec::new(); for id in payload.ids { let entry = match index.get_inscription_entry(id)? { Some(entry) => entry, @@ -369,7 +386,7 @@ impl Server { None => continue, }; - inscriptions.push(InscriptionData::new( + inscriptions.push(ordzaar::inscriptions::InscriptionData::new( entry.fee, entry.height, id, @@ -377,11 +394,94 @@ impl Server { entry.sequence_number, entry.sat, satpoint, - timestamp(entry.timestamp), + timestamp(entry.timestamp.into()), )); } Ok(Json(inscriptions).into_response()) } + + async fn ordzaar_rune_detail( + Extension(index): Extension>, + Path(DeserializeFromStr(rune_query)): Path>, + ) -> ServerResult { + task::block_in_place(|| { + if !index.has_rune_index() { + return Err(ServerError::NotFound( + "this server has no rune index".to_string(), + )); + } + + let rune = match rune_query { + query::Rune::SpacedRune(spaced_rune) => spaced_rune.rune, + query::Rune::RuneId(rune_id) => index + .get_rune_by_id(rune_id)? + .ok_or_not_found(|| format!("rune {rune_id}"))?, + }; + + let (id, entry, _) = index + .rune(rune)? + .ok_or_not_found(|| format!("rune {rune}"))?; + + let block_height = index.block_height()?.unwrap_or(Height(0)); + let mintable = entry.mintable((block_height.n() + 1).into()).is_ok(); + + Ok(Json(ordzaar::runes::RuneDetail::from_rune(id, entry, mintable)).into_response()) + }) + } + + async fn ordzaar_rune_output( + Extension(index): Extension>, + Path(outpoint): Path, + ) -> ServerResult { + task::block_in_place(|| { + if !index.has_rune_index() { + return Err(ServerError::NotFound( + "this server has no rune index".to_string(), + )); + } + let runes = index.get_rune_balances_for_outpoint(outpoint)?; + + let response: Vec = runes + .into_iter() + .map(|rune| ordzaar::runes::RuneOutpoint::from_spaced_rune_pile(rune)) + .collect(); + + Ok(Json(response).into_response()) + }) + } + + async fn ordzaar_rune_output_bulk( + Extension(index): Extension>, + Query(query): Query, + ) -> ServerResult { + task::block_in_place(|| { + if !index.has_rune_index() { + return Err(ServerError::NotFound( + "this server has no rune index".to_string(), + )); + } + + let mut results = HashMap::new(); + for outpoint_str in ordzaar::runes::str_coma_to_array(&query.outpoints) { + let outpoint_result = OutPoint::from_str(&outpoint_str); + if !outpoint_result.is_ok() { + return Err(ServerError::BadRequest(format!( + "error parsing outpoint: {outpoint_str}" + ))); + } + let runes = index.get_rune_balances_for_outpoint(outpoint_result.unwrap())?; + + let response: Vec = runes + .into_iter() + .map(|rune| ordzaar::runes::RuneOutpoint::from_spaced_rune_pile(rune)) + .collect(); + + results.insert(outpoint_str, response); + } + + Ok(Json(results).into_response()) + }) + } // ---- Ordzaar methods ---- fn spawn( @@ -520,7 +620,7 @@ impl Server { index.block_height()?.ok_or_not_found(|| "genesis block") } - async fn clock(Extension(index): Extension>) -> ServerResult { + async fn clock(Extension(index): Extension>) -> ServerResult { task::block_in_place(|| { Ok( ( @@ -535,12 +635,37 @@ impl Server { }) } + async fn fallback(Extension(index): Extension>, uri: Uri) -> ServerResult { + task::block_in_place(|| { + let path = urlencoding::decode(uri.path().trim_matches('/')) + .map_err(|err| ServerError::BadRequest(err.to_string()))?; + + let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) { + "inscription" + } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) { + "rune" + } else if re::OUTPOINT.is_match(&path) { + "output" + } else if re::HASH.is_match(&path) { + if index.block_header(path.parse().unwrap())?.is_some() { + "block" + } else { + "tx" + } + } else { + return Ok(StatusCode::NOT_FOUND.into_response()); + }; + + Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response()) + }) + } + async fn sat( Extension(server_config): Extension>, Extension(index): Extension>, Path(DeserializeFromStr(sat)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let inscriptions = index.get_inscription_ids_by_sat(sat)?; let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| { @@ -552,6 +677,9 @@ impl Server { }) }); let blocktime = index.block_time(sat.height())?; + + let charms = sat.charms(); + Ok(if accept_json { Json(api::Sat { number: sat.0, @@ -568,6 +696,7 @@ impl Server { satpoint, timestamp: blocktime.timestamp().timestamp(), inscriptions, + charms: Charm::charms(charms), }) .into_response() } else { @@ -592,7 +721,7 @@ impl Server { Extension(index): Extension>, Path(outpoint): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let sat_ranges = index.list(outpoint)?; @@ -684,7 +813,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(rune_query)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if !index.has_rune_index() { return Err(ServerError::NotFound( @@ -703,12 +832,27 @@ impl Server { .rune(rune)? .ok_or_not_found(|| format!("rune {rune}"))?; + let block_height = index.block_height()?.unwrap_or(Height(0)); + + let mintable = entry.mintable((block_height.n() + 1).into()).is_ok(); + Ok(if accept_json { - Json(api::Rune { entry, id, parent }).into_response() + Json(api::Rune { + entry, + id, + mintable, + parent, + }) + .into_response() } else { - RuneHtml { entry, id, parent } - .page(server_config) - .into_response() + RuneHtml { + entry, + id, + mintable, + parent, + } + .page(server_config) + .into_response() }) }) } @@ -717,7 +861,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { Ok(if accept_json { Json(api::Runes { @@ -738,11 +882,25 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let balances = index.get_rune_balance_map()?; Ok(if accept_json { - Json(balances).into_response() + Json( + balances + .into_iter() + .map(|(rune, balances)| { + ( + rune, + balances + .into_iter() + .map(|(outpoint, pile)| (outpoint, pile.amount)) + .collect(), + ) + }) + .collect::>>(), + ) + .into_response() } else { RuneBalancesHtml { balances } .page(server_config) @@ -769,7 +927,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let blocks = index.blocks(100)?; let mut featured_blocks = BTreeMap::new(); @@ -799,7 +957,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let (block, height) = match query { query::Block::Height(height) => { @@ -852,7 +1010,7 @@ impl Server { Extension(index): Extension>, Path(txid): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let transaction = index .get_transaction(txid)? @@ -883,6 +1041,20 @@ impl Server { }) } + async fn update( + Extension(settings): Extension>, + Extension(index): Extension>, + ) -> ServerResult { + task::block_in_place(|| { + if settings.integration_test() { + index.update()?; + Ok(index.block_count()?.to_string().into_response()) + } else { + Ok(StatusCode::NOT_FOUND.into_response()) + } + }) + } + async fn metadata( Extension(index): Extension>, Path(inscription_id): Path, @@ -901,7 +1073,7 @@ impl Server { async fn inscription_recursive( Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let inscription = index .get_inscription_by_id(inscription_id)? @@ -936,11 +1108,7 @@ impl Server { Ok( Json(api::InscriptionRecursive { - charms: Charm::ALL - .iter() - .filter(|charm| charm.is_set(entry.charms)) - .map(|charm| charm.title().into()) - .collect(), + charms: Charm::charms(entry.charms), content_type: inscription.content_type().map(|s| s.to_string()), content_length: inscription.content_length(), fee: entry.fee, @@ -951,7 +1119,7 @@ impl Server { value: output.as_ref().map(|o| o.value), sat: entry.sat, satpoint, - timestamp: timestamp(entry.timestamp).timestamp(), + timestamp: timestamp(entry.timestamp.into()).timestamp(), }) .into_response(), ) @@ -962,7 +1130,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { Ok(if accept_json { Json(index.status()?).into_response() @@ -992,29 +1160,21 @@ impl Server { async fn search_inner(index: Arc, query: String) -> ServerResult { task::block_in_place(|| { - lazy_static! { - static ref HASH: Regex = Regex::new(r"^[[:xdigit:]]{64}$").unwrap(); - static ref INSCRIPTION_ID: Regex = Regex::new(r"^[[:xdigit:]]{64}i\d+$").unwrap(); - static ref OUTPOINT: Regex = Regex::new(r"^[[:xdigit:]]{64}:\d+$").unwrap(); - static ref RUNE: Regex = Regex::new(r"^[A-Z•.]+$").unwrap(); - static ref RUNE_ID: Regex = Regex::new(r"^[0-9]+:[0-9]+$").unwrap(); - } - let query = query.trim(); - if HASH.is_match(query) { + if re::HASH.is_match(query) { if index.block_header(query.parse().unwrap())?.is_some() { Ok(Redirect::to(&format!("/block/{query}"))) } else { Ok(Redirect::to(&format!("/tx/{query}"))) } - } else if OUTPOINT.is_match(query) { + } else if re::OUTPOINT.is_match(query) { Ok(Redirect::to(&format!("/output/{query}"))) - } else if INSCRIPTION_ID.is_match(query) { + } else if re::INSCRIPTION_ID.is_match(query) || re::INSCRIPTION_NUMBER.is_match(query) { Ok(Redirect::to(&format!("/inscription/{query}"))) - } else if RUNE.is_match(query) { + } else if re::SPACED_RUNE.is_match(query) { Ok(Redirect::to(&format!("/rune/{query}"))) - } else if RUNE_ID.is_match(query) { + } else if re::RUNE_ID.is_match(query) { let id = query .parse::() .map_err(|err| ServerError::BadRequest(err.to_string()))?; @@ -1028,7 +1188,7 @@ impl Server { }) } - async fn favicon() -> ServerResult { + async fn favicon() -> ServerResult { Ok( Self::static_asset(Path("/favicon.png".to_string())) .await @@ -1039,7 +1199,7 @@ impl Server { async fn feed( Extension(server_config): Extension>, Extension(index): Extension>, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let mut builder = rss::ChannelBuilder::default(); @@ -1080,7 +1240,7 @@ impl Server { }) } - async fn static_asset(Path(path): Path) -> ServerResult { + async fn static_asset(Path(path): Path) -> ServerResult { let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') { stripped } else { @@ -1300,7 +1460,7 @@ impl Server { Extension(server_config): Extension>, Path(inscription_id): Path, accept_encoding: AcceptEncoding, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if settings.is_hidden(inscription_id) { return Ok(PreviewUnknownHtml.into_response()); @@ -1407,7 +1567,7 @@ impl Server { Extension(server_config): Extension>, Path(inscription_id): Path, accept_encoding: AcceptEncoding, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if settings.is_hidden(inscription_id) { return Ok(PreviewUnknownHtml.into_response()); @@ -1423,14 +1583,25 @@ impl Server { .ok_or_not_found(|| format!("delegate {inscription_id}"))? } - match inscription.media() { - Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()), + let media = inscription.media(); + + if let Media::Iframe = media { + return Ok( + Self::content_response(inscription, accept_encoding, &server_config)? + .ok_or_not_found(|| format!("inscription {inscription_id} content"))? + .into_response(), + ); + } + + let content_security_policy = server_config.preview_content_security_policy(media)?; + + match media { + Media::Audio => { + Ok((content_security_policy, PreviewAudioHtml { inscription_id }).into_response()) + } Media::Code(language) => Ok( ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://cdn.jsdelivr.net", - )], + content_security_policy, PreviewCodeHtml { inscription_id, language, @@ -1438,27 +1609,13 @@ impl Server { ) .into_response(), ), - Media::Font => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self'; style-src 'self' 'unsafe-inline';", - )], - PreviewFontHtml { inscription_id }, - ) - .into_response(), - ), - Media::Iframe => Ok( - Self::content_response(inscription, accept_encoding, &server_config)? - .ok_or_not_found(|| format!("inscription {inscription_id} content"))? - .into_response(), - ), + Media::Font => { + Ok((content_security_policy, PreviewFontHtml { inscription_id }).into_response()) + } + Media::Iframe => unreachable!(), Media::Image(image_rendering) => Ok( ( - [( - header::CONTENT_SECURITY_POLICY, - "default-src 'self' 'unsafe-inline'", - )], + content_security_policy, PreviewImageHtml { image_rendering, inscription_id, @@ -1468,37 +1625,24 @@ impl Server { ), Media::Markdown => Ok( ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://cdn.jsdelivr.net", - )], + content_security_policy, PreviewMarkdownHtml { inscription_id }, ) .into_response(), ), - Media::Model => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://ajax.googleapis.com", - )], - PreviewModelHtml { inscription_id }, - ) - .into_response(), - ), - Media::Pdf => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://cdn.jsdelivr.net", - )], - PreviewPdfHtml { inscription_id }, - ) - .into_response(), - ), - Media::Text => Ok(PreviewTextHtml { inscription_id }.into_response()), - Media::Unknown => Ok(PreviewUnknownHtml.into_response()), - Media::Video => Ok(PreviewVideoHtml { inscription_id }.into_response()), + Media::Model => { + Ok((content_security_policy, PreviewModelHtml { inscription_id }).into_response()) + } + Media::Pdf => { + Ok((content_security_policy, PreviewPdfHtml { inscription_id }).into_response()) + } + Media::Text => { + Ok((content_security_policy, PreviewTextHtml { inscription_id }).into_response()) + } + Media::Unknown => Ok((content_security_policy, PreviewUnknownHtml).into_response()), + Media::Video => { + Ok((content_security_policy, PreviewVideoHtml { inscription_id }).into_response()) + } } }) } @@ -1508,11 +1652,28 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { + if let query::Inscription::Sat(_) = query { + if !index.has_sat_index() { + return Err(ServerError::NotFound("sat index required".into())); + } + } + let info = Index::inscription_info(&index, query)? .ok_or_not_found(|| format!("inscription {query}"))?; + let effective_mime_type = if let Some(delegate_id) = info.inscription.delegate() { + let delegate_result = index.get_inscription_by_id(delegate_id); + if let Ok(Some(delegate)) = delegate_result { + delegate.content_type().map(str::to_string) + } else { + info.inscription.content_type().map(str::to_string) + } + } else { + info.inscription.content_type().map(str::to_string) + }; + Ok(if accept_json { Json(api::Inscription { address: info @@ -1525,31 +1686,31 @@ impl Server { .ok() }) .map(|address| address.to_string()), - charms: Charm::ALL - .iter() - .filter(|charm| charm.is_set(info.charms)) - .map(|charm| charm.title().into()) - .collect(), + charms: Charm::charms(info.charms), children: info.children, content_length: info.inscription.content_length(), content_type: info.inscription.content_type().map(|s| s.to_string()), + effective_content_type: effective_mime_type, fee: info.entry.fee, height: info.entry.height, id: info.entry.id, + next: info.next, number: info.entry.inscription_number, - parent: info.parent, - sat: info.entry.sat, - satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp).timestamp(), + parents: info.parents, previous: info.previous, - next: info.next, rune: info.rune, + sat: info.entry.sat, + satpoint: info.satpoint, + timestamp: timestamp(info.entry.timestamp.into()).timestamp(), value: info.output.as_ref().map(|o| o.value), // ---- Ordzaar ---- inscription_sequence: info.entry.sequence_number, delegate: info.inscription.delegate(), - content_encoding: info.inscription.content_encoding_str().map(|s| s.to_string()), + content_encoding: info + .inscription + .content_encoding_str() + .map(|s| s.to_string()), // ---- Ordzaar ---- }) .into_response() @@ -1565,12 +1726,12 @@ impl Server { number: info.entry.inscription_number, next: info.next, output: info.output, - parent: info.parent, + parents: info.parents, previous: info.previous, rune: info.rune, sat: info.entry.sat, satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp), + timestamp: timestamp(info.entry.timestamp.into()), } .page(server_config) .into_response() @@ -1581,7 +1742,7 @@ impl Server { async fn collections( Extension(server_config): Extension>, Extension(index): Extension>, - ) -> ServerResult { + ) -> ServerResult { Self::collections_paginated(Extension(server_config), Extension(index), Path(0)).await } @@ -1589,7 +1750,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, Path(page_index): Path, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let (collections, more_collections) = index.get_collections_paginated(100, page_index)?; @@ -1613,7 +1774,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult { + ) -> ServerResult { Self::children_paginated( Extension(server_config), Extension(index), @@ -1626,7 +1787,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let entry = index .get_inscription_entry(parent)? @@ -1658,14 +1819,14 @@ impl Server { async fn children_recursive( Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult { + ) -> ServerResult { Self::children_recursive_paginated(Extension(index), Path((inscription_id, 0))).await } async fn children_recursive_paginated( Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let parent_sequence_number = index .get_inscription_entry(parent)? @@ -1683,7 +1844,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, accept_json: AcceptJson, - ) -> ServerResult { + ) -> ServerResult { Self::inscriptions_paginated( Extension(server_config), Extension(index), @@ -1698,7 +1859,7 @@ impl Server { Extension(index): Extension>, Path(page_index): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?; @@ -1730,7 +1891,7 @@ impl Server { Extension(index): Extension>, Path(block_height): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { Self::inscriptions_in_block_paginated( Extension(server_config), Extension(index), @@ -1745,7 +1906,7 @@ impl Server { Extension(index): Extension>, Path((block_height, page_index)): Path<(u32, u32)>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let page_size = 100; @@ -1786,6 +1947,49 @@ impl Server { }) } + async fn parents( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(inscription_id): Path, + ) -> ServerResult { + Self::parents_paginated( + Extension(server_config), + Extension(index), + Path((inscription_id, 0)), + ) + .await + } + + async fn parents_paginated( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path((id, page)): Path<(InscriptionId, usize)>, + ) -> ServerResult { + task::block_in_place(|| { + let child = index + .get_inscription_entry(id)? + .ok_or_not_found(|| format!("inscription {id}"))?; + + let (parents, more) = index.get_parents_by_sequence_number_paginated(child.parents, page)?; + + let prev_page = page.checked_sub(1); + + let next_page = more.then_some(page + 1); + + Ok( + ParentsHtml { + id, + number: child.inscription_number, + parents, + prev_page, + next_page, + } + .page(server_config) + .into_response(), + ) + }) + } + async fn sat_inscriptions( Extension(index): Extension>, Path(sat): Path, @@ -1842,28 +2046,23 @@ impl Server { #[cfg(test)] mod tests { use { - super::*, - crate::runes::{Edict, Etching, Rune, Runestone}, - reqwest::Url, - serde::de::DeserializeOwned, - std::net::TcpListener, - tempfile::TempDir, + super::*, reqwest::Url, serde::de::DeserializeOwned, std::net::TcpListener, tempfile::TempDir, }; const RUNE: u128 = 99246114928149462; #[derive(Default)] struct Builder { - bitcoin_rpc_server: Option, + core: Option, config: String, ord_args: BTreeMap>, server_args: BTreeMap>, } impl Builder { - fn bitcoin_rpc_server(self, bitcoin_rpc_server: test_bitcoincore_rpc::Handle) -> Self { + fn core(self, core: mockcore::Handle) -> Self { Self { - bitcoin_rpc_server: Some(bitcoin_rpc_server), + core: Some(core), ..self } } @@ -1900,8 +2099,8 @@ mod tests { } fn build(self) -> TestServer { - let bitcoin_rpc_server = self.bitcoin_rpc_server.unwrap_or_else(|| { - test_bitcoincore_rpc::builder() + let core = self.core.unwrap_or_else(|| { + mockcore::builder() .network( self .ord_args @@ -1928,17 +2127,17 @@ mod tests { let mut args = vec!["ord".to_string()]; args.push("--bitcoin-rpc-url".into()); - args.push(bitcoin_rpc_server.url()); + args.push(core.url()); args.push("--cookie-file".into()); args.push(cookiefile.to_str().unwrap().into()); - args.push("--data-dir".into()); + args.push("--datadir".into()); args.push(tempdir.path().to_str().unwrap().into()); if !self.ord_args.contains_key("--chain") { args.push("--chain".into()); - args.push(bitcoin_rpc_server.network()); + args.push(core.network()); } for (arg, value) in self.ord_args { @@ -2011,7 +2210,7 @@ mod tests { } TestServer { - bitcoin_rpc_server, + core, index, ord_server_handle, tempdir, @@ -2037,7 +2236,7 @@ mod tests { } struct TestServer { - bitcoin_rpc_server: test_bitcoincore_rpc::Handle, + core: mockcore::Handle, index: Arc, ord_server_handle: Handle, #[allow(unused)] @@ -2054,6 +2253,63 @@ mod tests { Builder::default().build() } + #[track_caller] + pub(crate) fn etch( + &self, + runestone: Runestone, + outputs: usize, + witness: Option, + ) -> (Txid, RuneId) { + let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap(); + + self.mine_blocks(1); + + self.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Default::default())], + p2tr: true, + ..default() + }); + + self.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + + let witness = witness.unwrap_or_else(|| { + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + runestone + .etching + .unwrap() + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + let mut witness = Witness::default(); + witness.push(tapscript); + witness.push([]); + witness + }); + + let txid = self.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs, + ..default() + }); + + self.mine_blocks(1); + + ( + txid, + RuneId { + block: (self.index.block_count().unwrap() - 1).into(), + tx: 1, + }, + ) + } + #[track_caller] fn get(&self, path: impl AsRef) -> reqwest::blocking::Response { if let Err(error) = self.index.update() { @@ -2137,14 +2393,15 @@ mod tests { assert_eq!(response.headers().get(header::LOCATION).unwrap(), location); } + #[track_caller] fn mine_blocks(&self, n: u64) -> Vec { - let blocks = self.bitcoin_rpc_server.mine_blocks(n); + let blocks = self.core.mine_blocks(n); self.index.update().unwrap(); blocks } fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec { - let blocks = self.bitcoin_rpc_server.mine_blocks_with_subsidy(n, subsidy); + let blocks = self.core.mine_blocks_with_subsidy(n, subsidy); self.index.update().unwrap(); blocks } @@ -2292,7 +2549,7 @@ mod tests { #[test] fn acme_cache_defaults_to_data_dir() { - let arguments = Arguments::try_parse_from(["ord", "--data-dir", "foo", "server"]).unwrap(); + let arguments = Arguments::try_parse_from(["ord", "--datadir", "foo", "server"]).unwrap(); let settings = Settings::from_options(arguments.options) .or_defaults() @@ -2312,7 +2569,7 @@ mod tests { #[test] fn acme_cache_flag_is_respected() { let arguments = - Arguments::try_parse_from(["ord", "--data-dir", "foo", "server", "--acme-cache", "bar"]) + Arguments::try_parse_from(["ord", "--datadir", "foo", "server", "--acme-cache", "bar"]) .unwrap(); let settings = Settings::from_options(arguments.options) @@ -2363,11 +2620,6 @@ mod tests { TestServer::new().assert_redirect("/faq", "https://docs.ordinals.com/faq/"); } - #[test] - fn search_by_query_returns_sat() { - TestServer::new().assert_redirect("/search?query=0", "/sat/0"); - } - #[test] fn search_by_query_returns_rune() { TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD"); @@ -2386,14 +2638,19 @@ mod tests { ); } + #[test] + fn search_by_query_returns_inscription_by_number() { + TestServer::new().assert_redirect("/search?query=0", "/inscription/0"); + } + #[test] fn search_is_whitespace_insensitive() { - TestServer::new().assert_redirect("/search/ 0 ", "/sat/0"); + TestServer::new().assert_redirect("/search/ abc ", "/sat/abc"); } #[test] fn search_by_path_returns_sat() { - TestServer::new().assert_redirect("/search/0", "/sat/0"); + TestServer::new().assert_redirect("/search/abc", "/sat/abc"); } #[test] @@ -2451,30 +2708,27 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), + ..default() + }), + ..default() + }, + 1, + None, + ); server.mine_blocks(1); - server.assert_redirect("/search/2:1", "/rune/AAAAAAAAAAAAA"); - server.assert_redirect("/search?query=2:1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search/9:1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search?query=9:1", "/rune/AAAAAAAAAAAAA"); server.assert_response_regex( "/search/100000000000000000000:200000000000000000", @@ -2483,6 +2737,44 @@ mod tests { ); } + #[test] + fn fallback() { + let server = TestServer::new(); + + server.assert_redirect("/0", "/inscription/0"); + server.assert_redirect("/0/", "/inscription/0"); + server.assert_redirect("/0//", "/inscription/0"); + server.assert_redirect( + "/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0", + "/inscription/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0", + ); + server.assert_redirect("/-1", "/inscription/-1"); + server.assert_redirect("/FOO", "/rune/FOO"); + server.assert_redirect("/FO.O", "/rune/FO.O"); + server.assert_redirect("/FO•O", "/rune/FO•O"); + server.assert_redirect("/0:0", "/rune/0:0"); + server.assert_redirect( + "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0", + "/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0", + ); + server.assert_redirect( + "/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + ); + server.assert_redirect( + "/search/0000000000000000000000000000000000000000000000000000000000000000", + "/tx/0000000000000000000000000000000000000000000000000000000000000000", + ); + + server.assert_response_regex("/hello", StatusCode::NOT_FOUND, ""); + + server.assert_response_regex( + "/%C3%28", + StatusCode::BAD_REQUEST, + "invalid utf-8 sequence of 1 bytes from index 0", + ); + } + #[test] fn runes_can_be_queried_by_rune_id() { let server = TestServer::builder() @@ -2494,32 +2786,29 @@ mod tests { let rune = Rune(RUNE); - server.assert_response_regex("/rune/2:1", StatusCode::NOT_FOUND, ".*"); - - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + server.assert_response_regex("/rune/9:1", StatusCode::NOT_FOUND, ".*"); + + server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), + ..default() + }), + ..default() + }, + 1, + None, + ); server.mine_blocks(1); server.assert_response_regex( - "/rune/2:1", + "/rune/9:1", StatusCode::OK, ".*Rune AAAAAAAAAAAAA.*", ); @@ -2540,43 +2829,40 @@ mod tests { ".*Runes.*

        Runes

        \n
          \n
        .*", ); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + symbol: Some('%'), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + Default::default(), + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0 + }, + premine: u128::MAX, + timestamp: id.block, + symbol: Some('%'), + ..default() } )] ); @@ -2610,45 +2896,45 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - symbol: Some('%'), - ..Default::default() - }), - ..Default::default() + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), + symbol: Some('%'), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + Some( + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + rune: Some(rune.commitment()), + ..default() } - .encipher(), + .to_witness(), ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ); assert_eq!( server.index.runes().unwrap(), [( id, RuneEntry { + block: id.block, etching: txid, - rune, - supply: u128::MAX, + spaced_rune: SpacedRune { rune, spacers: 0 }, + premine: u128::MAX, symbol: Some('%'), - timestamp: 2, - ..Default::default() + timestamp: id.block, + ..default() } )] ); @@ -2664,24 +2950,26 @@ mod tests { format!( ".*Rune AAAAAAAAAAAAA.*

        AAAAAAAAAAAAA

        - +.*.*
        number
        0
        timestamp
        -
        +
        id
        -
        2:1
        -
        etching block height
        -
        2
        -
        etching transaction index
        +
        9:1
        +
        etching block
        +
        9
        +
        etching transaction
        1
        mint
        no
        supply
        -
        340282366920938463463374607431768211455\u{00A0}%
        +
        340282366920938463463374607431768211455\u{A0}%
        +
        premine
        +
        340282366920938463463374607431768211455\u{A0}%
        burned
        -
        0\u{00A0}%
        +
        0\u{A0}%
        divisibility
        0
        symbol
        @@ -2721,47 +3009,46 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - symbol: Some('%'), - spacers: 1, - ..Default::default() - }), - ..Default::default() + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), + symbol: Some('%'), + spacers: Some(1), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + Some( + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + rune: Some(rune.commitment()), + ..default() } - .encipher(), + .to_witness(), ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, RuneEntry { + block: id.block, etching: txid, - rune, - supply: u128::MAX, + spaced_rune: SpacedRune { rune, spacers: 1 }, + premine: u128::MAX, symbol: Some('%'), - timestamp: 2, - spacers: 1, - ..Default::default() + timestamp: id.block, + ..default() } )] ); @@ -2803,7 +3090,7 @@ mod tests { StatusCode::OK, ".* A•AAAAAAAAAAAA - 340282366920938463463374607431768211455\u{00A0}% + 340282366920938463463374607431768211455\u{A0}% .*", ); } @@ -2823,48 +3110,43 @@ mod tests { ".*Runes.*

        Runes

        \n
          \n
        .*", ); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + None, + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, RuneEntry { + block: id.block, etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0 + }, + premine: u128::MAX, + timestamp: id.block, + ..default() } )] ); - assert_eq!( + pretty_assert_eq!( server.index.get_rune_balances().unwrap(), [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])] ); @@ -2892,45 +3174,37 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Default::default())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - divisibility: 1, - rune: Some(rune), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + divisibility: Some(1), + rune: Some(rune), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + None, + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, RuneEntry { + block: id.block, divisibility: 1, etching: txid, - rune, - supply: u128::MAX, - timestamp: 2, - ..Default::default() + spaced_rune: SpacedRune { rune, spacers: 0 }, + premine: u128::MAX, + timestamp: id.block, + ..default() } )] ); @@ -2956,7 +3230,7 @@ mod tests { AAAAAAAAAAAAA - 34028236692093846346337460743176821145.5 + 34028236692093846346337460743176821145.5\u{A0}¤
        @@ -2964,12 +3238,14 @@ mod tests { ), ); - assert_eq!( + let address = default_address(Chain::Regtest); + + pretty_assert_eq!( server.get_json::(format!("/output/{output}")), api::Output { value: 5000000000, - script_pubkey: String::new(), - address: None, + script_pubkey: address.script_pubkey().to_asm_string(), + address: Some(uncheck(&address)), transaction: txid.to_string(), sat_ranges: None, indexed: true, @@ -3017,34 +3293,34 @@ mod tests { server.mine_blocks(3); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[( 3, 0, 0, Inscription::new(None, Some("hello".as_bytes().into())).to_witness(), )], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -3057,15 +3333,15 @@ mod tests {
        chain
        regtest
        height
        -
        4
        +
        4
        inscriptions
        -
        3
        +
        3
        blessed inscriptions
        3
        cursed inscriptions
        0
        runes
        -
        0
        +
        0
        lost sats
        .*
        started
        @@ -3380,22 +3656,22 @@ mod tests { server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 50 * 100_000_000, - ..Default::default() + ..default() }); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 2, 1, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -3452,17 +3728,17 @@ mod tests { let mut ids = Vec::new(); for i in 0..101 { - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())], - ..Default::default() + ..default() }); ids.push(InscriptionId { txid, index: 0 }); server.mine_blocks(1); } - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "{}").to_witness())], - ..Default::default() + server.core.broadcast_tx(TransactionTemplate { + inputs: &[(102, 0, 0, inscription("text/plain", "{}").to_witness())], + ..default() }); server.mine_blocks(1); @@ -3593,9 +3869,9 @@ mod tests { let transaction = TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], fee: 0, - ..Default::default() + ..default() }; - test_server.bitcoin_rpc_server.broadcast_tx(transaction); + test_server.core.broadcast_tx(transaction); let block_hash = test_server.mine_blocks(1)[0].block_hash(); test_server.assert_response_regex( @@ -3633,12 +3909,12 @@ mod tests {

        1 Output

        .*" @@ -3659,10 +3935,10 @@ mod tests { ); for _ in 0..15 { - test_server.bitcoin_rpc_server.invalidate_tip(); + test_server.core.invalidate_tip(); } - test_server.bitcoin_rpc_server.mine_blocks(21); + test_server.core.mine_blocks(21); test_server.assert_response_regex( "/status", @@ -3848,11 +4124,11 @@ mod tests { ); server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], outputs: 2, fee: 0, - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -3872,11 +4148,11 @@ mod tests { ); server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], outputs: 2, fee: 2, - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -3936,7 +4212,7 @@ mod tests { AcceptEncoding::default(), &ServerConfig { csp_origin: Some("https://ordinals.com".into()), - ..Default::default() + ..default() }, ) .unwrap() @@ -3945,19 +4221,69 @@ mod tests { assert_eq!(headers["content-security-policy"], HeaderValue::from_static("default-src https://ordinals.com/content/ https://ordinals.com/blockheight https://ordinals.com/blockhash https://ordinals.com/blockhash/ https://ordinals.com/blocktime https://ordinals.com/r/ 'unsafe-eval' 'unsafe-inline' data: blob:")); } + #[test] + fn preview_content_security_policy() { + { + let server = TestServer::builder().chain(Chain::Regtest).build(); + + server.mine_blocks(1); + + let txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_csp( + format!("/preview/{}", inscription_id), + StatusCode::OK, + "default-src 'self'", + format!(".*.*", inscription_id), + ); + } + + { + let server = TestServer::builder() + .chain(Chain::Regtest) + .server_option("--csp-origin", "https://ordinals.com") + .build(); + + server.mine_blocks(1); + + let txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_csp( + format!("/preview/{}", inscription_id), + StatusCode::OK, + "default-src https://ordinals.com", + format!(".*.*", inscription_id), + ); + } + } + #[test] fn code_preview() { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/javascript", "hello").to_witness(), )], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4003,14 +4329,14 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/plain;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4030,9 +4356,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("audio/flac", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4050,9 +4376,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("font/ttf", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4070,14 +4396,14 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("application/pdf", "hello").to_witness(), )], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4095,9 +4421,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/markdown", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4115,9 +4441,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("image/png", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4136,14 +4462,14 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, inscription("text/html;charset=utf-8", "hello").to_witness(), )], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4161,9 +4487,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4181,9 +4507,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("video/webm", "hello").to_witness())], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4204,9 +4530,9 @@ mod tests { .build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4226,9 +4552,9 @@ mod tests { .build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4240,14 +4566,45 @@ mod tests { ); } + #[test] + fn inscriptions_can_be_looked_up_by_sat_name() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_sats() + .build(); + server.mine_blocks(1); + + server.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], + ..default() + }); + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{}", Sat(5000000000).name()), + StatusCode::OK, + ".*Inscription 0</title.*", + ); + } + + #[test] + fn inscriptions_can_be_looked_up_by_sat_name_with_letter_i() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_sats() + .build(); + server.assert_response_regex("/inscription/i", StatusCode::NOT_FOUND, ".*"); + } + #[test] fn inscription_page_does_not_have_sat_when_sats_are_not_tracked() { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4279,9 +4636,9 @@ mod tests { .build(); server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4301,14 +4658,14 @@ mod tests { .build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, Inscription::new(Some("foo/bar".as_bytes().to_vec()), None).to_witness(), )], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4330,14 +4687,14 @@ mod tests { .build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[( 1, 0, 0, Inscription::new(Some("image/png".as_bytes().to_vec()), None).to_witness(), )], - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4356,9 +4713,9 @@ mod tests { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4403,9 +4760,9 @@ mod tests { for i in 0..101 { server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); } @@ -4427,9 +4784,9 @@ mod tests { for i in 0..101 { server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); } @@ -4455,9 +4812,9 @@ mod tests { server.mine_blocks(1); parent_ids.push(InscriptionId { - txid: server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + txid: server.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }), index: 0, }); @@ -4466,7 +4823,7 @@ mod tests { for (i, parent_id) in parent_ids.iter().enumerate().take(101) { server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[ (i + 2, 1, 0, Default::default()), ( @@ -4476,15 +4833,15 @@ mod tests { Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_id.value()), - ..Default::default() + parents: vec![parent_id.value()], + ..default() } .to_witness(), ), ], outputs: 2, output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE], - ..Default::default() + ..default() }); } @@ -4573,9 +4930,9 @@ next let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let parent_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4585,7 +4942,7 @@ next index: 0, }; - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ ( 2, @@ -4594,14 +4951,14 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), (2, 1, 0, Default::default()), ], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4611,7 +4968,7 @@ next server.assert_response_regex( format!("/inscription/{inscription_id}"), StatusCode::OK, - format!(".*<title>Inscription 1.*
        parent
        .*
        .**.*"), + format!(".*Inscription 1.*
        parents
        .*
        .**.*"), ); server.assert_response_regex( format!("/inscription/{parent_inscription_id}"), @@ -4622,8 +4979,8 @@ next assert_eq!( server .get_json::(format!("/inscription/{inscription_id}")) - .parent, - Some(parent_inscription_id), + .parents, + vec![parent_inscription_id], ); assert_eq!( @@ -4639,9 +4996,9 @@ next let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let parent_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4657,7 +5014,7 @@ next ".*

        No children

        .*", ); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ ( 2, @@ -4666,14 +5023,14 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), (2, 1, 0, Default::default()), ], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4692,9 +5049,9 @@ next let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let parent_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(6); @@ -4704,7 +5061,7 @@ next index: 0, }; - let _txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let _txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ ( 2, @@ -4713,8 +5070,8 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), @@ -4725,8 +5082,8 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), @@ -4737,8 +5094,8 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), @@ -4749,8 +5106,8 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), @@ -4761,14 +5118,14 @@ next Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), - ..Default::default() + parents: vec![parent_inscription_id.value()], + ..default() } .to_witness(), ), (2, 1, 0, Default::default()), ], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -4789,18 +5146,148 @@ next ); } + #[test] + fn inscription_with_parent_page() { + let server = TestServer::builder().chain(Chain::Regtest).build(); + server.mine_blocks(2); + + let parent_a_txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + + let parent_b_txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }); + + server.mine_blocks(1); + + let parent_a_inscription_id = InscriptionId { + txid: parent_a_txid, + index: 0, + }; + + let parent_b_inscription_id = InscriptionId { + txid: parent_b_txid, + index: 0, + }; + + let txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &[ + ( + 3, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: vec![ + parent_a_inscription_id.value(), + parent_b_inscription_id.value(), + ], + ..default() + } + .to_witness(), + ), + (3, 1, 0, Default::default()), + (3, 2, 0, Default::default()), + ], + ..default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/parents/{inscription_id}"), + StatusCode::OK, + format!(".*Inscription -1 Parents.*

        Inscription -1 Parents

        .*
        .*.*"), + ); + } + + #[test] + fn inscription_parent_page_pagination() { + let server = TestServer::builder().chain(Chain::Regtest).build(); + + server.mine_blocks(1); + + let mut parent_ids = Vec::new(); + let mut inputs = Vec::new(); + for i in 0..101 { + parent_ids.push( + InscriptionId { + txid: server.core.broadcast_tx(TransactionTemplate { + inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..default() + }), + index: 0, + } + .value(), + ); + + inputs.push((i + 2, 1, 0, Witness::default())); + + server.mine_blocks(1); + } + + inputs.insert( + 0, + ( + 102, + 0, + 0, + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + parents: parent_ids, + ..default() + } + .to_witness(), + ), + ); + + let txid = server.core.broadcast_tx(TransactionTemplate { + inputs: &inputs, + ..default() + }); + + server.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + server.assert_response_regex( + format!("/parents/{inscription_id}"), + StatusCode::OK, + format!(".*Inscription -1 Parents.*

        Inscription -1 Parents

        .*
        (.*.*){{100}}.*"), + ); + + server.assert_response_regex( + format!("/parents/{inscription_id}/1"), + StatusCode::OK, + format!(".*Inscription -1 Parents.*

        Inscription -1 Parents

        .*
        (.*.*){{1}}.*"), + ); + + server.assert_response_regex( + format!("/inscription/{inscription_id}"), + StatusCode::OK, + ".*Inscription -1.*

        Inscription -1

        .*
        (.*.*){4}.*", + ); + } + #[test] fn inscription_number_endpoint() { let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(2); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, inscription("text/plain", "hello").to_witness()), (2, 0, 0, inscription("text/plain", "cursed").to_witness()), ], outputs: 2, - ..Default::default() + ..default() }); let inscription_id = InscriptionId { txid, index: 0 }; @@ -4847,13 +5334,13 @@ next server.mine_blocks(2); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, Witness::default()), (2, 0, 0, inscription("text/plain", "cursed").to_witness()), ], outputs: 2, - ..Default::default() + ..default() }); let id = InscriptionId { txid, index: 0 }; @@ -4886,13 +5373,13 @@ next server.mine_blocks(110); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, Witness::default()), (2, 0, 0, inscription("text/plain", "cursed").to_witness()), ], outputs: 2, - ..Default::default() + ..default() }); let id = InscriptionId { txid, index: 0 }; @@ -4907,6 +5394,7 @@ next
        id
        {id}
        + .*
        value
        .*
        @@ -4925,9 +5413,9 @@ next server.mine_blocks(2); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); let id = InscriptionId { txid, index: 0 }; @@ -4961,9 +5449,9 @@ next server.mine_blocks(2); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); let id = InscriptionId { txid, index: 0 }; @@ -4997,9 +5485,9 @@ next server.mine_blocks(9); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); let id = InscriptionId { txid, index: 0 }; @@ -5030,16 +5518,16 @@ next server.mine_blocks(1); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, inscription("text/plain", "bar").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5102,9 +5590,9 @@ next let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5118,6 +5606,7 @@ next
        id
        {id}
        + .*
        value
        .*
        @@ -5154,17 +5643,17 @@ next let cursed_inscription = inscription("text/plain", "bar"); let reinscription: Inscription = InscriptionTemplate { pointer: Some(0), - ..Default::default() + ..default() } .into(); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, inscription("text/plain", "foo").to_witness()), (2, 0, 0, cursed_inscription.to_witness()), (3, 0, 0, reinscription.to_witness()), ], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5178,6 +5667,7 @@ next
        id
        {id}
        + .*
        value
        .*
        @@ -5210,9 +5700,9 @@ next server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, envelope(&[b"ord", &[128], &[0]]))], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5246,9 +5736,9 @@ next server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); let id = InscriptionId { txid, index: 0 }; @@ -5263,6 +5753,7 @@ next
        id
        {id}
        + .*
        value
        5000000000
        .* @@ -5272,10 +5763,10 @@ next ), ); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, Default::default())], fee: 50 * COIN_VALUE, - ..Default::default() + ..default() }); server.mine_blocks_with_subsidy(1, 0); @@ -5310,7 +5801,7 @@ next assert_eq!( server.get_json::("/r/sat/5000000000"), api::SatInscriptions { - ids: vec![], + ids: Vec::new(), page: 0, more: false } @@ -5323,9 +5814,9 @@ next server.mine_blocks(1); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5334,9 +5825,9 @@ next ids.push(InscriptionId { txid, index: 0 }); for i in 1..111 { - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 1, 0, inscription("text/plain", "foo").to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5405,9 +5896,9 @@ next let server = TestServer::builder().chain(Chain::Regtest).build(); server.mine_blocks(1); - let parent_txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let parent_txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - ..Default::default() + ..default() }); let parent_inscription_id = InscriptionId { @@ -5432,18 +5923,18 @@ next builder = Inscription { content_type: Some("text/plain".into()), body: Some("hello".into()), - parent: Some(parent_inscription_id.value()), + parents: vec![parent_inscription_id.value()], unrecognized_even_field: false, - ..Default::default() + ..default() } .append_reveal_script_to_builder(builder); } let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5484,9 +5975,9 @@ next } for i in 0..101 { - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + server.core.broadcast_tx(TransactionTemplate { inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); } @@ -5526,6 +6017,18 @@ next ); } + #[test] + fn looking_up_inscription_by_sat_requires_sat_index() { + TestServer::builder() + .chain(Chain::Regtest) + .build() + .assert_response( + "/inscription/abcd", + StatusCode::NOT_FOUND, + "sat index required", + ); + } + #[test] fn delegate() { let server = TestServer::builder().chain(Chain::Regtest).build(); @@ -5535,12 +6038,12 @@ next let delegate = Inscription { content_type: Some("text/html".into()), body: Some("foo".into()), - ..Default::default() + ..default() }; - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, delegate.to_witness())], - ..Default::default() + ..default() }); let delegate = InscriptionId { txid, index: 0 }; @@ -5549,12 +6052,12 @@ next let inscription = Inscription { delegate: Some(delegate.value()), - ..Default::default() + ..default() }; - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5592,12 +6095,12 @@ next let inscription = Inscription { content_type: Some("text/html".into()), body: Some("foo".into()), - ..Default::default() + ..default() }; - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = server.core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription.to_witness())], - ..Default::default() + ..default() }); server.mine_blocks(1); @@ -5713,23 +6216,23 @@ next #[test] fn inscriptions_can_be_hidden_with_config() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + let core = mockcore::builder() .network(Chain::Regtest.network()) .build(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = core.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())], - ..Default::default() + ..default() }); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let inscription = InscriptionId { txid, index: 0 }; let server = TestServer::builder() - .bitcoin_rpc_server(bitcoin_rpc_server) + .core(core) .config(&format!("hidden: [{inscription}]")) .build(); @@ -5741,4 +6244,10 @@ next PreviewUnknownHtml.to_string(), ); } + + #[test] + fn update_endpoint_is_not_available_when_not_in_integration_test_mode() { + let server = TestServer::builder().build(); + server.assert_response("/update", StatusCode::NOT_FOUND, ""); + } } diff --git a/src/subcommand/server/accept_encoding.rs b/src/subcommand/server/accept_encoding.rs index 0143339a59..5106eab882 100644 --- a/src/subcommand/server/accept_encoding.rs +++ b/src/subcommand/server/accept_encoding.rs @@ -59,7 +59,7 @@ mod tests { &Arc::new(ServerConfig { json_api_enabled: false, decompress: false, - ..Default::default() + ..default() }), ) .await @@ -80,7 +80,7 @@ mod tests { &Arc::new(ServerConfig { json_api_enabled: false, decompress: false, - ..Default::default() + ..default() }), ) .await @@ -109,7 +109,7 @@ mod tests { &Arc::new(ServerConfig { json_api_enabled: false, decompress: false, - ..Default::default() + ..default() }), ) .await diff --git a/src/subcommand/server/error.rs b/src/subcommand/server/error.rs index ccbff8bc62..61efbc640a 100644 --- a/src/subcommand/server/error.rs +++ b/src/subcommand/server/error.rs @@ -11,7 +11,7 @@ pub(super) enum ServerError { NotFound(String), } -pub(super) type ServerResult = Result; +pub(super) type ServerResult = Result; impl IntoResponse for ServerError { fn into_response(self) -> Response { diff --git a/src/subcommand/server/query.rs b/src/subcommand/server/query.rs index 6a57b59349..3acdc5e940 100644 --- a/src/subcommand/server/query.rs +++ b/src/subcommand/server/query.rs @@ -21,17 +21,22 @@ impl FromStr for Block { pub(crate) enum Inscription { Id(InscriptionId), Number(i32), + Sat(Sat), } impl FromStr for Inscription { type Err = Error; fn from_str(s: &str) -> Result { - Ok(if s.contains('i') { - Self::Id(s.parse()?) + if re::INSCRIPTION_ID.is_match(s) { + Ok(Self::Id(s.parse()?)) + } else if re::INSCRIPTION_NUMBER.is_match(s) { + Ok(Self::Number(s.parse()?)) + } else if re::SAT_NAME.is_match(s) { + Ok(Self::Sat(s.parse()?)) } else { - Self::Number(s.parse()?) - }) + Err(anyhow!("bad inscription query {s}")) + } } } @@ -40,6 +45,7 @@ impl Display for Inscription { match self { Self::Id(id) => write!(f, "{id}"), Self::Number(number) => write!(f, "{number}"), + Self::Sat(sat) => write!(f, "on sat {}", sat.name()), } } } diff --git a/src/subcommand/server/server_config.rs b/src/subcommand/server/server_config.rs new file mode 100644 index 0000000000..79f141ebdd --- /dev/null +++ b/src/subcommand/server/server_config.rs @@ -0,0 +1,48 @@ +use {super::*, axum::http::HeaderName}; + +#[derive(Default)] +pub(crate) struct ServerConfig { + pub(crate) chain: Chain, + pub(crate) content_proxy: Option, + pub(crate) csp_origin: Option, + pub(crate) decompress: bool, + pub(crate) domain: Option, + pub(crate) index_sats: bool, + pub(crate) json_api_enabled: bool, +} + +impl ServerConfig { + pub(super) fn preview_content_security_policy( + &self, + media: Media, + ) -> ServerResult<[(HeaderName, HeaderValue); 1]> { + let default = match media { + Media::Audio => "default-src 'self'", + Media::Code(_) => "script-src-elem 'self' https://cdn.jsdelivr.net", + Media::Font => "script-src-elem 'self'; style-src 'self' 'unsafe-inline'", + Media::Iframe => { + return Err( + anyhow!("preview_content_security_policy cannot be called with Media::Iframe").into(), + ) + } + Media::Image(_) => "default-src 'self' 'unsafe-inline'", + Media::Markdown => "script-src-elem 'self' https://cdn.jsdelivr.net", + Media::Model => "script-src-elem 'self' https://ajax.googleapis.com", + Media::Pdf => "script-src-elem 'self' https://cdn.jsdelivr.net", + Media::Text => "default-src 'self'", + Media::Unknown => "default-src 'self'", + Media::Video => "default-src 'self'", + }; + + let value = if let Some(csp_origin) = &self.csp_origin { + default + .replace("'self'", csp_origin) + .parse() + .map_err(|err| anyhow!("invalid content-security-policy origin `{csp_origin}`: {err}"))? + } else { + HeaderValue::from_static(default) + }; + + Ok([(header::CONTENT_SECURITY_POLICY, value)]) + } +} diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 59133dee65..5a2b55b18f 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -1,25 +1,25 @@ use { super::*, - crate::wallet::{ - inscribe::{Batch, Batchfile, Mode}, - Wallet, - }, + crate::wallet::{batch, Wallet}, bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult, - reqwest::Url, + shared_args::SharedArgs, }; pub mod balance; +mod batch_command; pub mod cardinals; pub mod create; pub mod dump; -pub mod etch; pub mod inscribe; pub mod inscriptions; +pub mod mint; pub mod outputs; pub mod receive; pub mod restore; +pub mod resume; pub mod sats; pub mod send; +mod shared_args; pub mod transactions; #[derive(Debug, Parser)] @@ -42,20 +42,24 @@ pub(crate) struct WalletCommand { pub(crate) enum Subcommand { #[command(about = "Get wallet balance")] Balance, + #[command(about = "Create inscriptions and runes")] + Batch(batch_command::Batch), #[command(about = "Create new wallet")] Create(create::Create), #[command(about = "Dump wallet descriptors")] Dump, - #[command(about = "Create rune")] - Etch(etch::Etch), #[command(about = "Create inscription")] Inscribe(inscribe::Inscribe), #[command(about = "List wallet inscriptions")] Inscriptions, + #[command(about = "Mint a rune")] + Mint(mint::Mint), #[command(about = "Generate receive address")] - Receive, + Receive(receive::Receive), #[command(about = "Restore wallet")] Restore(restore::Restore), + #[command(about = "Resume pending etchings")] + Resume, #[command(about = "List wallet satoshis")] Sats(sats::Sats), #[command(about = "Send sat or inscription")] @@ -92,11 +96,13 @@ impl WalletCommand { match self.subcommand { Subcommand::Balance => balance::run(wallet), + Subcommand::Batch(batch) => batch.run(wallet), Subcommand::Dump => dump::run(wallet), - Subcommand::Etch(etch) => etch.run(wallet), Subcommand::Inscribe(inscribe) => inscribe.run(wallet), Subcommand::Inscriptions => inscriptions::run(wallet), - Subcommand::Receive => receive::run(wallet), + Subcommand::Mint(mint) => mint.run(wallet), + Subcommand::Receive(receive) => receive.run(wallet), + Subcommand::Resume => resume::run(wallet), Subcommand::Sats(sats) => sats.run(wallet), Subcommand::Send(send) => send.run(wallet), Subcommand::Transactions(transactions) => transactions.run(wallet), diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 475559aab1..05d5b9fd53 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -1,11 +1,11 @@ -use {super::*, std::collections::BTreeSet}; +use super::*; #[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>, + pub runes: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub runic: Option, pub total: u64, @@ -28,16 +28,27 @@ pub(crate) fn run(wallet: Wallet) -> SubcommandResult { for (output, txout) in unspent_outputs { let rune_balances = wallet.get_runes_balances_for_output(output)?; - if inscription_outputs.contains(output) { + let is_ordinal = inscription_outputs.contains(output); + let is_runic = !rune_balances.is_empty(); + + if is_ordinal { ordinal += txout.value; - } else if !rune_balances.is_empty() { + } + + if is_runic { for (spaced_rune, pile) in rune_balances { - *runes.entry(spaced_rune.rune).or_default() += pile.amount; + *runes.entry(spaced_rune).or_default() += pile.amount; } runic += txout.value; - } else { + } + + if !is_ordinal && !is_runic { cardinal += txout.value; } + + if is_ordinal && is_runic { + eprintln!("warning: output {output} contains both inscriptions and runes"); + } } Ok(Some(Box::new(Output { diff --git a/src/subcommand/wallet/batch_command.rs b/src/subcommand/wallet/batch_command.rs new file mode 100644 index 0000000000..486789b298 --- /dev/null +++ b/src/subcommand/wallet/batch_command.rs @@ -0,0 +1,258 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(crate) struct Batch { + #[command(flatten)] + shared: SharedArgs, + #[arg( + long, + help = "Inscribe multiple inscriptions and rune defined in YAML ." + )] + pub(crate) batch: PathBuf, +} + +impl Batch { + pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { + let utxos = wallet.utxos(); + + let batchfile = batch::File::load(&self.batch)?; + + let parent_info = wallet.get_parent_info(batchfile.parent)?; + + let (inscriptions, reveal_satpoints, postages, destinations) = batchfile.inscriptions( + &wallet, + utxos, + parent_info.as_ref().map(|info| info.tx_out.value), + self.shared.compress, + )?; + + let mut locked_utxos = wallet.locked_utxos().clone(); + + locked_utxos.extend( + reveal_satpoints + .iter() + .map(|(satpoint, txout)| (satpoint.outpoint, txout.clone())), + ); + + if let Some(etching) = batchfile.etching { + Self::check_etching(&wallet, &etching)?; + } + + batch::Plan { + commit_fee_rate: self.shared.commit_fee_rate.unwrap_or(self.shared.fee_rate), + destinations, + dry_run: self.shared.dry_run, + etching: batchfile.etching, + inscriptions, + mode: batchfile.mode, + no_backup: self.shared.no_backup, + no_limit: self.shared.no_limit, + parent_info, + postages, + reinscribe: batchfile.reinscribe, + reveal_fee_rate: self.shared.fee_rate, + reveal_satpoints, + satpoint: if let Some(sat) = batchfile.sat { + Some(wallet.find_sat_in_outputs(sat)?) + } else { + batchfile.satpoint + }, + } + .inscribe( + &locked_utxos.into_keys().collect(), + wallet.get_runic_outputs()?, + utxos, + &wallet, + ) + } + + fn check_etching(wallet: &Wallet, etching: &batch::Etching) -> Result { + let rune = etching.rune.rune; + + ensure!( + wallet.load_etching(rune)?.is_none(), + "rune `{rune}` has pending etching, resume with `ord wallet resume`" + ); + + ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); + + ensure!( + etching.divisibility <= Etching::MAX_DIVISIBILITY, + " must be less than or equal 38" + ); + + ensure!( + wallet.has_rune_index(), + "etching runes requires index created with `--index-runes`", + ); + + ensure!( + wallet.get_rune(rune)?.is_none(), + "rune `{rune}` has already been etched", + ); + + let premine = etching.premine.to_integer(etching.divisibility)?; + + let supply = etching.supply.to_integer(etching.divisibility)?; + + let mintable = etching + .terms + .map(|terms| -> Result { + terms + .cap + .checked_mul(terms.amount.to_integer(etching.divisibility)?) + .ok_or_else(|| anyhow!("`terms.count` * `terms.amount` over maximum")) + }) + .transpose()? + .unwrap_or_default(); + + ensure!( + supply + == premine + .checked_add(mintable) + .ok_or_else(|| anyhow!("`premine` + `terms.count` * `terms.amount` over maximum"))?, + "`supply` not equal to `premine` + `terms.count` * `terms.amount`" + ); + + ensure!(supply > 0, "`supply` must be greater than zero"); + + let bitcoin_client = wallet.bitcoin_client(); + + let current_height = u32::try_from(bitcoin_client.get_block_count()?).unwrap(); + + let reveal_height = current_height + 1 + u32::from(Runestone::COMMIT_INTERVAL); + + if let Some(terms) = etching.terms { + if let Some((start, end)) = terms.offset.and_then(|range| range.start.zip(range.end)) { + ensure!( + end > start, + "`terms.offset.end` must be greater than `terms.offset.start`" + ); + } + + if let Some((start, end)) = terms.height.and_then(|range| range.start.zip(range.end)) { + ensure!( + end > start, + "`terms.height.end` must be greater than `terms.height.start`" + ); + } + + if let Some(end) = terms.height.and_then(|range| range.end) { + ensure!( + end > reveal_height.into(), + "`terms.height.end` must be greater than the reveal transaction block height of {reveal_height}" + ); + } + + if let Some(start) = terms.height.and_then(|range| range.start) { + ensure!( + start > reveal_height.into(), + "`terms.height.start` must be greater than the reveal transaction block height of {reveal_height}" + ); + } + + ensure!(terms.cap > 0, "`terms.cap` must be greater than zero"); + + ensure!( + terms.amount.to_integer(etching.divisibility)? > 0, + "`terms.amount` must be greater than zero", + ); + } + + let minimum = Rune::minimum_at_height(wallet.chain().into(), Height(reveal_height)); + + ensure!( + rune >= minimum, + "rune is less than minimum for next block: {rune} < {minimum}", + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::wallet::batch, + serde_yaml::{Mapping, Value}, + tempfile::TempDir, + }; + + #[test] + fn batch_is_loaded_from_yaml_file() { + let parent = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0" + .parse::() + .unwrap(); + + let tempdir = TempDir::new().unwrap(); + + let inscription_path = tempdir.path().join("tulip.txt"); + fs::write(&inscription_path, "tulips are pretty").unwrap(); + + let brc20_path = tempdir.path().join("token.json"); + + let batch_path = tempdir.path().join("batch.yaml"); + fs::write( + &batch_path, + format!( + "mode: separate-outputs +parent: {parent} +inscriptions: +- file: {} + metadata: + title: Lorem Ipsum + description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante. +- file: {} + metaprotocol: brc-20 +", + inscription_path.display(), + brc20_path.display() + ), + ) + .unwrap(); + + let mut metadata = Mapping::new(); + metadata.insert( + Value::String("title".to_string()), + Value::String("Lorem Ipsum".to_string()), + ); + metadata.insert(Value::String("description".to_string()), Value::String("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante.".to_string())); + + assert_eq!( + batch::File::load(&batch_path).unwrap(), + batch::File { + inscriptions: vec![ + batch::Entry { + file: inscription_path, + metadata: Some(Value::Mapping(metadata)), + ..default() + }, + batch::Entry { + file: brc20_path, + metaprotocol: Some("brc-20".to_string()), + ..default() + } + ], + parent: Some(parent), + ..default() + } + ); + } + + #[test] + fn batch_with_unknown_field_throws_error() { + let tempdir = TempDir::new().unwrap(); + let batch_path = tempdir.path().join("batch.yaml"); + fs::write( + &batch_path, + "mode: shared-output\ninscriptions:\n- file: meow.wav\nunknown: 1.)what", + ) + .unwrap(); + + assert!(batch::File::load(&batch_path) + .unwrap_err() + .to_string() + .contains("unknown field `unknown`")); + } +} diff --git a/src/subcommand/wallet/cardinals.rs b/src/subcommand/wallet/cardinals.rs index d441a94831..1da96c4b67 100644 --- a/src/subcommand/wallet/cardinals.rs +++ b/src/subcommand/wallet/cardinals.rs @@ -1,4 +1,4 @@ -use {super::*, std::collections::BTreeSet}; +use super::*; #[derive(Serialize, Deserialize)] pub struct CardinalUtxo { diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs deleted file mode 100644 index cb2c8cd088..0000000000 --- a/src/subcommand/wallet/etch.rs +++ /dev/null @@ -1,126 +0,0 @@ -use super::*; - -#[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 . May contain `.` or `•`as spacers.")] - rune: SpacedRune, - #[clap(long, help = "Set supply to .")] - supply: Decimal, - #[clap(long, help = "Set currency symbol to .")] - symbol: char, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Output { - pub rune: SpacedRune, - pub transaction: Txid, -} - -impl Etch { - pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { - ensure!( - wallet.has_rune_index(), - "`ord wallet etch` requires index created with `--index-runes` flag", - ); - - let SpacedRune { rune, spacers } = self.rune; - - let bitcoin_client = wallet.bitcoin_client(); - - let count = bitcoin_client.get_block_count()?; - - ensure!( - wallet.get_rune(rune)?.is_none(), - "rune `{}` has already been etched", - rune, - ); - - let minimum_at_height = - Rune::minimum_at_height(wallet.chain(), Height(u32::try_from(count).unwrap() + 1)); - - ensure!( - rune >= minimum_at_height, - "rune is less than minimum for next block: {} < {minimum_at_height}", - rune, - ); - - ensure!(!rune.is_reserved(), "rune `{}` is reserved", rune); - - ensure!( - self.divisibility <= crate::runes::MAX_DIVISIBILITY, - " must be equal to or less than 38" - ); - - let destination = wallet.get_change_address()?; - - let runestone = Runestone { - etching: Some(Etching { - divisibility: self.divisibility, - mint: None, - rune: Some(rune), - spacers, - symbol: Some(self.symbol), - }), - edicts: vec![Edict { - amount: self.supply.to_amount(self.divisibility)?, - id: 0, - output: 1, - }], - default_output: None, - burn: false, - claim: None, - }; - - 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: 2, - 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 inscriptions = wallet - .inscriptions() - .keys() - .map(|satpoint| satpoint.outpoint) - .collect::>(); - - if !bitcoin_client.lock_unspent(&inscriptions)? { - bail!("failed to lock UTXOs"); - } - - let unsigned_transaction = - fund_raw_transaction(bitcoin_client, self.fee_rate, &unfunded_transaction)?; - - let signed_transaction = bitcoin_client - .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? - .hex; - - let transaction = bitcoin_client.send_raw_transaction(&signed_transaction)?; - - Ok(Some(Box::new(Output { - rune: self.rune, - transaction, - }))) - } -} diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 6940603cb8..25ae0e51b5 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,44 +1,21 @@ use super::*; #[derive(Debug, Parser)] -#[clap( - group = ArgGroup::new("source") - .required(true) - .args(&["file", "batch"]), -)] pub(crate) struct Inscribe { - #[arg( - long, - help = "Inscribe multiple inscriptions defined in a yaml .", - conflicts_with_all = &[ - "cbor_metadata", "delegate", "destination", "file", "json_metadata", "metaprotocol", - "parent", "postage", "reinscribe", "sat", "satpoint" - ] - )] - pub(crate) batch: Option, + #[command(flatten)] + shared: SharedArgs, #[arg( long, help = "Include CBOR in file at as inscription metadata", conflicts_with = "json_metadata" )] pub(crate) cbor_metadata: Option, - #[arg( - long, - help = "Use sats/vbyte for commit transaction.\nDefaults to if unset." - )] - pub(crate) commit_fee_rate: Option, - #[arg(long, help = "Compress inscription content with brotli.")] - pub(crate) compress: bool, #[arg(long, help = "Delegate inscription content to .")] pub(crate) delegate: Option, #[arg(long, help = "Send inscription to .")] pub(crate) destination: Option>, - #[arg(long, help = "Don't sign or broadcast transactions.")] - pub(crate) dry_run: bool, - #[arg(long, help = "Use fee rate of sats/vB.")] - pub(crate) fee_rate: FeeRate, #[arg(long, help = "Inscribe sat with contents of .")] - pub(crate) file: Option, + pub(crate) file: PathBuf, #[arg( long, help = "Include JSON in file at converted to CBOR as inscription metadata", @@ -47,14 +24,6 @@ pub(crate) struct Inscribe { pub(crate) json_metadata: Option, #[clap(long, help = "Set inscription metaprotocol to .")] pub(crate) metaprotocol: Option, - #[arg(long, alias = "nobackup", help = "Do not back up recovery key.")] - pub(crate) no_backup: bool, - #[arg( - long, - alias = "nolimit", - help = "Do not check that transactions are equal to or below the MAX_STANDARD_TX_WEIGHT of 400,000 weight units. Transactions over this limit are currently nonstandard and will not be relayed by bitcoind in its default configuration. Do not use this flag unless you understand the implications." - )] - pub(crate) no_limit: bool, #[clap(long, help = "Make inscription a child of .")] pub(crate) parent: Option, #[arg( @@ -72,115 +41,52 @@ pub(crate) struct Inscribe { impl Inscribe { pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { - let metadata = Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?; - - let utxos = wallet.utxos(); - - let mut locked_utxos = wallet.locked_utxos().clone(); - - let runic_utxos = wallet.get_runic_outputs()?; - let chain = wallet.chain(); - let postages; - let destinations; - let inscriptions; - let mode; - let parent_info; - let reinscribe; - let reveal_satpoints; - - let satpoint = match (self.file, self.batch) { - (Some(file), None) => { - parent_info = wallet.get_parent_info(self.parent)?; - - postages = vec![self.postage.unwrap_or(TARGET_POSTAGE)]; - - if let Some(delegate) = self.delegate { - ensure! { - wallet.inscription_exists(delegate)?, - "delegate {delegate} does not exist" - } - } - - inscriptions = vec![Inscription::from_file( - chain, - self.compress, - self.delegate, - metadata, - self.metaprotocol, - self.parent, - file, - None, - )?]; - - mode = Mode::SeparateOutputs; - - reinscribe = self.reinscribe; - - reveal_satpoints = Vec::new(); - - destinations = vec![match self.destination.clone() { - Some(destination) => destination.require_network(chain.network())?, - None => wallet.get_change_address()?, - }]; - - if let Some(sat) = self.sat { - Some(wallet.find_sat_in_outputs(sat)?) - } else { - self.satpoint - } - } - (None, Some(batch)) => { - let batchfile = Batchfile::load(&batch)?; - - parent_info = wallet.get_parent_info(batchfile.parent)?; - - (inscriptions, reveal_satpoints, postages, destinations) = batchfile.inscriptions( - &wallet, - utxos, - parent_info.as_ref().map(|info| info.tx_out.value), - self.compress, - )?; - - locked_utxos.extend( - reveal_satpoints - .iter() - .map(|(satpoint, txout)| (satpoint.outpoint, txout.clone())), - ); - - mode = batchfile.mode; - - reinscribe = batchfile.reinscribe; - - if let Some(sat) = batchfile.sat { - Some(wallet.find_sat_in_outputs(sat)?) - } else { - batchfile.satpoint - } - } - _ => unreachable!(), - }; - - Batch { - commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), - destinations, - dry_run: self.dry_run, - inscriptions, - mode, - no_backup: self.no_backup, - no_limit: self.no_limit, - parent_info, - postages, - reinscribe, - reveal_fee_rate: self.fee_rate, - reveal_satpoints, - satpoint, + if let Some(delegate) = self.delegate { + ensure! { + wallet.inscription_exists(delegate)?, + "delegate {delegate} does not exist" + } + } + + batch::Plan { + commit_fee_rate: self.shared.commit_fee_rate.unwrap_or(self.shared.fee_rate), + destinations: vec![match self.destination.clone() { + Some(destination) => destination.require_network(chain.network())?, + None => wallet.get_change_address()?, + }], + dry_run: self.shared.dry_run, + etching: None, + inscriptions: vec![Inscription::from_file( + chain, + self.shared.compress, + self.delegate, + Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?, + self.metaprotocol, + self.parent.into_iter().collect(), + self.file, + None, + None, + )?], + mode: batch::Mode::SeparateOutputs, + no_backup: self.shared.no_backup, + no_limit: self.shared.no_limit, + parent_info: wallet.get_parent_info(self.parent)?, + postages: vec![self.postage.unwrap_or(TARGET_POSTAGE)], + reinscribe: self.reinscribe, + reveal_fee_rate: self.shared.fee_rate, + reveal_satpoints: Vec::new(), + satpoint: if let Some(sat) = self.sat { + Some(wallet.find_sat_in_outputs(sat)?) + } else { + self.satpoint + }, } .inscribe( - &locked_utxos.into_keys().collect(), - runic_utxos, - utxos, + &wallet.locked_utxos().clone().into_keys().collect(), + wallet.get_runic_outputs()?, + wallet.utxos(), &wallet, ) } @@ -194,7 +100,7 @@ impl Inscribe { Ok(Some(cbor)) } else if let Some(path) = json { let value: serde_json::Value = - serde_json::from_reader(File::open(path)?).context("failed to parse JSON metadata")?; + serde_json::from_reader(fs::File::open(path)?).context("failed to parse JSON metadata")?; let mut cbor = Vec::new(); ciborium::into_writer(&value, &mut cbor)?; @@ -207,491 +113,7 @@ impl Inscribe { #[cfg(test)] mod tests { - use { - super::*, - crate::wallet::inscribe::{BatchEntry, ParentInfo}, - bitcoin::policy::MAX_STANDARD_TX_WEIGHT, - serde_yaml::{Mapping, Value}, - tempfile::TempDir, - }; - - #[test] - fn reveal_transaction_pays_fee() { - let utxos = vec![(outpoint(1), tx_out(20000, address()))]; - let inscription = inscription("text/plain", "ord"); - let commit_address = change(0); - let reveal_address = recipient(); - let change = [commit_address, change(1)]; - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint: Some(satpoint(1, 0)), - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - change, - ) - .unwrap(); - - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_sign_loss)] - let fee = Amount::from_sat((1.0 * (reveal_tx.vsize() as f64)).ceil() as u64); - - assert_eq!( - reveal_tx.output[0].value, - 20000 - fee.to_sat() - (20000 - commit_tx.output[0].value), - ); - } - - #[test] - fn inscribe_transactions_opt_in_to_rbf() { - let utxos = vec![(outpoint(1), tx_out(20000, address()))]; - let inscription = inscription("text/plain", "ord"); - let commit_address = change(0); - let reveal_address = recipient(); - let change = [commit_address, change(1)]; - - let (commit_tx, reveal_tx, _, _) = Batch { - satpoint: Some(satpoint(1, 0)), - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - change, - ) - .unwrap(); - - assert!(commit_tx.is_explicitly_rbf()); - assert!(reveal_tx.is_explicitly_rbf()); - } - - #[test] - fn inscribe_with_no_satpoint_and_no_cardinal_utxos() { - let utxos = vec![(outpoint(1), tx_out(1000, address()))]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - let error = Batch { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - inscriptions, - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - ) - .unwrap_err() - .to_string(); - - assert!( - error.contains("wallet contains no cardinal utxos"), - "{}", - error - ); - } - - #[test] - fn inscribe_with_no_satpoint_and_enough_cardinal_utxos() { - let utxos = vec![ - (outpoint(1), tx_out(20_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - assert!(Batch { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - inscriptions, - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - ) - .is_ok()) - } - - #[test] - fn inscribe_with_custom_fee_rate() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - let fee_rate = 3.3; - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(commit_tx.vsize() + sig_vbytes) - .to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 20_000 - fee); - - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(reveal_tx.vsize()) - .to_sat(); - - assert_eq!( - reveal_tx.output[0].value, - 20_000 - fee - (20_000 - commit_tx.output[0].value), - ); - } - - #[test] - fn inscribe_with_parent() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - - let mut inscriptions = BTreeMap::new(); - let parent_inscription = inscription_id(1); - let parent_info = ParentInfo { - destination: change(3), - id: parent_inscription, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - inscriptions.insert(parent_info.location, vec![parent_inscription]); - - let child_inscription = InscriptionTemplate { - parent: Some(parent_inscription), - ..Default::default() - } - .into(); - - let commit_address = change(1); - let reveal_address = recipient(); - let fee_rate = 4.0; - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions: vec![child_inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(commit_tx.vsize() + sig_vbytes) - .to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 20_000 - fee); - - let sig_vbytes = 16; - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(reveal_tx.vsize() + sig_vbytes) - .to_sat(); - - assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - } - ); - } - - #[test] - fn inscribe_with_commit_fee_rate() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - let mut inscriptions = BTreeMap::new(); - inscriptions.insert( - SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - vec![inscription_id(1)], - ); - - let inscription = inscription("text/plain", "ord"); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - let commit_fee_rate = 3.3; - let fee_rate = 1.0; - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(commit_fee_rate).unwrap(), - reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = FeeRate::try_from(commit_fee_rate) - .unwrap() - .fee(commit_tx.vsize() + sig_vbytes) - .to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 20_000 - fee); - - let fee = FeeRate::try_from(fee_rate) - .unwrap() - .fee(reveal_tx.vsize()) - .to_sat(); - - assert_eq!( - reveal_tx.output[0].value, - 20_000 - fee - (20_000 - commit_tx.output[0].value), - ); - } - - #[test] - fn inscribe_over_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; - - let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - let error = Batch { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - ) - .unwrap_err() - .to_string(); - - assert!( - error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402799")), - "{}", - error - ); - } - - #[test] - fn inscribe_with_no_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; - - let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); - let satpoint = None; - let commit_address = change(0); - let reveal_address = recipient(); - - let (_commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint, - parent_info: None, - inscriptions: vec![inscription], - destinations: vec![reveal_address], - commit_fee_rate: FeeRate::try_from(1.0).unwrap(), - reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), - no_limit: true, - reinscribe: false, - postages: vec![TARGET_POSTAGE], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - BTreeMap::new(), - Chain::Mainnet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(1)], - ) - .unwrap(); - - assert!(reveal_tx.size() >= MAX_STANDARD_TX_WEIGHT as usize); - } + use super::*; #[test] fn cbor_and_json_metadata_flags_conflict() { @@ -713,731 +135,6 @@ mod tests { ); } - #[test] - fn batch_is_loaded_from_yaml_file() { - let parent = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0" - .parse::() - .unwrap(); - - let tempdir = TempDir::new().unwrap(); - - let inscription_path = tempdir.path().join("tulip.txt"); - fs::write(&inscription_path, "tulips are pretty").unwrap(); - - let brc20_path = tempdir.path().join("token.json"); - - let batch_path = tempdir.path().join("batch.yaml"); - fs::write( - &batch_path, - format!( - "mode: separate-outputs -parent: {parent} -inscriptions: -- file: {} - metadata: - title: Lorem Ipsum - description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante. -- file: {} - metaprotocol: brc-20 -", - inscription_path.display(), - brc20_path.display() - ), - ) - .unwrap(); - - let mut metadata = Mapping::new(); - metadata.insert( - Value::String("title".to_string()), - Value::String("Lorem Ipsum".to_string()), - ); - metadata.insert(Value::String("description".to_string()), Value::String("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante.".to_string())); - - assert_eq!( - Batchfile::load(&batch_path).unwrap(), - Batchfile { - inscriptions: vec![ - BatchEntry { - file: inscription_path, - metadata: Some(Value::Mapping(metadata)), - ..Default::default() - }, - BatchEntry { - file: brc20_path, - metaprotocol: Some("brc-20".to_string()), - ..Default::default() - } - ], - parent: Some(parent), - ..Default::default() - } - ); - } - - #[test] - fn batch_with_unknown_field_throws_error() { - let tempdir = TempDir::new().unwrap(); - let batch_path = tempdir.path().join("batch.yaml"); - fs::write( - &batch_path, - "mode: shared-output\ninscriptions:\n- file: meow.wav\nunknown: 1.)what", - ) - .unwrap(); - - assert!(Batchfile::load(&batch_path) - .unwrap_err() - .to_string() - .contains("unknown field `unknown`")); - } - - #[test] - fn batch_inscribe_with_parent() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(50_000, address())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let commit_address = change(1); - let reveal_addresses = vec![recipient()]; - - let inscriptions = vec![ - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - ]; - - let mode = Mode::SharedOutput; - - let fee_rate = 4.0.try_into().unwrap(); - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 50_000 - fee); - - let sig_vbytes = 16; - let fee = fee_rate.fee(reveal_tx.vsize() + sig_vbytes).to_sat(); - - assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - } - ); - } - - #[test] - fn batch_inscribe_satpoints_with_parent() { - let utxos = vec![ - (outpoint(1), tx_out(1_111, address())), - (outpoint(2), tx_out(2_222, address())), - (outpoint(3), tx_out(3_333, address())), - (outpoint(4), tx_out(10_000, address())), - (outpoint(5), tx_out(50_000, address())), - (outpoint(6), tx_out(60_000, address())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(4), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10_000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient(), recipient()]; - - let inscriptions = vec![ - InscriptionTemplate { - parent: Some(parent), - pointer: Some(10_000), - } - .into(), - InscriptionTemplate { - parent: Some(parent), - pointer: Some(11_111), - } - .into(), - InscriptionTemplate { - parent: Some(parent), - pointer: Some(13_3333), - } - .into(), - ]; - - let reveal_satpoints = utxos - .iter() - .take(3) - .map(|(outpoint, txout)| { - ( - SatPoint { - outpoint: *outpoint, - offset: 0, - }, - txout.clone(), - ) - }) - .collect::>(); - - let mode = Mode::SatPoints; - - let fee_rate = 1.0.try_into().unwrap(); - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - reveal_satpoints: reveal_satpoints.clone(), - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - postages: vec![ - Amount::from_sat(1_111), - Amount::from_sat(2_222), - Amount::from_sat(3_333), - ], - mode, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - reveal_satpoints - .iter() - .map(|(satpoint, _)| satpoint.outpoint) - .collect(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap(); - - let sig_vbytes = 17; - let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 50_000 - fee); - - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - } - ); - } - - #[test] - fn batch_inscribe_with_parent_not_enough_cardinals_utxos_fails() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(20_000, address())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let inscriptions = vec![ - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - ]; - - let commit_address = change(1); - let reveal_addresses = vec![recipient()]; - - let error = Batch { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: 4.0.try_into().unwrap(), - reveal_fee_rate: 4.0.try_into().unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap_err() - .to_string(); - - assert!(error.contains( - "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet." - )); - } - - #[test] - #[should_panic(expected = "invariant: shared-output has only one destination")] - fn batch_inscribe_with_inconsistent_reveal_addresses_panics() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(80_000, address())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let inscriptions = vec![ - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - ]; - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient()]; - - let _ = Batch { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: 4.0.try_into().unwrap(), - reveal_fee_rate: 4.0.try_into().unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000)], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ); - } - - #[test] - fn batch_inscribe_over_max_standard_tx_weight() { - let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; - - let wallet_inscriptions = BTreeMap::new(); - - let inscriptions = vec![ - inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), - inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), - inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), - ]; - - let commit_address = change(1); - let reveal_addresses = vec![recipient()]; - - let error = Batch { - satpoint: None, - parent_info: None, - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: 1.0.try_into().unwrap(), - reveal_fee_rate: 1.0.try_into().unwrap(), - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(30_000); 3], - mode: Mode::SharedOutput, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap_err() - .to_string(); - - assert!( - error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402841")), - "{}", - error - ); - } - - #[test] - fn batch_inscribe_into_separate_outputs() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(80_000, address())), - ]; - - let wallet_inscriptions = BTreeMap::new(); - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient(), recipient()]; - - let inscriptions = vec![ - inscription("text/plain", [b'O'; 100]), - inscription("text/plain", [b'O'; 111]), - inscription("text/plain", [b'O'; 222]), - ]; - - let mode = Mode::SeparateOutputs; - - let fee_rate = 4.0.try_into().unwrap(); - - let (_commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint: None, - parent_info: None, - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap(); - - assert_eq!(reveal_tx.output.len(), 3); - assert!(reveal_tx - .output - .iter() - .all(|output| output.value == TARGET_POSTAGE.to_sat())); - } - - #[test] - fn batch_inscribe_into_separate_outputs_with_parent() { - let utxos = vec![ - (outpoint(1), tx_out(10_000, address())), - (outpoint(2), tx_out(50_000, address())), - ]; - - let parent = inscription_id(1); - - let parent_info = ParentInfo { - destination: change(3), - id: parent, - location: SatPoint { - outpoint: outpoint(1), - offset: 0, - }, - tx_out: TxOut { - script_pubkey: change(0).script_pubkey(), - value: 10000, - }, - }; - - let mut wallet_inscriptions = BTreeMap::new(); - wallet_inscriptions.insert(parent_info.location, vec![parent]); - - let commit_address = change(1); - let reveal_addresses = vec![recipient(), recipient(), recipient()]; - - let inscriptions = vec![ - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - InscriptionTemplate { - parent: Some(parent), - ..Default::default() - } - .into(), - ]; - - let mode = Mode::SeparateOutputs; - - let fee_rate = 4.0.try_into().unwrap(); - - let (commit_tx, reveal_tx, _private_key, _) = Batch { - satpoint: None, - parent_info: Some(parent_info.clone()), - inscriptions, - destinations: reveal_addresses, - commit_fee_rate: fee_rate, - reveal_fee_rate: fee_rate, - no_limit: false, - reinscribe: false, - postages: vec![Amount::from_sat(10_000); 3], - mode, - ..Default::default() - } - .create_batch_inscription_transactions( - wallet_inscriptions, - Chain::Signet, - BTreeSet::new(), - BTreeSet::new(), - utxos.into_iter().collect(), - [commit_address, change(2)], - ) - .unwrap(); - - assert_eq!( - parent, - ParsedEnvelope::from_transaction(&reveal_tx)[0] - .payload - .parent() - .unwrap() - ); - assert_eq!( - parent, - ParsedEnvelope::from_transaction(&reveal_tx)[1] - .payload - .parent() - .unwrap() - ); - - let sig_vbytes = 17; - let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); - - let reveal_value = commit_tx - .output - .iter() - .map(|o| o.value) - .reduce(|acc, i| acc + i) - .unwrap(); - - assert_eq!(reveal_value, 50_000 - fee); - - assert_eq!( - reveal_tx.output[0].script_pubkey, - parent_info.destination.script_pubkey() - ); - assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); - pretty_assert_eq!( - reveal_tx.input[0], - TxIn { - previous_output: parent_info.location.outpoint, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - } - ); - } - - #[test] - fn example_batchfile_deserializes_successfully() { - Batchfile::load(Path::new("batch.yaml")).unwrap(); - } - - #[test] - fn flags_conflict_with_batch() { - for (flag, value) in [ - ("--file", Some("foo")), - ( - "--delegate", - Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bi0"), - ), - ( - "--destination", - Some("tb1qsgx55dp6gn53tsmyjjv4c2ye403hgxynxs0dnm"), - ), - ("--cbor-metadata", Some("foo")), - ("--json-metadata", Some("foo")), - ("--sat", Some("0")), - ( - "--satpoint", - Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0"), - ), - ("--reinscribe", None), - ("--metaprotocol", Some("foo")), - ( - "--parent", - Some("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33bi0"), - ), - ] { - let mut args = vec![ - "ord", - "wallet", - "inscribe", - "--fee-rate", - "1", - "--batch", - "foo.yaml", - flag, - ]; - - if let Some(value) = value { - args.push(value); - } - - assert!(Arguments::try_parse_from(args) - .unwrap_err() - .to_string() - .contains("the argument '--batch ' cannot be used with")); - } - } - - #[test] - fn batch_or_file_is_required() { - assert!( - Arguments::try_parse_from(["ord", "wallet", "inscribe", "--fee-rate", "1",]) - .unwrap_err() - .to_string() - .contains("error: the following required arguments were not provided:\n <--file |--batch >") - ); - } - #[test] fn satpoint_and_sat_flags_conflict() { assert_regex_match!( diff --git a/src/subcommand/wallet/mint.rs b/src/subcommand/wallet/mint.rs new file mode 100644 index 0000000000..069c108bf2 --- /dev/null +++ b/src/subcommand/wallet/mint.rs @@ -0,0 +1,98 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(crate) struct Mint { + #[clap(long, help = "Use sats/vbyte for mint transaction.")] + fee_rate: FeeRate, + #[clap(long, help = "Mint . May contain `.` or `•`as spacers.")] + rune: SpacedRune, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Output { + pub rune: SpacedRune, + pub pile: Pile, + pub mint: Txid, +} + +impl Mint { + pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { + ensure!( + wallet.has_rune_index(), + "`ord wallet etch` requires index created with `--index-runes` flag", + ); + + let rune = self.rune.rune; + + let bitcoin_client = wallet.bitcoin_client(); + + let block_height = bitcoin_client.get_block_count()?; + + let Some((id, rune_entry, _)) = wallet.get_rune(rune)? else { + bail!("rune {rune} has not been etched"); + }; + + let amount = rune_entry + .mintable(block_height) + .map_err(|err| anyhow!("rune {rune} {err}"))?; + + let destination = wallet.get_change_address()?; + + let runestone = Runestone { + mint: Some(id), + ..default() + }; + + 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: 2, + 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(), + }, + ], + }; + + wallet.lock_non_cardinal_outputs()?; + + let unsigned_transaction = + fund_raw_transaction(bitcoin_client, self.fee_rate, &unfunded_transaction)?; + + let signed_transaction = bitcoin_client + .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? + .hex; + + let signed_transaction = consensus::encode::deserialize(&signed_transaction)?; + + assert_eq!( + Runestone::decipher(&signed_transaction), + Some(Artifact::Runestone(runestone)), + ); + + let transaction = bitcoin_client.send_raw_transaction(&signed_transaction)?; + + Ok(Some(Box::new(Output { + rune: self.rune, + pile: Pile { + amount, + divisibility: rune_entry.divisibility, + symbol: rune_entry.symbol, + }, + mint: transaction, + }))) + } +} diff --git a/src/subcommand/wallet/receive.rs b/src/subcommand/wallet/receive.rs index 70d650f277..0087b92529 100644 --- a/src/subcommand/wallet/receive.rs +++ b/src/subcommand/wallet/receive.rs @@ -2,13 +2,27 @@ use super::*; #[derive(Deserialize, Serialize)] pub struct Output { - pub address: Address, + pub addresses: Vec>, } -pub(crate) fn run(wallet: Wallet) -> SubcommandResult { - let address = wallet - .bitcoin_client() - .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?; +#[derive(Debug, Parser)] +pub(crate) struct Receive { + #[arg(short, long, help = "Generate addresses.")] + number: Option, +} + +impl Receive { + pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { + let mut addresses: Vec> = Vec::new(); + + for _ in 0..self.number.unwrap_or(1) { + addresses.push( + wallet + .bitcoin_client() + .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?, + ); + } - Ok(Some(Box::new(Output { address }))) + Ok(Some(Box::new(Output { addresses }))) + } } diff --git a/src/subcommand/wallet/resume.rs b/src/subcommand/wallet/resume.rs new file mode 100644 index 0000000000..d844591b50 --- /dev/null +++ b/src/subcommand/wallet/resume.rs @@ -0,0 +1,18 @@ +use super::*; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct ResumeOutput { + pub etchings: Vec, +} + +pub(crate) fn run(wallet: Wallet) -> SubcommandResult { + let outputs: Result> = wallet + .pending_etchings()? + .into_iter() + .map(|(rune, entry)| { + wallet.wait_for_maturation(&rune, entry.commit, entry.reveal, entry.output) + }) + .collect(); + + outputs.map(|etchings| Some(Box::new(ResumeOutput { etchings }) as Box)) +} diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 5ffe971b3e..3a7fd5ae66 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,9 +1,4 @@ -use { - super::*, - crate::{outgoing::Outgoing, wallet::transaction_builder::Target}, - base64::Engine, - bitcoin::psbt::Psbt, -}; +use {super::*, crate::outgoing::Outgoing, base64::Engine, bitcoin::psbt::Psbt}; #[derive(Debug, Parser)] pub(crate) struct Send { @@ -66,6 +61,14 @@ impl Send { self.fee_rate, false, )?, + Outgoing::Sat(sat) => Self::create_unsigned_send_satpoint_transaction( + &wallet, + address, + wallet.find_sat_in_outputs(sat)?, + self.postage, + self.fee_rate, + true, + )?, }; let unspent_outputs = wallet.utxos(); @@ -107,63 +110,33 @@ impl Send { ) }; + let mut fee = 0; + for txin in unsigned_transaction.input.iter() { + let Some(txout) = unspent_outputs.get(&txin.previous_output) else { + panic!("input {} not found in utxos", txin.previous_output); + }; + fee += txout.value; + } + + for txout in unsigned_transaction.output.iter() { + fee = fee.checked_sub(txout.value).unwrap(); + } + Ok(Some(Box::new(Output { txid, psbt, outgoing: self.outgoing, - fee: unsigned_transaction - .input - .iter() - .map(|txin| unspent_outputs.get(&txin.previous_output).unwrap().value) - .sum::() - .checked_sub( - unsigned_transaction - .output - .iter() - .map(|txout| txout.value) - .sum::(), - ) - .unwrap(), + fee, }))) } - fn lock_non_cardinal_outputs( - bitcoin_client: &Client, - inscriptions: &BTreeMap>, - runic_outputs: &BTreeSet, - unspent_outputs: &BTreeMap, - ) -> Result { - let all_inscription_outputs = inscriptions - .keys() - .map(|satpoint| satpoint.outpoint) - .collect::>(); - - let locked_outputs = unspent_outputs - .keys() - .filter(|utxo| all_inscription_outputs.contains(utxo)) - .chain(runic_outputs.iter()) - .cloned() - .collect::>(); - - if !bitcoin_client.lock_unspent(&locked_outputs)? { - bail!("failed to lock UTXOs"); - } - - Ok(()) - } - fn create_unsigned_send_amount_transaction( wallet: &Wallet, destination: Address, amount: Amount, fee_rate: FeeRate, ) -> Result { - Self::lock_non_cardinal_outputs( - wallet.bitcoin_client(), - wallet.inscriptions(), - &wallet.get_runic_outputs()?, - wallet.utxos(), - )?; + wallet.lock_non_cardinal_outputs()?; let unfunded_transaction = Transaction { version: 2, @@ -243,23 +216,17 @@ impl Send { "sending runes with `ord send` requires index created with `--index-runes` flag", ); - let unspent_outputs = wallet.utxos(); let inscriptions = wallet.inscriptions(); let runic_outputs = wallet.get_runic_outputs()?; let bitcoin_client = wallet.bitcoin_client(); - Self::lock_non_cardinal_outputs( - bitcoin_client, - inscriptions, - &runic_outputs, - unspent_outputs, - )?; + wallet.lock_non_cardinal_outputs()?; let (id, entry, _parent) = wallet .get_rune(spaced_rune.rune)? .with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?; - let amount = decimal.to_amount(entry.divisibility)?; + let amount = decimal.to_integer(entry.divisibility)?; let inscribed_outputs = inscriptions .keys() @@ -274,7 +241,7 @@ impl Send { continue; } - let balance = wallet.get_rune_balance_in_output(&output, entry.rune)?; + let balance = wallet.get_rune_balance_in_output(&output, entry.spaced_rune.rune)?; if balance > 0 { input_runes += balance; @@ -300,10 +267,10 @@ impl Send { let runestone = Runestone { edicts: vec![Edict { amount, - id: id.into(), + id, output: 2, }], - ..Default::default() + ..default() }; let unfunded_transaction = Transaction { @@ -337,6 +304,13 @@ impl Send { let unsigned_transaction = fund_raw_transaction(bitcoin_client, fee_rate, &unfunded_transaction)?; - Ok(consensus::encode::deserialize(&unsigned_transaction)?) + let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?; + + assert_eq!( + Runestone::decipher(&unsigned_transaction), + Some(Artifact::Runestone(runestone)), + ); + + Ok(unsigned_transaction) } } diff --git a/src/subcommand/wallet/shared_args.rs b/src/subcommand/wallet/shared_args.rs new file mode 100644 index 0000000000..e9db3b5cb7 --- /dev/null +++ b/src/subcommand/wallet/shared_args.rs @@ -0,0 +1,24 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(super) struct SharedArgs { + #[arg( + long, + help = "Use sats/vbyte for commit transaction.\nDefaults to if unset." + )] + pub(crate) commit_fee_rate: Option, + #[arg(long, help = "Compress inscription content with brotli.")] + pub(crate) compress: bool, + #[arg(long, help = "Use fee rate of sats/vB.")] + pub(crate) fee_rate: FeeRate, + #[arg(long, help = "Don't sign or broadcast transactions.")] + pub(crate) dry_run: bool, + #[arg(long, alias = "nobackup", help = "Do not back up recovery key.")] + pub(crate) no_backup: bool, + #[arg( + long, + alias = "nolimit", + help = "Do not check that transactions are equal to or below the MAX_STANDARD_TX_WEIGHT of 400,000 weight units. Transactions over this limit are currently nonstandard and will not be relayed by bitcoind in its default configuration. Do not use this flag unless you understand the implications." + )] + pub(crate) no_limit: bool, +} diff --git a/src/templates.rs b/src/templates.rs index e0474c34a3..f01d722636 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,6 +1,7 @@ use {super::*, boilerplate::Boilerplate}; pub(crate) use { + crate::subcommand::server::ServerConfig, block::BlockHtml, children::ChildrenHtml, clock::ClockSvg, @@ -13,6 +14,7 @@ pub(crate) use { inscriptions_block::InscriptionsBlockHtml, metadata::MetadataHtml, output::OutputHtml, + parents::ParentsHtml, preview::{ PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, @@ -21,7 +23,6 @@ pub(crate) use { rare::RareTxt, rune_balances::RuneBalancesHtml, sat::SatHtml, - server_config::ServerConfig, }; pub use { @@ -42,6 +43,7 @@ pub mod inscriptions; mod inscriptions_block; mod metadata; pub mod output; +mod parents; mod preview; mod range; mod rare; @@ -76,7 +78,7 @@ where fn superscript(&self) -> String { if self.config.chain == Chain::Mainnet { - "beta".into() + "alpha".into() } else { self.config.chain.to_string() } @@ -92,10 +94,6 @@ pub(crate) trait PageContent: Display + 'static { { PageHtml::new(self, server_config) } - - fn preview_image_url(&self) -> Option> { - None - } } #[cfg(test)] @@ -124,7 +122,7 @@ mod tests { csp_origin: Some("https://signet.ordinals.com".into()), domain: Some("signet.ordinals.com".into()), index_sats: true, - ..Default::default() + ..default() }),), r" @@ -146,7 +144,7 @@ mod tests {
        -
        id
        -
        1{64}i1
        -
        parent
        +
        parents
        +
        + all +
        +
        id
        +
        1{64}i1
        preview
        link
        content
        @@ -258,7 +257,7 @@ mod tests {
        ethereum teleburn address
        0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059
        - " +" .unindent() ); } @@ -273,7 +272,7 @@ mod tests { id: inscription_id(1), number: 1, satpoint: satpoint(1, 0), - ..Default::default() + ..default() }, "

        Inscription 1

        @@ -335,7 +334,7 @@ mod tests { id: inscription_id(1), number: 1, satpoint: satpoint(1, 0), - ..Default::default() + ..default() }, "

        Inscription 1

        @@ -399,7 +398,7 @@ mod tests { rune: Rune(26), spacers: 1 }), - ..Default::default() + ..default() }, "

        Inscription 1

        @@ -426,7 +425,7 @@ mod tests { id: inscription_id(1), number: 1, satpoint: satpoint(1, 0), - ..Default::default() + ..default() }, "

        Inscription 1

        diff --git a/src/templates/output.rs b/src/templates/output.rs index a929d4b83d..ef13b6e2b8 100644 --- a/src/templates/output.rs +++ b/src/templates/output.rs @@ -207,7 +207,7 @@ mod tests { A•A - 1.1 + 1.1\u{A0}¤
        diff --git a/src/templates/parents.rs b/src/templates/parents.rs new file mode 100644 index 0000000000..e142c126d4 --- /dev/null +++ b/src/templates/parents.rs @@ -0,0 +1,71 @@ +use super::*; + +#[derive(Boilerplate)] +pub(crate) struct ParentsHtml { + pub(crate) id: InscriptionId, + pub(crate) number: i32, + pub(crate) parents: Vec, + pub(crate) prev_page: Option, + pub(crate) next_page: Option, +} + +impl PageContent for ParentsHtml { + fn title(&self) -> String { + format!("Inscription {} Parents", self.number) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn without_prev_and_next() { + assert_regex_match!( + ParentsHtml { + id: inscription_id(1), + number: 0, + parents: vec![inscription_id(2), inscription_id(3)], + prev_page: None, + next_page: None, + }, + " +

        Inscription 0 Parents

        +
        + + +
        + .* + prev + next + .* + " + .unindent() + ); + } + + #[test] + fn with_prev_and_next() { + assert_regex_match!( + ParentsHtml { + id: inscription_id(1), + number: 0, + parents: vec![inscription_id(2), inscription_id(3)], + next_page: Some(3), + prev_page: Some(1), + }, + " +

        Inscription 0 Parents

        +
        + + +
        + .* + + + .* + " + .unindent() + ); + } +} diff --git a/src/templates/rune.rs b/src/templates/rune.rs index 94282e0acf..e772ff83f1 100644 --- a/src/templates/rune.rs +++ b/src/templates/rune.rs @@ -4,51 +4,54 @@ use super::*; pub struct RuneHtml { pub entry: RuneEntry, pub id: RuneId, + pub mintable: bool, pub parent: Option, } impl PageContent for RuneHtml { fn title(&self) -> String { - format!("Rune {}", self.entry.spaced_rune()) + format!("Rune {}", self.entry.spaced_rune) } } #[cfg(test)] mod tests { - use {super::*, crate::runes::Rune}; + use super::*; #[test] fn display() { assert_regex_match!( RuneHtml { entry: RuneEntry { + block: 1, burned: 123456789123456789, divisibility: 9, etching: Txid::all_zeros(), mints: 100, - number: 25, - mint: Some(MintEntry { - end: Some(11), - limit: Some(1000000001), - deadline: Some(7), + terms: Some(Terms { + cap: Some(101), + offset: (None, None), + height: (Some(10), Some(11)), + amount: Some(1000000001), }), - rune: Rune(u128::MAX), - spacers: 1, - supply: 123456789123456789, + number: 25, + premine: 123456789, + spaced_rune: SpacedRune { + rune: Rune(u128::MAX), + spacers: 1 + }, symbol: Some('%'), timestamp: 0, }, - id: RuneId { - height: 10, - index: 9, - }, + id: RuneId { block: 10, tx: 9 }, + mintable: true, parent: Some(InscriptionId { txid: Txid::all_zeros(), index: 0, }), }, "

        B•CGDENLQRQWDSLRUGSNLBTMFIJAV

        - +.*.*
        number
        25
        @@ -56,27 +59,35 @@ mod tests {
        id
        10:9
        -
        etching block height
        +
        etching block
        10
        -
        etching transaction index
        +
        etching transaction
        9
        mint
        -
        deadline
        -
        +
        start
        +
        10
        end
        11
        -
        limit
        +
        amount
        1.000000001 %
        mints
        100
        +
        cap
        +
        101
        +
        remaining
        +
        1
        +
        mintable
        +
        true
        supply
        -
        123456789.123456789\u{00A0}%
        +
        100.123456889\u{A0}%
        +
        premine
        +
        0.123456789\u{A0}%
        burned
        -
        123456789.123456789\u{00A0}%
        +
        123456789.123456789\u{A0}%
        divisibility
        9
        symbol
        @@ -95,49 +106,30 @@ mod tests { assert_regex_match!( RuneHtml { entry: RuneEntry { + block: 0, burned: 123456789123456789, - mint: None, + terms: None, divisibility: 9, etching: Txid::all_zeros(), mints: 0, number: 25, - rune: Rune(u128::MAX), - spacers: 1, - supply: 123456789123456789, + premine: 0, + spaced_rune: SpacedRune { + rune: Rune(u128::MAX), + spacers: 1 + }, symbol: Some('%'), timestamp: 0, }, - id: RuneId { - height: 10, - index: 9, - }, + id: RuneId { block: 10, tx: 9 }, + mintable: false, parent: None, }, "

        B•CGDENLQRQWDSLRUGSNLBTMFIJAV

        -
        -
        number
        -
        25
        -
        timestamp
        -
        -
        id
        -
        10:9
        -
        etching block height
        -
        10
        -
        etching transaction index
        -
        9
        +
        .*
        mint
        no
        -
        supply
        -
        123456789.123456789\u{00A0}%
        -
        burned
        -
        123456789.123456789\u{00A0}%
        -
        divisibility
        -
        9
        -
        symbol
        -
        %
        -
        etching
        -
        0{64}
        -
        +.*
        " ); } @@ -147,64 +139,52 @@ mod tests { assert_regex_match!( RuneHtml { entry: RuneEntry { + block: 0, burned: 123456789123456789, - mint: Some(MintEntry { - deadline: None, - end: None, - limit: None, + terms: Some(Terms { + cap: None, + offset: (None, None), + height: (None, None), + amount: None, }), divisibility: 9, etching: Txid::all_zeros(), mints: 0, + premine: 0, number: 25, - rune: Rune(u128::MAX), - spacers: 1, - supply: 123456789123456789, + spaced_rune: SpacedRune { + rune: Rune(u128::MAX), + spacers: 1 + }, symbol: Some('%'), timestamp: 0, }, - id: RuneId { - height: 10, - index: 9, - }, + id: RuneId { block: 10, tx: 9 }, + mintable: false, parent: None, }, "

        B•CGDENLQRQWDSLRUGSNLBTMFIJAV

        -
        -
        number
        -
        25
        -
        timestamp
        -
        -
        id
        -
        10:9
        -
        etching block height
        -
        10
        -
        etching transaction index
        -
        9
        +
        .*
        mint
        -
        deadline
        +
        start
        none
        end
        none
        -
        limit
        +
        amount
        none
        mints
        0
        +
        cap
        +
        0
        +
        remaining
        +
        0
        +
        mintable
        +
        false
        -
        supply
        -
        123456789.123456789\u{00A0}%
        -
        burned
        -
        123456789.123456789\u{00A0}%
        -
        divisibility
        -
        9
        -
        symbol
        -
        %
        -
        etching
        -
        0{64}
        -
        +.*
        " ); } diff --git a/src/templates/rune_balances.rs b/src/templates/rune_balances.rs index 2ab7f4803e..0bd645bec9 100644 --- a/src/templates/rune_balances.rs +++ b/src/templates/rune_balances.rs @@ -2,7 +2,7 @@ use super::*; #[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)] pub struct RuneBalancesHtml { - pub balances: BTreeMap>, + pub balances: BTreeMap>, } impl PageContent for RuneBalancesHtml { @@ -19,27 +19,35 @@ mod tests { #[test] fn display_rune_balances() { - let balances: BTreeMap> = vec![ + let balances: BTreeMap> = vec![ ( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![( OutPoint { txid: txid(1), vout: 1, }, - 1000, + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('$'), + }, )] .into_iter() .collect(), ), ( - Rune(RUNE + 1), + SpacedRune::new(Rune(RUNE + 1), 0), vec![( OutPoint { txid: txid(2), vout: 2, }, - 12345678, + Pile { + amount: 12345678, + divisibility: 1, + symbol: Some('¢'), + }, )] .into_iter() .collect(), @@ -65,7 +73,7 @@ mod tests { 1{64}:1 - 1000 + 1000\u{A0}\\$ @@ -80,7 +88,7 @@ mod tests { 2{64}:2 - 12345678 + 1234567\\.8\u{A0}¢ diff --git a/src/templates/runes.rs b/src/templates/runes.rs index c8e9951e1a..1e616605fc 100644 --- a/src/templates/runes.rs +++ b/src/templates/runes.rs @@ -20,14 +20,13 @@ mod tests { assert_eq!( RunesHtml { entries: vec![( - RuneId { - height: 0, - index: 0, - }, + RuneId { block: 0, tx: 0 }, RuneEntry { - rune: Rune(26), - spacers: 1, - ..Default::default() + spaced_rune: SpacedRune { + rune: Rune(26), + spacers: 1 + }, + ..default() } )], } diff --git a/src/templates/sat.rs b/src/templates/sat.rs index fd1c5bab7d..b0428422ec 100644 --- a/src/templates/sat.rs +++ b/src/templates/sat.rs @@ -2,10 +2,10 @@ use super::*; #[derive(Boilerplate)] pub(crate) struct SatHtml { - pub(crate) sat: Sat, - pub(crate) satpoint: Option, pub(crate) blocktime: Blocktime, pub(crate) inscriptions: Vec, + pub(crate) sat: Sat, + pub(crate) satpoint: Option, } impl PageContent for SatHtml { @@ -39,8 +39,13 @@ mod tests {
        period
        0
        block
        0
        offset
        0
        -
        rarity
        mythic
        timestamp
        +
        rarity
        mythic
        +
        charms
        +
        + 🪙 + 🎃 +
        .* prev @@ -72,8 +77,12 @@ mod tests {
        period
        3437
        block
        6929999
        offset
        0
        -
        rarity
        uncommon
        timestamp
        +
        rarity
        uncommon
        +
        charms
        +
        + 🌱 +
        .* diff --git a/src/test.rs b/src/test.rs index 5e90c2c453..c4bea74599 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,42 +1,17 @@ pub(crate) use { super::*, bitcoin::{ - blockdata::{opcodes, script, script::PushBytesBuf}, + blockdata::script::{PushBytes, PushBytesBuf}, constants::COIN_VALUE, - ScriptBuf, Witness, + opcodes, WPubkeyHash, }, + mockcore::TransactionTemplate, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, tempfile::TempDir, - test_bitcoincore_rpc::TransactionTemplate, unindent::Unindent, }; -macro_rules! assert_regex_match { - ($value:expr, $pattern:expr $(,)?) => { - let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap(); - let string = $value.to_string(); - - if !regex.is_match(string.as_ref()) { - eprintln!("Regex did not match:"); - pretty_assert_eq!(regex.as_str(), string); - } - }; -} - -macro_rules! assert_matches { - ($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => { - match $expression { - $( $pattern )|+ $( if $guard )? => {} - left => panic!( - "assertion failed: (left ~= right)\n left: `{:?}`\n right: `{}`", - left, - stringify!($($pattern)|+ $(if $guard)?) - ), - } - } -} - pub(crate) fn txid(n: u64) -> Txid { let hex = format!("{n:x}"); @@ -103,16 +78,16 @@ pub(crate) fn tx_out(value: u64, address: Address) -> TxOut { #[derive(Default, Debug)] pub(crate) struct InscriptionTemplate { - pub(crate) parent: Option, + pub(crate) parents: Vec, pub(crate) pointer: Option, } impl From for Inscription { fn from(template: InscriptionTemplate) -> Self { Self { - parent: template.parent.map(|id| id.value()), + parents: template.parents.into_iter().map(|id| id.value()).collect(), pointer: template.pointer.map(Inscription::pointer_value), - ..Default::default() + ..default() } } } @@ -146,3 +121,11 @@ pub(crate) fn envelope(payload: &[&[u8]]) -> Witness { Witness::from_slice(&[script.into_bytes(), Vec::new()]) } + +pub(crate) fn default_address(chain: Chain) -> Address { + Address::from_script( + &ScriptBuf::new_v0_p2wpkh(&WPubkeyHash::all_zeros()), + chain.network(), + ) + .unwrap() +} diff --git a/src/wallet.rs b/src/wallet.rs index 88a0ea2541..b1ff75f0b3 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,27 +1,55 @@ use { super::*, base64::{self, Engine}, + batch::ParentInfo, bitcoin::secp256k1::{All, Secp256k1}, bitcoin::{ bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint}, psbt::Psbt, - Network, }, bitcoincore_rpc::bitcoincore_rpc_json::{Descriptor, ImportDescriptors, Timestamp}, + entry::{EtchingEntry, EtchingEntryValue}, fee_rate::FeeRate, futures::{ future::{self, FutureExt}, try_join, TryFutureExt, }, - inscribe::ParentInfo, + index::entry::Entry, + indicatif::{ProgressBar, ProgressStyle}, + log::log_enabled, miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, Wildcard}, - reqwest::{header, Url}, + redb::{Database, DatabaseError, ReadableTable, RepairSession, StorageError, TableDefinition}, + reqwest::header, + std::sync::Once, transaction_builder::TransactionBuilder, }; -pub mod inscribe; +pub mod batch; +pub mod entry; pub mod transaction_builder; +const SCHEMA_VERSION: u64 = 1; + +define_table! { RUNE_TO_ETCHING, u128, EtchingEntryValue } +define_table! { STATISTICS, u64, u64 } + +#[derive(Copy, Clone)] +pub(crate) enum Statistic { + Schema = 0, +} + +impl Statistic { + fn key(self) -> u64 { + self.into() + } +} + +impl From for u64 { + fn from(statistic: Statistic) -> Self { + statistic as u64 + } +} + #[derive(Clone)] struct OrdClient { url: Url, @@ -30,10 +58,9 @@ struct OrdClient { impl OrdClient { pub async fn get(&self, path: &str) -> Result { - let url = self.url.join(path)?; self .client - .get(url) + .get(self.url.join(path)?) .send() .map_err(|err| anyhow!(err)) .await @@ -41,7 +68,8 @@ impl OrdClient { } pub(crate) struct Wallet { - bitcoin_client: bitcoincore_rpc::Client, + bitcoin_client: Client, + database: Database, has_rune_index: bool, has_sat_index: bool, rpc_url: Url, @@ -61,7 +89,7 @@ impl Wallet { settings: Settings, rpc_url: Url, ) -> Result { - let mut headers = header::HeaderMap::new(); + let mut headers = HeaderMap::new(); headers.insert( header::ACCEPT, @@ -77,6 +105,8 @@ impl Wallet { ); } + let database = Self::open_database(&name, &settings)?; + let ord_client = reqwest::blocking::ClientBuilder::new() .default_headers(headers.clone()) .build()?; @@ -109,10 +139,16 @@ impl Wallet { if !no_sync { for i in 0.. { let response = async_ord_client.get("/blockcount").await?; - if response.text().await?.parse::().unwrap() >= chain_block_count { + if response + .text() + .await? + .parse::() + .expect("wallet failed to talk to server. Make sure `ord server` is running.") + >= chain_block_count + { break; } else if i == 20 { - bail!("wallet failed to synchronize with ord server"); + bail!("wallet failed to synchronize with `ord server` after {i} attempts"); } tokio::time::sleep(Duration::from_millis(50)).await; } @@ -173,6 +209,7 @@ impl Wallet { Ok(Wallet { bitcoin_client, + database, has_rune_index: status.rune_index, has_sat_index: status.sat_index, inscription_info, @@ -203,7 +240,7 @@ impl Wallet { Ok(output_json) } - fn get_utxos(bitcoin_client: &bitcoincore_rpc::Client) -> Result> { + fn get_utxos(bitcoin_client: &Client) -> Result> { Ok( bitcoin_client .list_unspent(None, None, None, None, None)? @@ -221,12 +258,10 @@ impl Wallet { ) } - fn get_locked_utxos( - bitcoin_client: &bitcoincore_rpc::Client, - ) -> Result> { + fn get_locked_utxos(bitcoin_client: &Client) -> Result> { #[derive(Deserialize)] pub(crate) struct JsonOutPoint { - txid: bitcoin::Txid, + txid: Txid, vout: u32, } @@ -319,7 +354,7 @@ impl Wallet { ))) } - pub(crate) fn bitcoin_client(&self) -> &bitcoincore_rpc::Client { + pub(crate) fn bitcoin_client(&self) -> &Client { &self.bitcoin_client } @@ -331,6 +366,35 @@ impl Wallet { &self.locked_utxos } + pub(crate) fn lock_non_cardinal_outputs(&self) -> Result { + let inscriptions = self + .inscriptions() + .keys() + .map(|satpoint| satpoint.outpoint) + .collect::>(); + + let locked = self + .locked_utxos() + .keys() + .cloned() + .collect::>(); + + let outputs = self + .utxos() + .keys() + .filter(|utxo| inscriptions.contains(utxo)) + .chain(self.get_runic_outputs()?.iter()) + .cloned() + .filter(|utxo| !locked.contains(utxo)) + .collect::>(); + + if !self.bitcoin_client().lock_unspent(&outputs)? { + bail!("failed to lock UTXOs"); + } + + Ok(()) + } + pub(crate) fn inscriptions(&self) -> &BTreeMap> { &self.inscriptions } @@ -473,6 +537,73 @@ impl Wallet { self.settings.chain() } + pub(crate) fn integration_test(&self) -> bool { + self.settings.integration_test() + } + + pub(crate) fn wait_for_maturation( + &self, + rune: &Rune, + commit: Transaction, + reveal: Transaction, + output: batch::Output, + ) -> Result { + eprintln!("Waiting for rune commitment {} to mature…", commit.txid()); + + self.save_etching(rune, &commit, &reveal, output.clone())?; + + loop { + if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) { + eprintln!("Suspending batch. Run `ord wallet resume` to continue."); + return Ok(output); + } + + let transaction = self + .bitcoin_client() + .get_transaction(&commit.txid(), Some(true)) + .into_option()?; + + if let Some(transaction) = transaction { + if u32::try_from(transaction.info.confirmations).unwrap() + < Runestone::COMMIT_INTERVAL.into() + { + continue; + } + } + + let tx_out = self + .bitcoin_client() + .get_tx_out(&commit.txid(), 0, Some(true))?; + + if let Some(tx_out) = tx_out { + if tx_out.confirmations >= Runestone::COMMIT_INTERVAL.into() { + break; + } + } + + if !self.integration_test() { + thread::sleep(Duration::from_secs(5)); + } + } + + match self.bitcoin_client().send_raw_transaction(&reveal) { + Ok(txid) => txid, + Err(err) => { + return Err(anyhow!( + "Failed to send reveal transaction: {err}\nCommit tx {} will be recovered once mined", + commit.txid() + )) + } + }; + + self.clear_etching(rune)?; + + Ok(batch::Output { + reveal_broadcast: true, + ..output + }) + } + fn check_descriptors(wallet_name: &str, descriptors: Vec) -> Result> { let tr = descriptors .iter() @@ -586,7 +717,7 @@ impl Wallet { let public_key = secret_key.to_public(secp)?; - let mut key_map = std::collections::HashMap::new(); + let mut key_map = HashMap::new(); key_map.insert(public_key.clone(), secret_key); let descriptor = miniscript::descriptor::Descriptor::new_tr(public_key, None)?; @@ -629,4 +760,154 @@ impl Wallet { version % 100 ) } + + pub(crate) fn open_database(wallet_name: &String, settings: &Settings) -> Result { + let path = settings.data_dir().join(format!("{wallet_name}.redb")); + + if let Err(err) = fs::create_dir_all(path.parent().unwrap()) { + bail!( + "failed to create data dir `{}`: {err}", + path.parent().unwrap().display() + ); + } + + let db_path = path.clone().to_owned(); + let once = Once::new(); + let progress_bar = Mutex::new(None); + let integration_test = settings.integration_test(); + + let repair_callback = move |progress: &mut RepairSession| { + once.call_once(|| { + println!( + "Wallet database file `{}` needs recovery. This can take some time.", + db_path.display() + ) + }); + + if !(cfg!(test) || log_enabled!(log::Level::Info) || integration_test) { + let mut guard = progress_bar.lock().unwrap(); + + let progress_bar = guard.get_or_insert_with(|| { + let progress_bar = ProgressBar::new(100); + progress_bar.set_style( + ProgressStyle::with_template("[repairing database] {wide_bar} {pos}/{len}").unwrap(), + ); + progress_bar + }); + + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + progress_bar.set_position((progress.progress() * 100.0) as u64); + } + }; + + let database = match Database::builder() + .set_repair_callback(repair_callback) + .open(&path) + { + Ok(database) => { + { + let schema_version = database + .begin_read()? + .open_table(STATISTICS)? + .get(&Statistic::Schema.key())? + .map(|x| x.value()) + .unwrap_or(0); + + match schema_version.cmp(&SCHEMA_VERSION) { + cmp::Ordering::Less => + bail!( + "wallet database at `{}` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema {schema_version}, ord schema {SCHEMA_VERSION}", + path.display() + ), + cmp::Ordering::Greater => + bail!( + "wallet database at `{}` appears to have been built with a newer, incompatible version of ord, consider updating ord: index schema {schema_version}, ord schema {SCHEMA_VERSION}", + path.display() + ), + cmp::Ordering::Equal => { + } + } + } + + database + } + Err(DatabaseError::Storage(StorageError::Io(error))) + if error.kind() == io::ErrorKind::NotFound => + { + let database = Database::builder().create(&path)?; + + let tx = database.begin_write()?; + + tx.open_table(RUNE_TO_ETCHING)?; + + tx.open_table(STATISTICS)? + .insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; + + tx.commit()?; + + database + } + Err(error) => bail!("failed to open wallet database: {error}"), + }; + + Ok(database) + } + + pub(crate) fn save_etching( + &self, + rune: &Rune, + commit: &Transaction, + reveal: &Transaction, + output: batch::Output, + ) -> Result { + let wtx = self.database.begin_write()?; + + wtx.open_table(RUNE_TO_ETCHING)?.insert( + rune.0, + EtchingEntry { + commit: commit.clone(), + reveal: reveal.clone(), + output, + } + .store(), + )?; + + wtx.commit()?; + + Ok(()) + } + + pub(crate) fn load_etching(&self, rune: Rune) -> Result> { + let rtx = self.database.begin_read()?; + + Ok( + rtx + .open_table(RUNE_TO_ETCHING)? + .get(rune.0)? + .map(|result| EtchingEntry::load(result.value())), + ) + } + + pub(crate) fn clear_etching(&self, rune: &Rune) -> Result { + let wtx = self.database.begin_write()?; + + wtx.open_table(RUNE_TO_ETCHING)?.remove(rune.0)?; + wtx.commit()?; + + Ok(()) + } + + pub(crate) fn pending_etchings(&self) -> Result> { + let rtx = self.database.begin_read()?; + + Ok( + rtx + .open_table(RUNE_TO_ETCHING)? + .iter()? + .map(|result| { + result.map(|(key, value)| (Rune(key.value()), EtchingEntry::load(value.value()))) + }) + .collect::, StorageError>>()?, + ) + } } diff --git a/src/wallet/batch.rs b/src/wallet/batch.rs new file mode 100644 index 0000000000..9e8e71dc9a --- /dev/null +++ b/src/wallet/batch.rs @@ -0,0 +1,1183 @@ +use { + super::*, + bitcoin::{ + blockdata::{opcodes, script}, + key::PrivateKey, + key::{TapTweak, TweakedKeyPair, TweakedPublicKey, UntweakedKeyPair}, + policy::MAX_STANDARD_TX_WEIGHT, + secp256k1::{self, constants::SCHNORR_SIGNATURE_SIZE, rand, Secp256k1, XOnlyPublicKey}, + sighash::{Prevouts, SighashCache, TapSighashType}, + taproot::Signature, + taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder}, + }, + bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, SignRawTransactionInput, Timestamp}, + wallet::transaction_builder::Target, +}; + +pub(crate) use transactions::Transactions; + +pub use { + entry::Entry, etching::Etching, file::File, mode::Mode, plan::Plan, range::Range, terms::Terms, +}; + +pub mod entry; +mod etching; +pub mod file; +pub mod mode; +pub mod plan; +mod range; +mod terms; +mod transactions; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Output { + pub commit: Txid, + pub commit_psbt: Option, + pub inscriptions: Vec, + pub parent: Option, + pub reveal: Txid, + pub reveal_broadcast: bool, + pub reveal_psbt: Option, + pub rune: Option, + pub total_fees: u64, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct InscriptionInfo { + pub destination: Address, + pub id: InscriptionId, + pub location: SatPoint, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct RuneInfo { + pub destination: Option>, + pub location: Option, + pub rune: SpacedRune, +} + +#[derive(Clone, Debug)] +pub struct ParentInfo { + pub destination: Address, + pub id: InscriptionId, + pub location: SatPoint, + pub tx_out: TxOut, +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::wallet::batch::{self, ParentInfo}, + bitcoin::policy::MAX_STANDARD_TX_WEIGHT, + }; + + #[test] + fn reveal_transaction_pays_fee() { + let utxos = vec![(outpoint(1), tx_out(20000, address()))]; + let inscription = inscription("text/plain", "ord"); + let commit_address = change(0); + let reveal_address = recipient(); + let reveal_change = [commit_address, change(1)]; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: Some(satpoint(1, 0)), + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + reveal_change, + change(2), + ) + .unwrap(); + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + let fee = Amount::from_sat((1.0 * (reveal_tx.vsize() as f64)).ceil() as u64); + + assert_eq!( + reveal_tx.output[0].value, + 20000 - fee.to_sat() - (20000 - commit_tx.output[0].value), + ); + } + + #[test] + fn inscribe_transactions_opt_in_to_rbf() { + let utxos = vec![(outpoint(1), tx_out(20000, address()))]; + let inscription = inscription("text/plain", "ord"); + let commit_address = change(0); + let reveal_address = recipient(); + let reveal_change = [commit_address, change(1)]; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: Some(satpoint(1, 0)), + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + reveal_change, + change(2), + ) + .unwrap(); + + assert!(commit_tx.is_explicitly_rbf()); + assert!(reveal_tx.is_explicitly_rbf()); + } + + #[test] + fn inscribe_with_no_satpoint_and_no_cardinal_utxos() { + let utxos = vec![(outpoint(1), tx_out(1000, address()))]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + let error = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap_err() + .to_string(); + + assert!( + error.contains("wallet contains no cardinal utxos"), + "{}", + error + ); + } + + #[test] + fn inscribe_with_no_satpoint_and_enough_cardinal_utxos() { + let utxos = vec![ + (outpoint(1), tx_out(20_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + assert!(batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .is_ok()) + } + + #[test] + fn inscribe_with_custom_fee_rate() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + let fee_rate = 3.3; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(commit_tx.vsize() + sig_vbytes) + .to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 20_000 - fee); + + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize()) + .to_sat(); + + assert_eq!( + reveal_tx.output[0].value, + 20_000 - fee - (20_000 - commit_tx.output[0].value), + ); + } + + #[test] + fn inscribe_with_parent() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + + let mut inscriptions = BTreeMap::new(); + let parent_inscription = inscription_id(1); + let parent_info = ParentInfo { + destination: change(3), + id: parent_inscription, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + inscriptions.insert(parent_info.location, vec![parent_inscription]); + + let child_inscription = InscriptionTemplate { + parents: vec![parent_inscription], + ..default() + } + .into(); + + let commit_address = change(1); + let reveal_address = recipient(); + let fee_rate = 4.0; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions: vec![child_inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(1), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(commit_tx.vsize() + sig_vbytes) + .to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 20_000 - fee); + + let sig_vbytes = 16; + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize() + sig_vbytes) + .to_sat(); + + assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } + + #[test] + fn inscribe_with_commit_fee_rate() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + vec![inscription_id(1)], + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + let commit_fee_rate = 3.3; + let fee_rate = 1.0; + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(commit_fee_rate).unwrap(), + reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = FeeRate::try_from(commit_fee_rate) + .unwrap() + .fee(commit_tx.vsize() + sig_vbytes) + .to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 20_000 - fee); + + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize()) + .to_sat(); + + assert_eq!( + reveal_tx.output[0].value, + 20_000 - fee - (20_000 - commit_tx.output[0].value), + ); + } + + #[test] + fn inscribe_over_max_standard_tx_weight() { + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + + let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + let error = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap_err() + .to_string(); + + assert!( + error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402799")), + "{}", + error + ); + } + + #[test] + fn inscribe_with_no_max_standard_tx_weight() { + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + + let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + + let batch::Transactions { reveal_tx, .. } = batch::Plan { + satpoint, + parent_info: None, + inscriptions: vec![inscription], + destinations: vec![reveal_address], + commit_fee_rate: FeeRate::try_from(1.0).unwrap(), + reveal_fee_rate: FeeRate::try_from(1.0).unwrap(), + no_limit: true, + reinscribe: false, + postages: vec![TARGET_POSTAGE], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + BTreeMap::new(), + Chain::Mainnet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(1)], + change(2), + ) + .unwrap(); + + assert!(reveal_tx.size() >= MAX_STANDARD_TX_WEIGHT as usize); + } + + #[test] + fn batch_inscribe_with_parent() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(50_000, address())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let commit_address = change(1); + let reveal_addresses = vec![recipient()]; + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let mode = batch::Mode::SharedOutput; + + let fee_rate = 4.0.try_into().unwrap(); + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(2), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 50_000 - fee); + + let sig_vbytes = 16; + let fee = fee_rate.fee(reveal_tx.vsize() + sig_vbytes).to_sat(); + + assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value,); + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } + + #[test] + fn batch_inscribe_satpoints_with_parent() { + let utxos = vec![ + (outpoint(1), tx_out(1_111, address())), + (outpoint(2), tx_out(2_222, address())), + (outpoint(3), tx_out(3_333, address())), + (outpoint(4), tx_out(10_000, address())), + (outpoint(5), tx_out(50_000, address())), + (outpoint(6), tx_out(60_000, address())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(4), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10_000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient(), recipient()]; + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + pointer: Some(10_000), + } + .into(), + InscriptionTemplate { + parents: vec![parent], + pointer: Some(11_111), + } + .into(), + InscriptionTemplate { + parents: vec![parent], + pointer: Some(13_3333), + } + .into(), + ]; + + let reveal_satpoints = utxos + .iter() + .take(3) + .map(|(outpoint, txout)| { + ( + SatPoint { + outpoint: *outpoint, + offset: 0, + }, + txout.clone(), + ) + }) + .collect::>(); + + let mode = batch::Mode::SatPoints; + + let fee_rate = 1.0.try_into().unwrap(); + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + reveal_satpoints: reveal_satpoints.clone(), + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + postages: vec![ + Amount::from_sat(1_111), + Amount::from_sat(2_222), + Amount::from_sat(3_333), + ], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + reveal_satpoints + .iter() + .map(|(satpoint, _)| satpoint.outpoint) + .collect(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap(); + + let sig_vbytes = 17; + let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 50_000 - fee); + + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } + + #[test] + fn batch_inscribe_with_parent_not_enough_cardinals_utxos_fails() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(20_000, address())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let commit_address = change(1); + let reveal_addresses = vec![recipient()]; + + let error = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: 4.0.try_into().unwrap(), + reveal_fee_rate: 4.0.try_into().unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains( + "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet." + )); + } + + #[test] + #[should_panic(expected = "invariant: shared-output has only one destination")] + fn batch_inscribe_with_inconsistent_reveal_addresses_panics() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(80_000, address())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient()]; + + let _ = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: 4.0.try_into().unwrap(), + reveal_fee_rate: 4.0.try_into().unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000)], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ); + } + + #[test] + fn batch_inscribe_over_max_standard_tx_weight() { + let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address()))]; + + let wallet_inscriptions = BTreeMap::new(); + + let inscriptions = vec![ + inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), + inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), + inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]), + ]; + + let commit_address = change(1); + let reveal_addresses = vec![recipient()]; + + let error = batch::Plan { + satpoint: None, + parent_info: None, + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: 1.0.try_into().unwrap(), + reveal_fee_rate: 1.0.try_into().unwrap(), + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(30_000); 3], + mode: batch::Mode::SharedOutput, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap_err() + .to_string(); + + assert!( + error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402841")), + "{}", + error + ); + } + + #[test] + fn batch_inscribe_into_separate_outputs() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(80_000, address())), + ]; + + let wallet_inscriptions = BTreeMap::new(); + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient(), recipient()]; + + let inscriptions = vec![ + inscription("text/plain", [b'O'; 100]), + inscription("text/plain", [b'O'; 111]), + inscription("text/plain", [b'O'; 222]), + ]; + + let mode = batch::Mode::SeparateOutputs; + + let fee_rate = 4.0.try_into().unwrap(); + + let batch::Transactions { reveal_tx, .. } = batch::Plan { + satpoint: None, + parent_info: None, + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap(); + + assert_eq!(reveal_tx.output.len(), 3); + assert!(reveal_tx + .output + .iter() + .all(|output| output.value == TARGET_POSTAGE.to_sat())); + } + + #[test] + fn batch_inscribe_into_separate_outputs_with_parent() { + let utxos = vec![ + (outpoint(1), tx_out(10_000, address())), + (outpoint(2), tx_out(50_000, address())), + ]; + + let parent = inscription_id(1); + + let parent_info = ParentInfo { + destination: change(3), + id: parent, + location: SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + tx_out: TxOut { + script_pubkey: change(0).script_pubkey(), + value: 10000, + }, + }; + + let mut wallet_inscriptions = BTreeMap::new(); + wallet_inscriptions.insert(parent_info.location, vec![parent]); + + let commit_address = change(1); + let reveal_addresses = vec![recipient(), recipient(), recipient()]; + + let inscriptions = vec![ + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + InscriptionTemplate { + parents: vec![parent], + ..default() + } + .into(), + ]; + + let mode = batch::Mode::SeparateOutputs; + + let fee_rate = 4.0.try_into().unwrap(); + + let batch::Transactions { + commit_tx, + reveal_tx, + .. + } = batch::Plan { + satpoint: None, + parent_info: Some(parent_info.clone()), + inscriptions, + destinations: reveal_addresses, + commit_fee_rate: fee_rate, + reveal_fee_rate: fee_rate, + no_limit: false, + reinscribe: false, + postages: vec![Amount::from_sat(10_000); 3], + mode, + ..default() + } + .create_batch_transactions( + wallet_inscriptions, + Chain::Signet, + BTreeSet::new(), + BTreeSet::new(), + utxos.into_iter().collect(), + [commit_address, change(2)], + change(3), + ) + .unwrap(); + + assert_eq!( + vec![parent], + ParsedEnvelope::from_transaction(&reveal_tx)[0] + .payload + .parents(), + ); + assert_eq!( + vec![parent], + ParsedEnvelope::from_transaction(&reveal_tx)[1] + .payload + .parents(), + ); + + let sig_vbytes = 17; + let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat(); + + let reveal_value = commit_tx + .output + .iter() + .map(|o| o.value) + .reduce(|acc, i| acc + i) + .unwrap(); + + assert_eq!(reveal_value, 50_000 - fee); + + assert_eq!( + reveal_tx.output[0].script_pubkey, + parent_info.destination.script_pubkey() + ); + assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value); + pretty_assert_eq!( + reveal_tx.input[0], + TxIn { + previous_output: parent_info.location.outpoint, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..default() + } + ); + } +} diff --git a/src/wallet/batch/entry.rs b/src/wallet/batch/entry.rs new file mode 100644 index 0000000000..cba1bb537b --- /dev/null +++ b/src/wallet/batch/entry.rs @@ -0,0 +1,25 @@ +use super::*; + +#[derive(Serialize, Deserialize, Default, PartialEq, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct Entry { + pub delegate: Option, + pub destination: Option>, + pub file: PathBuf, + pub metadata: Option, + pub metaprotocol: Option, + pub satpoint: Option, +} + +impl Entry { + pub(crate) fn metadata(&self) -> Result>> { + Ok(match &self.metadata { + None => None, + Some(metadata) => { + let mut cbor = Vec::new(); + ciborium::into_writer(&metadata, &mut cbor)?; + Some(cbor) + } + }) + } +} diff --git a/src/wallet/batch/etching.rs b/src/wallet/batch/etching.rs new file mode 100644 index 0000000000..700e90eaf9 --- /dev/null +++ b/src/wallet/batch/etching.rs @@ -0,0 +1,12 @@ +use super::*; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct Etching { + pub divisibility: u8, + pub premine: Decimal, + pub rune: SpacedRune, + pub supply: Decimal, + pub symbol: char, + pub terms: Option, +} diff --git a/src/wallet/batch/file.rs b/src/wallet/batch/file.rs new file mode 100644 index 0000000000..68f8bae4f3 --- /dev/null +++ b/src/wallet/batch/file.rs @@ -0,0 +1,449 @@ +use super::*; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct File { + pub inscriptions: Vec, + pub mode: Mode, + pub parent: Option, + pub postage: Option, + #[serde(default)] + pub reinscribe: bool, + pub etching: Option, + pub sat: Option, + pub satpoint: Option, +} + +impl File { + pub(crate) fn load(path: &Path) -> Result { + let batchfile: Self = serde_yaml::from_reader(fs::File::open(path)?)?; + + ensure!( + !batchfile.inscriptions.is_empty(), + "batchfile must contain at least one inscription", + ); + + let sat_and_satpoint = batchfile.sat.is_some() && batchfile.satpoint.is_some(); + + ensure!( + !sat_and_satpoint, + "batchfile cannot set both `sat` and `satpoint`", + ); + + let sat_or_satpoint = batchfile.sat.is_some() || batchfile.satpoint.is_some(); + + if sat_or_satpoint { + ensure!( + batchfile.mode == Mode::SameSat, + "neither `sat` nor `satpoint` can be set in `same-sat` mode", + ); + } + + if batchfile + .inscriptions + .iter() + .any(|entry| entry.destination.is_some()) + && (batchfile.mode == Mode::SharedOutput || batchfile.mode == Mode::SameSat) + { + bail!( + "individual inscription destinations cannot be set in `shared-output` or `same-sat` mode" + ); + } + + let any_entry_has_satpoint = batchfile + .inscriptions + .iter() + .any(|entry| entry.satpoint.is_some()); + + if any_entry_has_satpoint { + ensure!( + batchfile.mode == Mode::SatPoints, + "specifying `satpoint` in an inscription only works in `satpoints` mode" + ); + + ensure!( + batchfile.inscriptions.iter().all(|entry| entry.satpoint.is_some()), + "if `satpoint` is set for any inscription, then all inscriptions need to specify a satpoint" + ); + + ensure!( + batchfile + .inscriptions + .iter() + .all(|entry| entry.satpoint.unwrap().offset == 0), + "`satpoint` can only be specified for first sat of an output" + ); + } + + if batchfile.mode == Mode::SatPoints { + ensure!( + batchfile.postage.is_none(), + "`postage` cannot be set if in `satpoints` mode" + ); + + ensure!( + batchfile.sat.is_none(), + "`sat` cannot be set if in `satpoints` mode" + ); + + ensure!( + batchfile.satpoint.is_none(), + "`satpoint cannot be set if in `satpoints` mode" + ); + + let mut seen = HashSet::new(); + for entry in batchfile.inscriptions.iter() { + let satpoint = entry.satpoint.unwrap_or_default(); + if !seen.insert(satpoint) { + bail!("duplicate satpoint {}", satpoint); + } + } + } + + Ok(batchfile) + } + + pub(crate) fn inscriptions( + &self, + wallet: &Wallet, + utxos: &BTreeMap, + parent_value: Option, + compress: bool, + ) -> Result<( + Vec, + Vec<(SatPoint, TxOut)>, + Vec, + Vec
        , + )> { + let mut inscriptions = Vec::new(); + let mut reveal_satpoints = Vec::new(); + let mut postages = Vec::new(); + + let mut pointer = parent_value.unwrap_or_default(); + + for (i, entry) in self.inscriptions.iter().enumerate() { + if let Some(delegate) = entry.delegate { + ensure! { + wallet.inscription_exists(delegate)?, + "delegate {delegate} does not exist" + } + } + + inscriptions.push(Inscription::from_file( + wallet.chain(), + compress, + entry.delegate, + entry.metadata()?, + entry.metaprotocol.clone(), + self.parent.into_iter().collect(), + &entry.file, + Some(pointer), + self + .etching + .and_then(|etch| (i == 0).then_some(etch.rune.rune)), + )?); + + let postage = if self.mode == Mode::SatPoints { + let satpoint = entry + .satpoint + .ok_or_else(|| anyhow!("no satpoint specified for {}", entry.file.display()))?; + + let txout = utxos + .get(&satpoint.outpoint) + .ok_or_else(|| anyhow!("{} not in wallet", satpoint))?; + + reveal_satpoints.push((satpoint, txout.clone())); + + txout.value + } else { + self + .postage + .map(Amount::from_sat) + .unwrap_or(TARGET_POSTAGE) + .to_sat() + }; + + pointer += postage; + + if self.mode == Mode::SameSat && i > 0 { + continue; + } else { + postages.push(Amount::from_sat(postage)); + } + } + + let destinations = match self.mode { + Mode::SharedOutput | Mode::SameSat => vec![wallet.get_change_address()?], + Mode::SeparateOutputs | Mode::SatPoints => self + .inscriptions + .iter() + .map(|entry| { + entry.destination.as_ref().map_or_else( + || wallet.get_change_address(), + |address| { + address + .clone() + .require_network(wallet.chain().network()) + .map_err(|e| e.into()) + }, + ) + }) + .collect::, _>>()?, + }; + + Ok((inscriptions, reveal_satpoints, postages, destinations)) + } +} + +#[cfg(test)] +mod tests { + use {super::*, pretty_assertions::assert_eq}; + + #[test] + fn batchfile_not_sat_and_satpoint() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: same-sat +sat: 55555 +satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0 +inscriptions: +- file: inscription.txt +- file: tulip.png +- file: meow.wav +"#, + ) + .unwrap(); + + assert_eq!( + File::load(batch_file.as_path()).unwrap_err().to_string(), + "batchfile cannot set both `sat` and `satpoint`" + ); + } + + #[test] + fn batchfile_wrong_mode_for_satpoints() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: separate-outputs +inscriptions: +- file: inscription.txt + satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0 +- file: tulip.png + satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0 +- file: meow.wav + satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0 +"#, + ) + .unwrap(); + + assert_eq!( + batch::File::load(batch_file.as_path()) + .unwrap_err() + .to_string(), + "specifying `satpoint` in an inscription only works in `satpoints` mode" + ); + } + + #[test] + fn batchfile_missing_satpoint() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: satpoints +inscriptions: +- file: inscription.txt + satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0 +- file: tulip.png +- file: meow.wav + satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0 +"#, + ) + .unwrap(); + + assert_eq!( + batch::File::load(batch_file.as_path()) + .unwrap_err() + .to_string(), + "if `satpoint` is set for any inscription, then all inscriptions need to specify a satpoint" + ); + } + + #[test] + fn batchfile_only_first_sat_of_outpoint() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: satpoints +inscriptions: +- file: inscription.txt + satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0 +- file: tulip.png + satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:21 +- file: meow.wav + satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0 +"#, + ) + .unwrap(); + + assert_eq!( + batch::File::load(batch_file.as_path()) + .unwrap_err() + .to_string(), + "`satpoint` can only be specified for first sat of an output" + ); + } + + #[test] + fn batchfile_no_postage_if_mode_satpoints() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: satpoints +postage: 1111 +inscriptions: +- file: inscription.txt + satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0 +- file: tulip.png + satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0 +- file: meow.wav + satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0 +"#, + ) + .unwrap(); + + assert_eq!( + batch::File::load(batch_file.as_path()) + .unwrap_err() + .to_string(), + "`postage` cannot be set if in `satpoints` mode" + ); + } + + #[test] + fn batchfile_no_duplicate_satpoints() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: satpoints +inscriptions: +- file: inscription.txt + satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0 +- file: tulip.png + satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0 +- file: meow.wav + satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0 +- file: inscription_1.txt + satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0 +"#, + ) + .unwrap(); + + assert_eq!( + batch::File::load(batch_file.as_path()) + .unwrap_err() + .to_string(), + "duplicate satpoint bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0" + ); + } + + #[test] + fn example_batchfile_deserializes_successfully() { + assert_eq!( + batch::File::load(Path::new("batch.yaml")).unwrap(), + batch::File { + mode: batch::Mode::SeparateOutputs, + parent: Some( + "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0" + .parse() + .unwrap() + ), + postage: Some(12345), + reinscribe: true, + sat: None, + satpoint: None, + etching: Some(Etching { + rune: "THE•BEST•RUNE".parse().unwrap(), + divisibility: 2, + premine: "1000.00".parse().unwrap(), + supply: "10000.00".parse().unwrap(), + symbol: '$', + terms: Some(batch::Terms { + amount: "100.00".parse().unwrap(), + cap: 90, + height: Some(batch::Range { + start: Some(840000), + end: Some(850000), + }), + offset: Some(batch::Range { + start: Some(1000), + end: Some(9000), + }), + }), + }), + inscriptions: vec![ + batch::Entry { + file: "mango.avif".into(), + delegate: Some( + "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0" + .parse() + .unwrap() + ), + destination: Some( + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + .parse() + .unwrap() + ), + metadata: Some(serde_yaml::Value::Mapping({ + let mut mapping = serde_yaml::Mapping::new(); + mapping.insert("title".into(), "Delicious Mangos".into()); + mapping.insert( + "description".into(), + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam semper, \ + ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel blandit \ + metus est et odio. Nullam venenatis, urna et molestie vestibulum, orci \ + mi efficitur risus, eu malesuada diam lorem sed velit. Nam fermentum \ + dolor et luctus euismod.\n" + .into(), + ); + mapping + })), + ..default() + }, + batch::Entry { + file: "token.json".into(), + metaprotocol: Some("DOPEPROTOCOL-42069".into()), + ..default() + }, + batch::Entry { + file: "tulip.png".into(), + destination: Some( + "bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6" + .parse() + .unwrap() + ), + metadata: Some(serde_yaml::Value::Mapping({ + let mut mapping = serde_yaml::Mapping::new(); + mapping.insert("author".into(), "Satoshi Nakamoto".into()); + mapping + })), + ..default() + }, + ], + } + ); + } +} diff --git a/src/wallet/batch/mode.rs b/src/wallet/batch/mode.rs new file mode 100644 index 0000000000..c77ca7234e --- /dev/null +++ b/src/wallet/batch/mode.rs @@ -0,0 +1,14 @@ +use super::*; + +#[derive(PartialEq, Debug, Copy, Clone, Serialize, Deserialize, Default)] +pub enum Mode { + #[serde(rename = "same-sat")] + SameSat, + #[serde(rename = "satpoints")] + SatPoints, + #[default] + #[serde(rename = "separate-outputs")] + SeparateOutputs, + #[serde(rename = "shared-output")] + SharedOutput, +} diff --git a/src/wallet/inscribe/batch.rs b/src/wallet/batch/plan.rs similarity index 71% rename from src/wallet/inscribe/batch.rs rename to src/wallet/batch/plan.rs index 1c27df1669..1f51127464 100644 --- a/src/wallet/inscribe/batch.rs +++ b/src/wallet/batch/plan.rs @@ -1,9 +1,10 @@ use super::*; -pub struct Batch { +pub struct Plan { pub(crate) commit_fee_rate: FeeRate, pub(crate) destinations: Vec
        , pub(crate) dry_run: bool, + pub(crate) etching: Option, pub(crate) inscriptions: Vec, pub(crate) mode: Mode, pub(crate) no_backup: bool, @@ -16,12 +17,13 @@ pub struct Batch { pub(crate) satpoint: Option, } -impl Default for Batch { +impl Default for Plan { fn default() -> Self { Self { commit_fee_rate: 1.0.try_into().unwrap(), destinations: Vec::new(), dry_run: false, + etching: None, inscriptions: Vec::new(), mode: Mode::SharedOutput, no_backup: false, @@ -36,7 +38,7 @@ impl Default for Batch { } } -impl Batch { +impl Plan { pub(crate) fn inscribe( &self, locked_utxos: &BTreeSet, @@ -44,17 +46,21 @@ impl Batch { utxos: &BTreeMap, wallet: &Wallet, ) -> SubcommandResult { - let commit_tx_change = [wallet.get_change_address()?, wallet.get_change_address()?]; - - let (commit_tx, reveal_tx, recovery_key_pair, total_fees) = self - .create_batch_inscription_transactions( - wallet.inscriptions().clone(), - wallet.chain(), - locked_utxos.clone(), - runic_utxos, - utxos.clone(), - commit_tx_change, - )?; + let Transactions { + commit_tx, + reveal_tx, + recovery_key_pair, + total_fees, + rune, + } = self.create_batch_transactions( + wallet.inscriptions().clone(), + wallet.chain(), + locked_utxos.clone(), + runic_utxos, + utxos.clone(), + [wallet.get_change_address()?, wallet.get_change_address()?], + wallet.get_change_address()?, + )?; if self.dry_run { let commit_psbt = wallet @@ -74,9 +80,11 @@ impl Batch { commit_tx.txid(), Some(commit_psbt), reveal_tx.txid(), + false, Some(base64::engine::general_purpose::STANDARD.encode(reveal_psbt.serialize())), total_fees, self.inscriptions.clone(), + rune, )))); } @@ -115,30 +123,53 @@ impl Batch { Self::backup_recovery_key(wallet, recovery_key_pair)?; } - let commit = wallet + let commit_txid = wallet .bitcoin_client() .send_raw_transaction(&signed_commit_tx)?; - let reveal = match wallet - .bitcoin_client() - .send_raw_transaction(&signed_reveal_tx) - { - Ok(txid) => txid, - Err(err) => { - return Err(anyhow!( - "Failed to send reveal transaction: {err}\nCommit tx {commit} will be recovered once mined" + if let Some(ref rune_info) = rune { + let commit = consensus::encode::deserialize::(&signed_commit_tx)?; + let reveal = consensus::encode::deserialize::(&signed_reveal_tx)?; + + Ok(Some(Box::new(wallet.wait_for_maturation( + &rune_info.rune.rune, + commit.clone(), + reveal.clone(), + self.output( + commit.txid(), + None, + reveal.txid(), + false, + None, + total_fees, + self.inscriptions.clone(), + rune.clone(), + ), + )?))) + } else { + let reveal = match wallet + .bitcoin_client() + .send_raw_transaction(&signed_reveal_tx) + { + Ok(txid) => txid, + Err(err) => { + return Err(anyhow!( + "Failed to send reveal transaction: {err}\nCommit tx {commit_txid} will be recovered once mined" )) - } - }; + } + }; - Ok(Some(Box::new(self.output( - commit, - None, - reveal, - None, - total_fees, - self.inscriptions.clone(), - )))) + Ok(Some(Box::new(self.output( + commit_txid, + None, + reveal, + true, + None, + total_fees, + self.inscriptions.clone(), + rune, + )))) + } } fn remove_witnesses(mut transaction: Transaction) -> Transaction { @@ -154,13 +185,15 @@ impl Batch { commit: Txid, commit_psbt: Option, reveal: Txid, + reveal_broadcast: bool, reveal_psbt: Option, total_fees: u64, inscriptions: Vec, + rune: Option, ) -> Output { let mut inscriptions_output = Vec::new(); - for index in 0..inscriptions.len() { - let index = u32::try_from(index).unwrap(); + for i in 0..inscriptions.len() { + let index = u32::try_from(i).unwrap(); let vout = match self.mode { Mode::SharedOutput | Mode::SameSat => { @@ -180,18 +213,24 @@ impl Batch { }; let offset = match self.mode { - Mode::SharedOutput => self.postages[0..usize::try_from(index).unwrap()] + Mode::SharedOutput => self.postages[0..i] .iter() .map(|amount| amount.to_sat()) .sum(), Mode::SeparateOutputs | Mode::SameSat | Mode::SatPoints => 0, }; + let destination = match self.mode { + Mode::SameSat | Mode::SharedOutput => &self.destinations[0], + Mode::SatPoints | Mode::SeparateOutputs => &self.destinations[i], + }; + inscriptions_output.push(InscriptionInfo { id: InscriptionId { txid: reveal, index, }, + destination: uncheck(destination), location: SatPoint { outpoint: OutPoint { txid: reveal, vout }, offset, @@ -202,28 +241,30 @@ impl Batch { Output { commit, commit_psbt, + inscriptions: inscriptions_output, + parent: self.parent_info.clone().map(|info| info.id), reveal, + reveal_broadcast, reveal_psbt, + rune, total_fees, - parent: self.parent_info.clone().map(|info| info.id), - inscriptions: inscriptions_output, } } - pub(crate) fn create_batch_inscription_transactions( + pub(crate) fn create_batch_transactions( &self, wallet_inscriptions: BTreeMap>, chain: Chain, locked_utxos: BTreeSet, runic_utxos: BTreeSet, mut utxos: BTreeMap, - change: [Address; 2], - ) -> Result<(Transaction, Transaction, TweakedKeyPair, u64)> { + commit_change: [Address; 2], + reveal_change: Address, + ) -> Result { if let Some(parent_info) = &self.parent_info { - assert!(self - .inscriptions - .iter() - .all(|inscription| inscription.parent().unwrap() == parent_info.id)) + for inscription in &self.inscriptions { + assert_eq!(inscription.parents(), vec![parent_info.id]); + } } match self.mode { @@ -377,22 +418,103 @@ impl Batch { }); } + let rune; + let premine; + let runestone; + + if let Some(etching) = self.etching { + let vout; + let destination; + premine = etching.premine.to_integer(etching.divisibility)?; + + if premine > 0 { + let output = u32::try_from(reveal_outputs.len()).unwrap(); + destination = Some(reveal_change.clone()); + + reveal_outputs.push(TxOut { + script_pubkey: reveal_change.into(), + value: TARGET_POSTAGE.to_sat(), + }); + + vout = Some(output); + } else { + vout = None; + destination = None; + } + + let inner = Runestone { + edicts: Vec::new(), + etching: Some(ordinals::Etching { + divisibility: (etching.divisibility > 0).then_some(etching.divisibility), + terms: etching + .terms + .map(|terms| -> Result { + Ok(ordinals::Terms { + cap: (terms.cap > 0).then_some(terms.cap), + height: ( + terms.height.and_then(|range| (range.start)), + terms.height.and_then(|range| (range.end)), + ), + amount: Some(terms.amount.to_integer(etching.divisibility)?), + offset: ( + terms.offset.and_then(|range| (range.start)), + terms.offset.and_then(|range| (range.end)), + ), + }) + }) + .transpose()?, + premine: (premine > 0).then_some(premine), + rune: Some(etching.rune.rune), + spacers: (etching.rune.spacers > 0).then_some(etching.rune.spacers), + symbol: Some(etching.symbol), + }), + mint: None, + pointer: (premine > 0).then_some((reveal_outputs.len() - 1).try_into().unwrap()), + }; + + let script_pubkey = inner.encipher(); + + runestone = Some(inner); + + ensure!( + self.no_limit || script_pubkey.len() <= 82, + "runestone greater than maximum OP_RETURN size: {} > 82", + script_pubkey.len() + ); + + reveal_outputs.push(TxOut { + script_pubkey, + value: 0, + }); + + rune = Some((destination, etching.rune, vout)); + } else { + premine = 0; + rune = None; + runestone = None; + } + let commit_input = usize::from(self.parent_info.is_some()) + self.reveal_satpoints.len(); - let (_, reveal_fee) = Self::build_reveal_transaction( + let (_reveal_tx, reveal_fee) = Self::build_reveal_transaction( + commit_input, &control_block, self.reveal_fee_rate, - reveal_inputs.clone(), - commit_input, reveal_outputs.clone(), + reveal_inputs.clone(), &reveal_script, + rune.is_some(), ); - let target = if self.mode == Mode::SatPoints { - Target::Value(reveal_fee) - } else { - Target::Value(reveal_fee + Amount::from_sat(total_postage)) - }; + let mut target_value = reveal_fee; + + if self.mode != Mode::SatPoints { + target_value += Amount::from_sat(total_postage); + } + + if premine > 0 { + target_value += TARGET_POSTAGE; + } let unsigned_commit_tx = TransactionBuilder::new( satpoint, @@ -401,9 +523,9 @@ impl Batch { locked_utxos.clone(), runic_utxos, commit_tx_address.clone(), - change, + commit_change, self.commit_fee_rate, - target, + Target::Value(target_value), ) .build_transaction()?; @@ -420,19 +542,20 @@ impl Batch { }; let (mut reveal_tx, _fee) = Self::build_reveal_transaction( + commit_input, &control_block, self.reveal_fee_rate, - reveal_inputs, - commit_input, reveal_outputs.clone(), + reveal_inputs, &reveal_script, + rune.is_some(), ); for output in reveal_tx.output.iter() { ensure!( output.value >= output.script_pubkey.dust_value().to_sat(), "commit transaction output would be dust" - ) + ); } let mut prevouts = Vec::new(); @@ -509,7 +632,35 @@ impl Batch { let total_fees = Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos); - Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair, total_fees)) + match (Runestone::decipher(&reveal_tx), runestone) { + (Some(actual), Some(expected)) => assert_eq!( + actual, + Artifact::Runestone(expected), + "commit transaction runestone did not match expected runestone" + ), + (Some(_), None) => panic!("commit transaction contained runestone, but none was expected"), + (None, Some(_)) => { + panic!("commit transaction did not contain runestone, but one was expected") + } + (None, None) => {} + } + + let rune = rune.map(|(destination, rune, vout)| RuneInfo { + destination: destination.map(|destination| uncheck(&destination)), + location: vout.map(|vout| OutPoint { + txid: reveal_tx.txid(), + vout, + }), + rune, + }); + + Ok(Transactions { + commit_tx: unsigned_commit_tx, + recovery_key_pair, + reveal_tx, + rune, + total_fees, + }) } fn backup_recovery_key(wallet: &Wallet, recovery_key_pair: TweakedKeyPair) -> Result { @@ -544,24 +695,29 @@ impl Batch { } fn build_reveal_transaction( + commit_input_index: usize, control_block: &ControlBlock, fee_rate: FeeRate, - reveal_inputs: Vec, - commit_input_index: usize, - outputs: Vec, + output: Vec, + input: Vec, script: &Script, + etching: bool, ) -> (Transaction, Amount) { let reveal_tx = Transaction { - input: reveal_inputs - .iter() - .map(|outpoint| TxIn { - previous_output: *outpoint, + input: input + .into_iter() + .map(|previous_output| TxIn { + previous_output, script_sig: script::Builder::new().into_script(), witness: Witness::new(), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + sequence: if etching { + Sequence::from_height(Runestone::COMMIT_INTERVAL) + } else { + Sequence::ENABLE_RBF_NO_LOCKTIME + }, }) .collect(), - output: outputs, + output, lock_time: LockTime::ZERO, version: 2, }; diff --git a/src/wallet/batch/range.rs b/src/wallet/batch/range.rs new file mode 100644 index 0000000000..69e83476a1 --- /dev/null +++ b/src/wallet/batch/range.rs @@ -0,0 +1,8 @@ +use super::*; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct Range { + pub start: Option, + pub end: Option, +} diff --git a/src/wallet/batch/terms.rs b/src/wallet/batch/terms.rs new file mode 100644 index 0000000000..c5fe8b7948 --- /dev/null +++ b/src/wallet/batch/terms.rs @@ -0,0 +1,10 @@ +use super::*; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct Terms { + pub amount: Decimal, + pub cap: u128, + pub height: Option, + pub offset: Option, +} diff --git a/src/wallet/batch/transactions.rs b/src/wallet/batch/transactions.rs new file mode 100644 index 0000000000..72f1416b67 --- /dev/null +++ b/src/wallet/batch/transactions.rs @@ -0,0 +1,10 @@ +use super::*; + +#[derive(Debug)] +pub(crate) struct Transactions { + pub(crate) rune: Option, + pub(crate) commit_tx: Transaction, + pub(crate) recovery_key_pair: TweakedKeyPair, + pub(crate) reveal_tx: Transaction, + pub(crate) total_fees: u64, +} diff --git a/src/wallet/entry.rs b/src/wallet/entry.rs new file mode 100644 index 0000000000..78331754cc --- /dev/null +++ b/src/wallet/entry.rs @@ -0,0 +1,105 @@ +use super::*; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct EtchingEntry { + pub commit: Transaction, + pub reveal: Transaction, + pub output: batch::Output, +} + +pub(super) type EtchingEntryValue = ( + Vec, // commit + Vec, // reveal + Vec, // output +); + +impl Entry for EtchingEntry { + type Value = EtchingEntryValue; + + fn load((commit, reveal, output): EtchingEntryValue) -> Self { + Self { + commit: consensus::encode::deserialize::(&commit).unwrap(), + reveal: consensus::encode::deserialize::(&reveal).unwrap(), + output: serde_json::from_slice(&output).unwrap(), + } + } + + fn store(self) -> Self::Value { + ( + consensus::encode::serialize(&self.commit), + consensus::encode::serialize(&self.reveal), + serde_json::to_string(&self.output) + .unwrap() + .as_bytes() + .to_owned(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn etching_entry() { + let commit = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: Vec::new(), + }; + + let reveal = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }], + output: Vec::new(), + }; + + let txid = Txid::from_byte_array([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, + 0x1E, 0x1F, + ]); + + let output = batch::Output { + commit: txid, + commit_psbt: None, + inscriptions: Vec::new(), + parent: None, + reveal: txid, + reveal_broadcast: true, + reveal_psbt: None, + rune: None, + total_fees: 0, + }; + + let value = ( + consensus::encode::serialize(&commit), + consensus::encode::serialize(&reveal), + serde_json::to_string(&output) + .unwrap() + .as_bytes() + .to_owned(), + ); + + let entry = EtchingEntry { + commit, + reveal, + output, + }; + + assert_eq!(entry.clone().store(), value); + assert_eq!(EtchingEntry::load(value), entry); + } +} diff --git a/templates/block.html b/templates/block.html index b13fd560ea..dec589f8fb 100644 --- a/templates/block.html +++ b/templates/block.html @@ -2,7 +2,7 @@

        Block {{ self.height }}

        hash
        {{self.hash}}
        target
        {{self.target}}
        -
        timestamp
        +
        timestamp
        size
        {{self.block.size()}}
        weight
        {{self.block.weight()}}
        %% if self.height.0 > 0 { diff --git a/templates/inscription.html b/templates/inscription.html index 005940ae81..09000ac48a 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -13,6 +13,19 @@

        Inscription {{ self.number }}

        %% }
        +%% if !&self.parents.is_empty() { +
        parents
        +
        +
        +%% for parent in &self.parents { + {{Iframe::thumbnail(*parent)}} +%% } +
        +
        + all +
        +
        +%% } %% if !self.children.is_empty() {
        children
        @@ -28,20 +41,12 @@

        Inscription {{ self.number }}

        %% }
        id
        {{ self.id }}
        -%% if let Some(parent) = &self.parent { -
        parent
        -
        -
        - {{Iframe::thumbnail(*parent)}} -
        -
        -%% } %% if self.charms != 0 {
        charms
        %% for charm in Charm::ALL { %% if charm.is_set(self.charms) { - {{charm.icon()}} + {{charm.icon()}} %% } %% }
        diff --git a/templates/parents.html b/templates/parents.html new file mode 100644 index 0000000000..9612de657a --- /dev/null +++ b/templates/parents.html @@ -0,0 +1,22 @@ +

        Inscription {{ self.number }} Parents

        +%% if self.parents.is_empty() { +

        No parents

        +%% } else { +
        +%% for id in &self.parents { + {{ Iframe::thumbnail(*id) }} +%% } +
        +
        +%% if let Some(prev_page) = &self.prev_page { + +%% } else { +prev +%% } +%% if let Some(next_page) = &self.next_page { + +%% } else { +next +%% } +
        +%% } diff --git a/templates/rune.html b/templates/rune.html index 89ffb10ce6..19e6d72498 100644 --- a/templates/rune.html +++ b/templates/rune.html @@ -1,6 +1,8 @@ -

        {{ self.entry.spaced_rune() }}

        +

        {{ self.entry.spaced_rune }}

        %% if let Some(parent) = self.parent { -{{Iframe::main(parent)}} +
        + {{Iframe::thumbnail(parent)}} +
        %% }
        number
        @@ -9,43 +11,51 @@

        {{ self.entry.spaced_rune() }}

        id
        {{ self.id }}
        -
        etching block height
        -
        {{ self.id.height }}
        -
        etching transaction index
        -
        {{ self.id.index }}
        +
        etching block
        +
        {{ self.id.block }}
        +
        etching transaction
        +
        {{ self.id.tx }}
        mint
        -%% if let Some(mint) = self.entry.mint { +%% if let Some(terms) = self.entry.terms {
        -
        deadline
        -%% if let Some(deadline) = mint.deadline { -
        +
        start
        +%% if let Some(start) = self.entry.start() { +
        {{ start }}
        %% } else {
        none
        %% }
        end
        -%% if let Some(end) = mint.end { +%% if let Some(end) = self.entry.end() {
        {{ end }}
        %% } else {
        none
        %% } -
        limit
        -%% if let Some(limit) = mint.limit { -
        {{ Pile{ amount: limit, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}
        +
        amount
        +%% if let Some(amount) = terms.amount { +
        {{ self.entry.pile(amount) }}
        %% } else {
        none
        %% }
        mints
        {{ self.entry.mints }}
        +
        cap
        +
        {{ terms.cap.unwrap_or_default() }}
        +
        remaining
        +
        {{ terms.cap.unwrap_or_default() - self.entry.mints }}
        +
        mintable
        +
        {{ self.mintable }}
        %% } else {
        no
        %% }
        supply
        -
        {{ Pile{ amount: self.entry.supply, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}
        +
        {{ self.entry.pile(self.entry.supply()) }}
        +
        premine
        +
        {{ self.entry.pile(self.entry.premine) }}
        burned
        -
        {{ Pile{ amount: self.entry.burned, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}
        +
        {{ self.entry.pile(self.entry.burned) }}
        divisibility
        {{ self.entry.divisibility }}
        %% if let Some(symbol) = self.entry.symbol { diff --git a/templates/runes.html b/templates/runes.html index c3d582fe6b..d2852d94b7 100644 --- a/templates/runes.html +++ b/templates/runes.html @@ -1,6 +1,6 @@

        Runes

        diff --git a/templates/sat.html b/templates/sat.html index 51ac8b3e85..9709d79c5a 100644 --- a/templates/sat.html +++ b/templates/sat.html @@ -9,8 +9,19 @@

        Sat {{ self.sat.n() }}

        period
        {{ self.sat.period() }}
        block
        {{ self.sat.height() }}
        offset
        {{ self.sat.third() }}
        -
        rarity
        {{ self.sat.rarity() }}
        timestamp
        {{self.blocktime.suffix()}}
        +
        rarity
        {{ self.sat.rarity() }}
        +%% let charms = self.sat.charms(); +%% if charms != 0 { +
        charms
        +
        +%% for charm in Charm::ALL { +%% if charm.is_set(charms) { + {{charm.icon()}} +%% } +%% } +
        +%% } %% if !self.inscriptions.is_empty() {
        inscriptions
        diff --git a/templates/status.html b/templates/status.html index 0d2c3957b7..f35cb5309b 100644 --- a/templates/status.html +++ b/templates/status.html @@ -4,16 +4,16 @@

        Status

        {{ self.chain }}
        %% if let Some(height) = self.height {
        height
        -
        {{ height }}
        +
        {{ height }}
        %% }
        inscriptions
        -
        {{ self.inscriptions }}
        +
        {{ self.inscriptions }}
        blessed inscriptions
        {{ self.blessed_inscriptions }}
        cursed inscriptions
        {{ self.cursed_inscriptions }}
        runes
        -
        {{ self.runes }}
        +
        {{ self.runes }}
        lost sats
        {{ self.lost_sats }}
        started
        diff --git a/tests/balances.rs b/tests/balances.rs index 79de88aae7..c290737e4c 100644 --- a/tests/balances.rs +++ b/tests/balances.rs @@ -2,12 +2,10 @@ use {super::*, ord::subcommand::balances::Output}; #[test] fn flag_is_required() { - let rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); CommandBuilder::new("--regtest balances") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_exit_code(1) .expected_stderr("error: `ord balances` requires index created with `--index-runes` flag\n") .run_and_extract_stdout(); @@ -15,12 +13,10 @@ fn flag_is_required() { #[test] fn no_runes() { - let rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); let output = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); assert_eq!( @@ -33,20 +29,17 @@ fn no_runes() { #[test] fn with_runes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let a = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - let b = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE + 1)); + let a = etch(&core, &ord, Rune(RUNE)); + let b = etch(&core, &ord, Rune(RUNE + 1)); let output = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .run_and_deserialize_output::(); assert_eq!( @@ -54,25 +47,33 @@ fn with_runes() { Output { runes: vec![ ( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![( OutPoint { - txid: a.transaction, + txid: a.output.reveal, vout: 1 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() ), ( - Rune(RUNE + 1), + SpacedRune::new(Rune(RUNE + 1), 0), vec![( OutPoint { - txid: b.transaction, + txid: b.output.reveal, vout: 1 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() diff --git a/tests/command_builder.rs b/tests/command_builder.rs index 7a03d1a5d8..574b797476 100644 --- a/tests/command_builder.rs +++ b/tests/command_builder.rs @@ -28,17 +28,67 @@ impl ToArgs for Vec { } } +pub(crate) struct Spawn { + pub(crate) child: Child, + expected_exit_code: i32, + expected_stderr: Expected, + expected_stdout: Expected, + tempdir: Arc, +} + +impl Spawn { + #[track_caller] + fn run(self) -> (TempDir, String) { + let output = self.child.wait_with_output().unwrap(); + + let stdout = str::from_utf8(&output.stdout).unwrap(); + let stderr = str::from_utf8(&output.stderr).unwrap(); + if output.status.code() != Some(self.expected_exit_code) { + panic!( + "Test failed: {}\nstdout:\n{}\nstderr:\n{}", + output.status, stdout, stderr + ); + } + + self.expected_stderr.assert_match(stderr); + self.expected_stdout.assert_match(stdout); + + (Arc::try_unwrap(self.tempdir).unwrap(), stdout.into()) + } + + #[track_caller] + pub(crate) fn run_and_deserialize_output(self) -> T { + let stdout = self.stdout_regex(".*").run_and_extract_stdout(); + serde_json::from_str(&stdout) + .unwrap_or_else(|err| panic!("Failed to deserialize JSON: {err}\n{stdout}")) + } + + #[track_caller] + pub(crate) fn run_and_extract_stdout(self) -> String { + self.run().1 + } + + pub(crate) fn stdout_regex(self, expected_stdout: impl AsRef) -> Self { + Self { + expected_stdout: Expected::regex(expected_stdout.as_ref()), + ..self + } + } +} + pub(crate) struct CommandBuilder { args: Vec, - bitcoin_rpc_server_cookie_file: Option, - bitcoin_rpc_server_url: Option, + core_cookie_file: Option, + core_url: Option, env: BTreeMap, expected_exit_code: i32, expected_stderr: Expected, expected_stdout: Expected, integration_test: bool, - ord_rpc_server_url: Option, + ord_url: Option, + stderr: bool, stdin: Vec, + stdout: bool, tempdir: Arc, } @@ -46,15 +96,17 @@ impl CommandBuilder { pub(crate) fn new(args: impl ToArgs) -> Self { Self { args: args.to_args(), - bitcoin_rpc_server_cookie_file: None, - bitcoin_rpc_server_url: None, + core_cookie_file: None, + core_url: None, env: BTreeMap::new(), expected_exit_code: 0, expected_stderr: Expected::String(String::new()), expected_stdout: Expected::String(String::new()), integration_test: true, - ord_rpc_server_url: None, + ord_url: None, + stderr: true, stdin: Vec::new(), + stdout: true, tempdir: Arc::new(TempDir::new().unwrap()), } } @@ -76,28 +128,35 @@ impl CommandBuilder { self } - pub(crate) fn bitcoin_rpc_server( - self, - bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, - ) -> Self { + pub(crate) fn core(self, core: &mockcore::Handle) -> Self { Self { - bitcoin_rpc_server_url: Some(bitcoin_rpc_server.url()), - bitcoin_rpc_server_cookie_file: Some(bitcoin_rpc_server.cookie_file()), + core_url: Some(core.url()), + core_cookie_file: Some(core.cookie_file()), ..self } } - pub(crate) fn ord_rpc_server(self, ord_rpc_server: &TestServer) -> Self { + pub(crate) fn ord(self, ord: &TestServer) -> Self { Self { - ord_rpc_server_url: Some(ord_rpc_server.url()), + ord_url: Some(ord.url()), ..self } } + #[allow(unused)] + pub(crate) fn stderr(self, stderr: bool) -> Self { + Self { stderr, ..self } + } + pub(crate) fn stdin(self, stdin: Vec) -> Self { Self { stdin, ..self } } + #[allow(unused)] + pub(crate) fn stdout(self, stdout: bool) -> Self { + Self { stdout, ..self } + } + pub(crate) fn stdout_regex(self, expected_stdout: impl AsRef) -> Self { Self { expected_stdout: Expected::regex(expected_stdout.as_ref()), @@ -133,17 +192,12 @@ impl CommandBuilder { pub(crate) fn command(&self) -> Command { let mut command = Command::new(executable_path("ord")); - if let Some(rpc_server_url) = &self.bitcoin_rpc_server_url { + if let Some(rpc_server_url) = &self.core_url { command.args([ "--bitcoin-rpc-url", rpc_server_url, "--cookie-file", - self - .bitcoin_rpc_server_cookie_file - .as_ref() - .unwrap() - .to_str() - .unwrap(), + self.core_cookie_file.as_ref().unwrap().to_str().unwrap(), ]); } @@ -152,7 +206,7 @@ impl CommandBuilder { for arg in self.args.iter() { args.push(arg.clone()); if arg == "wallet" { - if let Some(ord_server_url) = &self.ord_rpc_server_url { + if let Some(ord_server_url) = &self.ord_url { args.push("--server-url".to_string()); args.push(ord_server_url.to_string()); } @@ -169,10 +223,18 @@ impl CommandBuilder { command .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stdout(if self.stdout { + Stdio::piped() + } else { + Stdio::inherit() + }) + .stderr(if self.stderr { + Stdio::piped() + } else { + Stdio::inherit() + }) .current_dir(&*self.tempdir) - .arg("--data-dir") + .arg("--datadir") .arg(self.tempdir.path()) .args(&args); @@ -180,7 +242,7 @@ impl CommandBuilder { } #[track_caller] - fn run(self) -> (TempDir, String) { + pub(crate) fn spawn(self) -> Spawn { let mut command = self.command(); let child = command.spawn().unwrap(); @@ -191,21 +253,18 @@ impl CommandBuilder { .write_all(&self.stdin) .unwrap(); - let output = child.wait_with_output().unwrap(); - - let stdout = str::from_utf8(&output.stdout).unwrap(); - let stderr = str::from_utf8(&output.stderr).unwrap(); - if output.status.code() != Some(self.expected_exit_code) { - panic!( - "Test failed: {}\nstdout:\n{}\nstderr:\n{}", - output.status, stdout, stderr - ); + Spawn { + child, + expected_exit_code: self.expected_exit_code, + expected_stderr: self.expected_stderr, + expected_stdout: self.expected_stdout, + tempdir: self.tempdir, } + } - self.expected_stderr.assert_match(stderr); - self.expected_stdout.assert_match(stdout); - - (Arc::try_unwrap(self.tempdir).unwrap(), stdout.into()) + #[track_caller] + fn run(self) -> (TempDir, String) { + self.spawn().run() } pub(crate) fn run_and_extract_file(self, path: impl AsRef) -> String { @@ -221,7 +280,9 @@ impl CommandBuilder { #[track_caller] pub(crate) fn run_and_deserialize_output(self) -> T { let stdout = self.stdout_regex(".*").run_and_extract_stdout(); - serde_json::from_str(&stdout) - .unwrap_or_else(|err| panic!("Failed to deserialize JSON: {err}\n{stdout}")) + match serde_json::from_str(&stdout) { + Ok(output) => output, + Err(err) => panic!("Failed to deserialize JSON: {err}\n{stdout}"), + } } } diff --git a/tests/decode.rs b/tests/decode.rs index d27d855d8b..20f87c7936 100644 --- a/tests/decode.rs +++ b/tests/decode.rs @@ -2,7 +2,7 @@ use { super::*, bitcoin::{ absolute::LockTime, consensus::Encodable, opcodes, script, ScriptBuf, Sequence, Transaction, - TxIn, Witness, + TxIn, TxOut, Witness, }, ord::{ subcommand::decode::{CompactInscription, CompactOutput, RawOutput}, @@ -36,7 +36,10 @@ fn transaction() -> Vec { sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, witness, }], - output: Vec::new(), + output: vec![TxOut { + script_pubkey: Runestone::default().encipher(), + value: 0, + }], }; let mut buffer = Vec::new(); @@ -48,7 +51,7 @@ fn transaction() -> Vec { #[test] fn from_file() { - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("decode --file transaction.bin") .write("transaction.bin", transaction()) .run_and_deserialize_output::(), @@ -57,20 +60,21 @@ fn from_file() { payload: Inscription { body: Some(vec![0, 1, 2, 3]), content_type: Some(b"text/plain;charset=utf-8".into()), - ..Default::default() + ..default() }, input: 0, offset: 0, pushnum: false, stutter: false, }], + runestone: Some(Artifact::Runestone(Runestone::default())), }, ); } #[test] fn from_stdin() { - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("decode") .stdin(transaction()) .run_and_deserialize_output::(), @@ -79,51 +83,53 @@ fn from_stdin() { payload: Inscription { body: Some(vec![0, 1, 2, 3]), content_type: Some(b"text/plain;charset=utf-8".into()), - ..Default::default() + ..default() }, input: 0, offset: 0, pushnum: false, stutter: false, }], + runestone: Some(Artifact::Runestone(Runestone::default())), }, ); } #[test] fn from_core() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (_inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_inscription, reveal) = inscribe(&core, &ord); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new(format!("decode --txid {reveal}")) - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .run_and_deserialize_output::(), RawOutput { inscriptions: vec![Envelope { payload: Inscription { body: Some(b"FOO".into()), content_type: Some(b"text/plain;charset=utf-8".into()), - ..Default::default() + ..default() }, input: 0, offset: 0, pushnum: false, stutter: false, }], + runestone: None, }, ); } #[test] fn compact() { - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("decode --compact --file transaction.bin") .write("transaction.bin", transaction()) .run_and_deserialize_output::(), @@ -136,10 +142,11 @@ fn compact() { incomplete_field: false, metadata: None, metaprotocol: None, - parent: None, + parents: Vec::new(), pointer: None, unrecognized_even_field: false, }], + runestone: Some(Artifact::Runestone(Runestone::default())), }, ); } diff --git a/tests/etch.rs b/tests/etch.rs deleted file mode 100644 index c66cf121b5..0000000000 --- a/tests/etch.rs +++ /dev/null @@ -1,480 +0,0 @@ -use { - super::*, - ord::{ - subcommand::wallet::{balance, etch::Output}, - Rune, - }, -}; - -#[test] -fn flag_is_required() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - CommandBuilder::new(format!( - "--regtest wallet etch --rune {} --divisibility 39 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: `ord wallet etch` requires index created with `--index-runes` flag\n") - .run_and_extract_stdout(); -} - -#[test] -fn divisibility_over_max_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 39 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: 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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 340282366920938463463374607431768211456 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(99229755678436031 - 1), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune is less than minimum for next block: ZZWZRFAGQTKY < ZZWZRFAGQTKZ\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn reserved_rune_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - "--index-runes --regtest wallet etch --rune AAAAAAAAAAAAAAAAAAAAAAAAAAA --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn trying_to_etch_an_existing_rune_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new( - "--index-runes --regtest wallet etch --rune A•A•A•A•A•A•A•A•A•A•A•A•A --divisibility 1 --fee-rate 1 --supply 1000 --symbol ¢", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - pretty_assert_eq!( - runes(&bitcoin_rpc_server), - vec![( - Rune(RUNE), - RuneInfo { - burned: 0, - mint: None, - divisibility: 1, - etching: output.transaction, - height: 2, - id: RuneId { - height: 2, - index: 1 - }, - index: 1, - mints: 0, - number: 0, - rune: Rune(RUNE), - spacers: 0b111111111111, - supply: 10000, - symbol: Some('¢'), - timestamp: ord::timestamp(2), - } - )] - .into_iter() - .collect() - ); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.runes.unwrap()[&Rune(RUNE)], 10000); -} - -#[test] -fn etch_sets_integer_fee_rate_correctly() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 100 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let tx = bitcoin_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 100.5 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let tx = bitcoin_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.cardinal, 0); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stderr_regex("error: JSON-RPC error: .*") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn inscribe_does_not_select_runic_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - CommandBuilder::new("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 600sat") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - CommandBuilder::new(format!("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}:1:0", output.transaction)) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - let inscribe = - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 wallet send --postage 10001sat --fee-rate 0 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}", inscribe.inscriptions[0].id)) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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/find.rs b/tests/find.rs index e313debdaf..cae01be400 100644 --- a/tests/find.rs +++ b/tests/find.rs @@ -5,10 +5,10 @@ use { #[test] fn find_command_returns_satpoint_for_sat() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); assert_eq!( CommandBuilder::new("--index-sats find 0") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(), Output { satpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0" @@ -20,28 +20,36 @@ fn find_command_returns_satpoint_for_sat() { #[test] fn find_range_command_returns_satpoints_and_ranges() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - rpc_server.mine_blocks(1); + core.mine_blocks(1); pretty_assert_eq!( CommandBuilder::new(format!("--index-sats find 0 {}", 55 * COIN_VALUE)) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::>(), vec![ FindRangeOutput { start: 0, size: 50 * COIN_VALUE, - satpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0" - .parse() - .unwrap() + satpoint: SatPoint { + outpoint: OutPoint { + txid: core.tx(0, 0).into(), + vout: 0, + }, + offset: 0, + } }, FindRangeOutput { start: 50 * COIN_VALUE, size: 5 * COIN_VALUE, - satpoint: "84aca0d43f45ac753d4744f40b2f54edec3a496b298951735d450e601386089d:0:0" - .parse() - .unwrap() + satpoint: SatPoint { + outpoint: OutPoint { + txid: core.tx(1, 0).into(), + vout: 0, + }, + offset: 0, + } } ] ); @@ -49,14 +57,14 @@ fn find_range_command_returns_satpoints_and_ranges() { #[test] fn find_range_command_fails_for_unmined_sat_ranges() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new(format!( "--index-sats find {} {}", 50 * COIN_VALUE, 100 * COIN_VALUE )) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_exit_code(1) .expected_stderr("error: range has not been mined as of index height\n") .run_and_extract_stdout(); @@ -64,9 +72,9 @@ fn find_range_command_fails_for_unmined_sat_ranges() { #[test] fn unmined_sat() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("--index-sats find 5000000000") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_stderr("error: sat has not been mined as of index height\n") .expected_exit_code(1) .run_and_extract_stdout(); @@ -74,9 +82,9 @@ fn unmined_sat() { #[test] fn no_satoshi_index() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("find 0") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_stderr("error: find requires index created with `--index-sats` flag\n") .expected_exit_code(1) .run_and_extract_stdout(); diff --git a/tests/index.rs b/tests/index.rs index 8d9ca36dd4..3a13ebbfd1 100644 --- a/tests/index.rs +++ b/tests/index.rs @@ -2,15 +2,15 @@ use super::*; #[test] fn run_is_an_alias_for_update() { - let rpc_server = test_bitcoincore_rpc::spawn(); - rpc_server.mine_blocks(1); + let core = mockcore::spawn(); + core.mine_blocks(1); let tempdir = TempDir::new().unwrap(); let index_path = tempdir.path().join("foo.redb"); CommandBuilder::new(format!("--index {} index run", index_path.display())) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); assert!(index_path.is_file()) @@ -18,15 +18,15 @@ fn run_is_an_alias_for_update() { #[test] fn custom_index_path() { - let rpc_server = test_bitcoincore_rpc::spawn(); - rpc_server.mine_blocks(1); + let core = mockcore::spawn(); + core.mine_blocks(1); let tempdir = TempDir::new().unwrap(); let index_path = tempdir.path().join("foo.redb"); CommandBuilder::new(format!("--index {} index update", index_path.display())) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); assert!(index_path.is_file()) @@ -34,42 +34,42 @@ fn custom_index_path() { #[test] fn re_opening_database_does_not_trigger_schema_check() { - let rpc_server = test_bitcoincore_rpc::spawn(); - rpc_server.mine_blocks(1); + let core = mockcore::spawn(); + core.mine_blocks(1); let tempdir = TempDir::new().unwrap(); let index_path = tempdir.path().join("foo.redb"); CommandBuilder::new(format!("--index {} index update", index_path.display())) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); assert!(index_path.is_file()); CommandBuilder::new(format!("--index {} index update", index_path.display())) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); } #[test] fn export_inscription_number_to_id_tsv() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let temp_dir = TempDir::new().unwrap(); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); + inscribe(&core, &ord); + inscribe(&core, &ord); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let tsv = CommandBuilder::new("index export --tsv foo.tsv") - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .temp_dir(Arc::new(temp_dir)) .run_and_extract_file("foo.tsv"); diff --git a/tests/info.rs b/tests/info.rs index ec0c6c3a48..170ec5aab8 100644 --- a/tests/info.rs +++ b/tests/info.rs @@ -2,9 +2,9 @@ use {super::*, ord::subcommand::index::info::TransactionsOutput}; #[test] fn json_with_satoshi_index() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("--index-sats index info") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .stdout_regex( r#"\{ "blocks_indexed": 1, @@ -36,9 +36,9 @@ fn json_with_satoshi_index() { #[test] fn json_without_satoshi_index() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("index info") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .stdout_regex( r#"\{ "blocks_indexed": 1, @@ -70,7 +70,7 @@ fn json_without_satoshi_index() { #[test] fn transactions() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let tempdir = TempDir::new().unwrap(); @@ -80,30 +80,30 @@ fn transactions() { "--index {} index info --transactions", index_path.display() )) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::>() .is_empty()); - rpc_server.mine_blocks(10); + core.mine_blocks(10); let output = CommandBuilder::new(format!( "--index {} index info --transactions", index_path.display() )) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::>(); assert_eq!(output[0].start, 0); assert_eq!(output[0].end, 1); assert_eq!(output[0].count, 1); - rpc_server.mine_blocks(10); + core.mine_blocks(10); let output = CommandBuilder::new(format!( "--index {} index info --transactions", index_path.display() )) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::>(); assert_eq!(output[1].start, 1); diff --git a/tests/json_api.rs b/tests/json_api.rs index 183c75fc2a..b048d63f74 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -2,10 +2,10 @@ use {super::*, bitcoin::BlockHash}; #[test] fn get_sat_without_sat_index() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let response = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]) - .json_request("/sat/2099999997689999"); + let response = + TestServer::spawn_with_server_args(&core, &[], &[]).json_request("/sat/2099999997689999"); assert_eq!(response.status(), StatusCode::OK); @@ -30,23 +30,23 @@ fn get_sat_without_sat_index() { percentile: "100%".into(), satpoint: None, timestamp: 0, - inscriptions: vec![], + inscriptions: Vec::new(), + charms: vec![Charm::Uncommon], } ) } #[test] fn get_sat_with_inscription_and_sat_index() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription_id, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription_id, reveal) = inscribe(&core, &ord); - let response = ord_rpc_server.json_request(format!("/sat/{}", 50 * COIN_VALUE)); + let response = ord.json_request(format!("/sat/{}", 50 * COIN_VALUE)); assert_eq!(response.status(), StatusCode::OK); @@ -69,40 +69,40 @@ fn get_sat_with_inscription_and_sat_index() { satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()), timestamp: 1, inscriptions: vec![inscription_id], + charms: vec![Charm::Coin, Charm::Uncommon], } ) } #[test] fn get_sat_with_inscription_on_common_sat_and_more_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); + inscribe(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = core.mine_blocks(1)[0].txdata[0].txid(); - let Inscribe { reveal, .. } = CommandBuilder::new(format!( + let Batch { reveal, .. } = CommandBuilder::new(format!( "wallet inscribe --satpoint {}:0:1 --fee-rate 1 --file foo.txt", txid )) .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let inscription_id = InscriptionId { txid: reveal, index: 0, }; - let response = ord_rpc_server.json_request(format!("/sat/{}", 3 * 50 * COIN_VALUE + 1)); + let response = ord.json_request(format!("/sat/{}", 3 * 50 * COIN_VALUE + 1)); assert_eq!(response.status(), StatusCode::OK); @@ -125,22 +125,22 @@ fn get_sat_with_inscription_on_common_sat_and_more_inscriptions() { satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()), timestamp: 3, inscriptions: vec![inscription_id], + charms: Vec::new(), } ) } #[test] fn get_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription_id, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription_id, reveal) = inscribe(&core, &ord); - let response = ord_rpc_server.json_request(format!("/inscription/{}", inscription_id)); + let response = ord.json_request(format!("/inscription/{}", inscription_id)); assert_eq!(response.status(), StatusCode::OK); @@ -153,24 +153,25 @@ fn get_inscription() { inscription_json, api::Inscription { address: None, - charms: vec!["coin".into(), "uncommon".into()], + charms: vec![Charm::Coin, Charm::Uncommon], children: Vec::new(), content_length: Some(3), content_type: Some("text/plain;charset=utf-8".to_string()), + effective_content_type: Some("text/plain;charset=utf-8".to_string()), fee: 138, height: 2, id: inscription_id, number: 0, next: None, value: Some(10000), - parent: None, + parents: Vec::new(), previous: None, rune: None, sat: Some(Sat(50 * COIN_VALUE)), satpoint: SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap(), timestamp: 2, - // ---- Ordzaar ---- + // ---- Ordzaar ---- inscription_sequence: 0, delegate: None, content_encoding: None, @@ -181,12 +182,11 @@ fn get_inscription() { #[test] fn get_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); @@ -194,17 +194,17 @@ fn get_inscriptions() { // Create 150 inscriptions for i in 0..50 { - bitcoin_rpc_server.mine_blocks(1); - bitcoin_rpc_server.mine_blocks(1); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); + core.mine_blocks(1); + core.mine_blocks(1); - let txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = core.broadcast_tx(TransactionTemplate { inputs: &[ (i * 3 + 1, 0, 0, witness.clone()), (i * 3 + 2, 0, 0, witness.clone()), (i * 3 + 3, 0, 0, witness.clone()), ], - ..Default::default() + ..default() }); inscriptions.push(InscriptionId { txid, index: 0 }); @@ -212,9 +212,9 @@ fn get_inscriptions() { inscriptions.push(InscriptionId { txid, index: 2 }); } - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let response = ord_rpc_server.json_request("/inscriptions"); + let response = ord.json_request("/inscriptions"); assert_eq!(response.status(), StatusCode::OK); let inscriptions_json: api::Inscriptions = serde_json::from_str(&response.text().unwrap()).unwrap(); @@ -223,7 +223,7 @@ fn get_inscriptions() { assert!(inscriptions_json.more); assert_eq!(inscriptions_json.page_index, 0); - let response = ord_rpc_server.json_request("/inscriptions/1"); + let response = ord.json_request("/inscriptions/1"); assert_eq!(response.status(), StatusCode::OK); let inscriptions_json: api::Inscriptions = serde_json::from_str(&response.text().unwrap()).unwrap(); @@ -235,47 +235,47 @@ fn get_inscriptions() { #[test] fn get_inscriptions_in_block() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args( - &bitcoin_rpc_server, + let ord = TestServer::spawn_with_server_args( + &core, &["--index-sats", "--first-inscription-height", "0"], &[], ); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(10); + core.mine_blocks(10); let envelope = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); - let txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, envelope.clone()), (2, 0, 0, envelope.clone()), (3, 0, 0, envelope.clone()), ], - ..Default::default() + ..default() }); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let _ = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let _ = core.broadcast_tx(TransactionTemplate { inputs: &[(4, 0, 0, envelope.clone()), (5, 0, 0, envelope.clone())], - ..Default::default() + ..default() }); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let _ = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let _ = core.broadcast_tx(TransactionTemplate { inputs: &[(6, 0, 0, envelope.clone())], - ..Default::default() + ..default() }); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); // get all inscriptions from block 11 - let response = ord_rpc_server.json_request(format!("/inscriptions/block/{}", 11)); + let response = ord.json_request(format!("/inscriptions/block/{}", 11)); assert_eq!(response.status(), StatusCode::OK); let inscriptions_json: api::Inscriptions = @@ -293,27 +293,26 @@ fn get_inscriptions_in_block() { #[test] fn get_output() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(3); + create_wallet(&core, &ord); + core.mine_blocks(3); let envelope = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); - let txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + let txid = core.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, envelope.clone()), (2, 0, 0, envelope.clone()), (3, 0, 0, envelope.clone()), ], - ..Default::default() + ..default() }); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &["--no-sync"]); + let server = TestServer::spawn_with_server_args(&core, &["--index-sats"], &["--no-sync"]); let response = reqwest::blocking::Client::new() .get(server.url().join(&format!("/output/{}:0", txid)).unwrap()) @@ -329,7 +328,7 @@ fn get_output() { .indexed ); - let server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let server = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); let response = server.json_request(format!("/output/{}:0", txid)); assert_eq!(response.status(), StatusCode::OK); @@ -339,7 +338,11 @@ fn get_output() { pretty_assert_eq!( output_json, api::Output { - address: None, + address: Some( + "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq9e75rs" + .parse() + .unwrap() + ), inscriptions: vec![ InscriptionId { txid, index: 0 }, InscriptionId { txid, index: 1 }, @@ -352,7 +355,7 @@ fn get_output() { (10000000000, 15000000000,), (15000000000, 20000000000,), ],), - script_pubkey: "".to_string(), + script_pubkey: "OP_0 OP_PUSHBYTES_20 0000000000000000000000000000000000000000".into(), spent: false, transaction: txid.to_string(), value: 3 * 50 * COIN_VALUE, @@ -362,23 +365,21 @@ fn get_output() { #[test] fn json_request_fails_when_disabled() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let response = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &["--disable-json-api"]) - .json_request("/sat/2099999997689999"); + let response = TestServer::spawn_with_server_args(&core, &[], &["--disable-json-api"]) + .json_request("/sat/2099999997689999"); assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); } #[test] fn get_block() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let response = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]).json_request("/block/0"); + let response = TestServer::spawn_with_server_args(&core, &[], &[]).json_request("/block/0"); assert_eq!(response.status(), StatusCode::OK); @@ -395,17 +396,17 @@ fn get_block() { .unwrap(), best_height: 1, height: 0, - inscriptions: vec![], + inscriptions: Vec::new(), } ); } #[test] fn get_blocks() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - let blocks: Vec = bitcoin_rpc_server + let blocks: Vec = core .mine_blocks(101) .iter() .rev() @@ -413,9 +414,9 @@ fn get_blocks() { .map(|block| block.block_hash()) .collect(); - ord_rpc_server.sync_server(); + ord.sync_server(); - let response = ord_rpc_server.json_request("/blocks"); + let response = ord.json_request("/blocks"); assert_eq!(response.status(), StatusCode::OK); @@ -437,15 +438,15 @@ fn get_blocks() { #[test] fn get_transaction() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let ord = TestServer::spawn(&core); - let transaction = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].clone(); + let transaction = core.mine_blocks(1)[0].txdata[0].clone(); let txid = transaction.txid(); - let response = ord_rpc_server.json_request(format!("/tx/{txid}")); + let response = ord.json_request(format!("/tx/{txid}")); assert_eq!(response.status(), StatusCode::OK); @@ -463,22 +464,17 @@ fn get_transaction() { #[test] fn get_status() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = TestServer::spawn_with_server_args( - &bitcoin_rpc_server, - &["--regtest", "--index-sats", "--index-runes"], - &[], - ); + let ord = + TestServer::spawn_with_server_args(&core, &["--regtest", "--index-sats", "--index-runes"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(1); + create_wallet(&core, &ord); + core.mine_blocks(1); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); + inscribe(&core, &ord); - let response = ord_rpc_server.json_request("/status"); + let response = ord.json_request("/status"); assert_eq!(response.status(), StatusCode::OK); @@ -519,24 +515,21 @@ fn get_status() { #[test] fn get_runes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(3); + core.mine_blocks(3); - let a = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - let b = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE + 1)); - let c = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE + 2)); + let a = etch(&core, &ord, Rune(RUNE)); + let b = etch(&core, &ord, Rune(RUNE + 1)); + let c = etch(&core, &ord, Rune(RUNE + 2)); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let response = ord_rpc_server.json_request(format!("/rune/{}", a.rune)); + let response = ord.json_request(format!("/rune/{}", a.output.rune.unwrap().rune)); assert_eq!(response.status(), StatusCode::OK); let rune_json: api::Rune = serde_json::from_str(&response.text().unwrap()).unwrap(); @@ -545,27 +538,31 @@ fn get_runes() { rune_json, api::Rune { entry: RuneEntry { + block: a.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, - etching: a.transaction, + etching: a.output.reveal, mints: 0, number: 0, - rune: Rune(RUNE), - spacers: 0, - supply: 1000, + premine: 1000, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0 + }, symbol: Some('¢'), - timestamp: 5, - }, - id: RuneId { - height: 5, - index: 1 + timestamp: 11, }, - parent: None, + id: RuneId { block: 11, tx: 1 }, + mintable: false, + parent: Some(InscriptionId { + txid: a.output.reveal, + index: 0, + }), } ); - let response = ord_rpc_server.json_request("/runes"); + let response = ord.json_request("/runes"); assert_eq!(response.status(), StatusCode::OK); @@ -576,60 +573,60 @@ fn get_runes() { api::Runes { entries: vec![ ( - RuneId { - height: 5, - index: 1 - }, + RuneId { block: 11, tx: 1 }, RuneEntry { + block: a.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, - etching: a.transaction, + etching: a.output.reveal, mints: 0, number: 0, - rune: Rune(RUNE), - spacers: 0, - supply: 1000, + premine: 1000, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0 + }, symbol: Some('¢'), - timestamp: 5, + timestamp: 11, } ), ( - RuneId { - height: 7, - index: 1 - }, + RuneId { block: 19, tx: 1 }, RuneEntry { + block: b.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, - etching: b.transaction, + etching: b.output.reveal, mints: 0, number: 1, - rune: Rune(RUNE + 1), - spacers: 0, - supply: 1000, + premine: 1000, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0 + }, symbol: Some('¢'), - timestamp: 7, + timestamp: 19, } ), ( - RuneId { - height: 9, - index: 1 - }, + RuneId { block: 27, tx: 1 }, RuneEntry { + block: c.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, - etching: c.transaction, + etching: c.output.reveal, mints: 0, number: 2, - rune: Rune(RUNE + 2), - spacers: 0, - supply: 1000, + premine: 1000, + spaced_rune: SpacedRune { + rune: Rune(RUNE + 2), + spacers: 0 + }, symbol: Some('¢'), - timestamp: 9, + timestamp: 27, } ) ] @@ -638,33 +635,30 @@ fn get_runes() { } #[test] fn get_runes_balances() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(3); + core.mine_blocks(3); let rune0 = Rune(RUNE); let rune1 = Rune(RUNE + 1); let rune2 = Rune(RUNE + 2); - let e0 = etch(&bitcoin_rpc_server, &ord_rpc_server, rune0); - let e1 = etch(&bitcoin_rpc_server, &ord_rpc_server, rune1); - let e2 = etch(&bitcoin_rpc_server, &ord_rpc_server, rune2); + let e0 = etch(&core, &ord, rune0); + let e1 = etch(&core, &ord, rune1); + let e2 = etch(&core, &ord, rune2); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let rune_balances: BTreeMap> = vec![ ( rune0, vec![( OutPoint { - txid: e0.transaction, + txid: e0.output.reveal, vout: 1, }, 1000, @@ -676,7 +670,7 @@ fn get_runes_balances() { rune1, vec![( OutPoint { - txid: e1.transaction, + txid: e1.output.reveal, vout: 1, }, 1000, @@ -688,7 +682,7 @@ fn get_runes_balances() { rune2, vec![( OutPoint { - txid: e2.transaction, + txid: e2.output.reveal, vout: 1, }, 1000, @@ -700,7 +694,7 @@ fn get_runes_balances() { .into_iter() .collect(); - let response = ord_rpc_server.json_request("/runes/balances"); + let response = ord.json_request("/runes/balances"); assert_eq!(response.status(), StatusCode::OK); let runes_balance_json: BTreeMap> = diff --git a/tests/lib.rs b/tests/lib.rs index 0d859cc5d5..fd0cc96363 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,16 +5,19 @@ use { bitcoin::{ address::{Address, NetworkUnchecked}, blockdata::constants::COIN_VALUE, - Network, OutPoint, Txid, + Network, OutPoint, Sequence, Txid, Witness, }, bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult, chrono::{DateTime, Utc}, executable_path::executable_path, + mockcore::TransactionTemplate, ord::{ - api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, Edict, InscriptionId, Rune, - RuneEntry, RuneId, Runestone, + api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, wallet::batch, + InscriptionId, RuneEntry, + }, + ordinals::{ + Artifact, Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune, }, - ordinals::{Rarity, Sat, SatPoint}, pretty_assertions::assert_eq as pretty_assert_eq, regex::Regex, reqwest::{StatusCode, Url}, @@ -24,16 +27,15 @@ use { collections::BTreeMap, ffi::{OsStr, OsString}, fs, - io::Write, + io::{BufRead, BufReader, Write}, net::TcpListener, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::{Child, Command, Stdio}, str::{self, FromStr}, thread, time::Duration, }, tempfile::TempDir, - test_bitcoincore_rpc::TransactionTemplate, }; macro_rules! assert_regex_match { @@ -55,7 +57,6 @@ mod test_server; mod balances; mod decode; mod epochs; -mod etch; mod find; mod index; mod info; @@ -73,65 +74,342 @@ mod wallet; const RUNE: u128 = 99246114928149462; -type Inscribe = ord::wallet::inscribe::Output; +type Balance = ord::subcommand::wallet::balance::Output; +type Batch = ord::wallet::batch::Output; +type Create = ord::subcommand::wallet::create::Output; type Inscriptions = Vec; -type Etch = ord::subcommand::wallet::etch::Output; +type Send = ord::subcommand::wallet::send::Output; +type Supply = ord::subcommand::supply::Output; -fn create_wallet(bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_server: &TestServer) { - CommandBuilder::new(format!( - "--chain {} wallet create", - bitcoin_rpc_server.network() - )) - .bitcoin_rpc_server(bitcoin_rpc_server) - .ord_rpc_server(ord_rpc_server) - .run_and_deserialize_output::(); +fn create_wallet(core: &mockcore::Handle, ord: &TestServer) { + CommandBuilder::new(format!("--chain {} wallet create", core.network())) + .core(core) + .ord(ord) + .stdout_regex(".*") + .run_and_extract_stdout(); +} + +fn sats( + core: &mockcore::Handle, + ord: &TestServer, +) -> Vec { + CommandBuilder::new(format!("--chain {} wallet sats", core.network())) + .core(core) + .ord(ord) + .run_and_deserialize_output::>() } -fn inscribe( - bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, - ord_rpc_server: &TestServer, -) -> (InscriptionId, Txid) { - bitcoin_rpc_server.mine_blocks(1); +fn inscribe(core: &mockcore::Handle, ord: &TestServer) -> (InscriptionId, Txid) { + core.mine_blocks(1); let output = CommandBuilder::new(format!( "--chain {} wallet inscribe --fee-rate 1 --file foo.txt", - bitcoin_rpc_server.network() + core.network() )) .write("foo.txt", "FOO") - .bitcoin_rpc_server(bitcoin_rpc_server) - .ord_rpc_server(ord_rpc_server) - .run_and_deserialize_output::(); + .core(core) + .ord(ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); assert_eq!(output.inscriptions.len(), 1); (output.inscriptions[0].id, output.reveal) } -fn etch( - bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, - ord_rpc_server: &TestServer, - rune: Rune, -) -> Etch { - bitcoin_rpc_server.mine_blocks(1); +fn drain(core: &mockcore::Handle, ord: &TestServer) { + let balance = CommandBuilder::new("--regtest --index-runes wallet balance") + .core(core) + .ord(ord) + .run_and_deserialize_output::(); - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 0 --supply 1000 --symbol ¢", - rune - ) + CommandBuilder::new(format!( + " + --chain regtest + --index-runes + wallet send + --fee-rate 0 + bcrt1pyrmadgg78e38ewfv0an8c6eppk2fttv5vnuvz04yza60qau5va0saknu8k + {}sat + ", + balance.cardinal + )) + .core(core) + .ord(ord) + .run_and_deserialize_output::(); + + core.mine_blocks_with_subsidy(1, 0); + + let balance = CommandBuilder::new("--regtest --index-runes wallet balance") + .core(core) + .ord(ord) + .run_and_deserialize_output::(); + + pretty_assert_eq!(balance.cardinal, 0); +} + +struct Etched { + id: RuneId, + output: Batch, +} + +fn etch(core: &mockcore::Handle, ord: &TestServer, rune: Rune) -> Etched { + batch( + core, + ord, + batch::File { + etching: Some(batch::Etching { + supply: "1000".parse().unwrap(), + divisibility: 0, + terms: None, + premine: "1000".parse().unwrap(), + rune: SpacedRune { rune, spacers: 0 }, + symbol: '¢', + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, ) - .bitcoin_rpc_server(bitcoin_rpc_server) - .ord_rpc_server(ord_rpc_server) - .run_and_deserialize_output(); +} + +fn batch(core: &mockcore::Handle, ord: &TestServer, batchfile: batch::File) -> Etched { + core.mine_blocks(1); + + let mut builder = + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap()) + .core(core) + .ord(ord); + + for inscription in &batchfile.inscriptions { + builder = builder.write(&inscription.file, "inscription"); + } + + let mut spawn = builder.spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_regex_match!( + buffer, + "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n" + ); + + core.mine_blocks(6); + + let output = spawn.run_and_deserialize_output::(); + + core.mine_blocks(1); + + let block_height = core.height(); + + let id = RuneId { + block: block_height, + tx: 1, + }; + + let reveal = output.reveal; + let parent = output.inscriptions[0].id; + + let batch::Etching { + divisibility, + premine, + rune, + supply, + symbol, + terms, + } = batchfile.etching.unwrap(); + + { + let supply = supply.to_integer(divisibility).unwrap(); + let premine = premine.to_integer(divisibility).unwrap(); + + let mintable = terms + .map(|terms| terms.cap * terms.amount.to_integer(divisibility).unwrap()) + .unwrap_or_default(); - bitcoin_rpc_server.mine_blocks(1); + assert_eq!(supply, premine + mintable); + } + + let mut mint_definition = Vec::::new(); + + if let Some(terms) = terms { + mint_definition.push("
        ".into()); + mint_definition.push("
        ".into()); + + let mut mintable = true; + + mint_definition.push("
        start
        ".into()); + { + let relative = terms + .offset + .and_then(|range| range.start) + .map(|start| start + block_height); + let absolute = terms.height.and_then(|range| range.start); + + let start = relative + .zip(absolute) + .map(|(relative, absolute)| relative.max(absolute)) + .or(relative) + .or(absolute); + + if let Some(start) = start { + mintable &= block_height + 1 >= start; + mint_definition.push(format!("
        {start}
        ")); + } else { + mint_definition.push("
        none
        ".into()); + } + } + + mint_definition.push("
        end
        ".into()); + { + let relative = terms + .offset + .and_then(|range| range.end) + .map(|end| end + block_height); + let absolute = terms.height.and_then(|range| range.end); + + let end = relative + .zip(absolute) + .map(|(relative, absolute)| relative.min(absolute)) + .or(relative) + .or(absolute); + + if let Some(end) = end { + mintable &= block_height + 1 < end; + mint_definition.push(format!("
        {end}
        ")); + } else { + mint_definition.push("
        none
        ".into()); + } + } + + mint_definition.push("
        amount
        ".into()); + + mint_definition.push(format!( + "
        {}
        ", + Pile { + amount: terms.amount.to_integer(divisibility).unwrap(), + divisibility, + symbol: Some(symbol), + } + )); + + mint_definition.push("
        mints
        ".into()); + mint_definition.push("
        0
        ".into()); + mint_definition.push("
        cap
        ".into()); + mint_definition.push(format!("
        {}
        ", terms.cap)); + mint_definition.push("
        remaining
        ".into()); + mint_definition.push(format!("
        {}
        ", terms.cap)); + + mint_definition.push("
        mintable
        ".into()); + mint_definition.push(format!("
        {mintable}
        ")); + + mint_definition.push("
        ".into()); + mint_definition.push("
        ".into()); + } else { + mint_definition.push("
        no
        ".into()); + } + + let RuneId { block, tx } = id; + + ord.assert_response_regex( + format!("/rune/{rune}"), + format!( + r".*
        id
        +
        {id}
        .* +
        etching block
        +
        {block}
        +
        etching transaction
        +
        {tx}
        +
        mint
        + {} +
        supply
        +
        {premine} {symbol}
        +
        premine
        +
        {premine} {symbol}
        +
        burned
        +
        0 {symbol}
        +
        divisibility
        +
        {divisibility}
        +
        symbol
        +
        {symbol}
        +
        etching
        +
        {reveal}
        +
        parent
        +
        {parent}
        +.*", + mint_definition.join("\\s+"), + ), + ); + + let batch::RuneInfo { + destination, + location, + rune, + } = output.rune.clone().unwrap(); + + if premine.to_integer(divisibility).unwrap() > 0 { + let destination = destination + .unwrap() + .clone() + .require_network(Network::Regtest) + .unwrap(); + + assert!(core.state().is_wallet_address(&destination)); + + let location = location.unwrap(); + + ord.assert_response_regex( + "/runes/balances", + format!( + ".* + {rune} + + + + + + +
        + {location} + + {premine}\u{A0}{symbol} +
        + + .*" + ), + ); + + assert_eq!(core.address(location), destination); + } else { + assert!(destination.is_none()); + assert!(location.is_none()); + } + + let response = ord.json_request("/inscriptions"); + + assert!(response.status().is_success()); + + for id in response.json::().unwrap().ids { + let response = ord.json_request(format!("/inscription/{id}")); + assert!(response.status().is_success()); + if let Some(location) = location { + let inscription = response.json::().unwrap(); + assert!(inscription.satpoint.outpoint != location); + } + } - output + Etched { output, id } } -fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { +fn envelope(payload: &[&[u8]]) -> Witness { let mut builder = bitcoin::script::Builder::new() .push_opcode(bitcoin::opcodes::OP_FALSE) .push_opcode(bitcoin::opcodes::all::OP_IF); @@ -146,12 +424,9 @@ fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { .push_opcode(bitcoin::opcodes::all::OP_ENDIF) .into_script(); - bitcoin::Witness::from_slice(&[script.into_bytes(), Vec::new()]) + Witness::from_slice(&[script.into_bytes(), Vec::new()]) } -fn runes(rpc_server: &test_bitcoincore_rpc::Handle) -> BTreeMap { - CommandBuilder::new("--index-runes --regtest runes") - .bitcoin_rpc_server(rpc_server) - .run_and_deserialize_output::() - .runes +fn default() -> T { + Default::default() } diff --git a/tests/list.rs b/tests/list.rs index 71de89a859..f59e79b314 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -5,11 +5,11 @@ use { #[test] fn output_found() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let output = CommandBuilder::new( "--index-sats list 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0", ) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); assert_eq!( @@ -30,11 +30,11 @@ fn output_found() { #[test] fn output_not_found() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new( "--index-sats list 0000000000000000000000000000000000000000000000000000000000000000:0", ) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_exit_code(1) .expected_stderr("error: output not found\n") .run_and_extract_stdout(); @@ -42,9 +42,9 @@ fn output_not_found() { #[test] fn no_satoshi_index() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("list 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_stderr("error: list requires index created with `--index-sats` flag\n") .expected_exit_code(1) .run_and_extract_stdout(); diff --git a/tests/runes.rs b/tests/runes.rs index 341279ad70..7e9810143f 100644 --- a/tests/runes.rs +++ b/tests/runes.rs @@ -2,15 +2,13 @@ use {super::*, ord::subcommand::runes::Output}; #[test] fn flag_is_required() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]); CommandBuilder::new("--regtest runes") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: `ord runes` requires index created with `--index-runes` flag\n") .run_and_extract_stdout(); @@ -18,13 +16,11 @@ fn flag_is_required() { #[test] fn no_runes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); assert_eq!( CommandBuilder::new("--index-runes --regtest runes") - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .run_and_deserialize_output::(), Output { runes: BTreeMap::new(), @@ -34,42 +30,39 @@ fn no_runes() { #[test] fn one_rune() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let etch = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + let etch = etch(&core, &ord, Rune(RUNE)); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("--index-runes --regtest runes") - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .run_and_deserialize_output::(), Output { runes: vec![( Rune(RUNE), RuneInfo { + block: 8, burned: 0, - mint: None, divisibility: 0, - etching: etch.transaction, - height: 2, - id: RuneId { - height: 2, - index: 1 - }, - index: 1, + etching: etch.output.reveal, + id: RuneId { block: 8, tx: 1 }, + terms: None, mints: 0, number: 0, - rune: Rune(RUNE), - spacers: 0, + premine: 1000, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0 + }, supply: 1000, symbol: Some('¢'), - timestamp: ord::timestamp(2), + timestamp: ord::timestamp(8), + tx: 1, } )] .into_iter() @@ -80,66 +73,63 @@ fn one_rune() { #[test] fn two_runes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let a = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - let b = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE + 1)); + let a = etch(&core, &ord, Rune(RUNE)); + let b = etch(&core, &ord, Rune(RUNE + 1)); pretty_assert_eq!( CommandBuilder::new("--index-runes --regtest runes") - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .run_and_deserialize_output::(), Output { runes: vec![ ( Rune(RUNE), RuneInfo { + block: 8, burned: 0, - mint: None, divisibility: 0, - etching: a.transaction, - height: 2, - id: RuneId { - height: 2, - index: 1 - }, - index: 1, + etching: a.output.reveal, + id: RuneId { block: 8, tx: 1 }, + terms: None, mints: 0, number: 0, - rune: Rune(RUNE), - spacers: 0, + premine: 1000, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0 + }, supply: 1000, symbol: Some('¢'), - timestamp: ord::timestamp(2), + timestamp: ord::timestamp(8), + tx: 1, } ), ( Rune(RUNE + 1), RuneInfo { + block: 16, burned: 0, - mint: None, divisibility: 0, - etching: b.transaction, - height: 4, - id: RuneId { - height: 4, - index: 1 - }, - index: 1, + etching: b.output.reveal, + id: RuneId { block: 16, tx: 1 }, + terms: None, mints: 0, number: 1, - rune: Rune(RUNE + 1), - spacers: 0, + premine: 1000, + rune: SpacedRune { + rune: Rune(RUNE + 1), + spacers: 0 + }, supply: 1000, symbol: Some('¢'), - timestamp: ord::timestamp(4), + timestamp: ord::timestamp(16), + tx: 1, } ) ] diff --git a/tests/server.rs b/tests/server.rs index 5f50b03537..89fb77af08 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -2,7 +2,7 @@ use {super::*, ciborium::value::Integer, ord::subcommand::wallet::send::Output}; #[test] fn run() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let port = TcpListener::bind("127.0.0.1:0") .unwrap() @@ -10,8 +10,8 @@ fn run() { .unwrap() .port(); - let builder = CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}")) - .bitcoin_rpc_server(&rpc_server); + let builder = + CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}")).core(&core); let mut command = builder.command(); @@ -36,19 +36,19 @@ fn run() { #[test] fn inscription_page() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, reveal) = inscribe(&core, &ord); let ethereum_teleburn_address = CommandBuilder::new(format!("teleburn {inscription}")) - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .run_and_deserialize_output::() .ethereum; - TestServer::spawn_with_args(&bitcoin_rpc_server, &[]).assert_response_regex( + TestServer::spawn_with_args(&core, &[]).assert_response_regex( format!("/inscription/{inscription}"), format!( ".*.* @@ -94,16 +94,16 @@ fn inscription_page() { #[test] fn inscription_appears_on_reveal_transaction_page() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (_, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_, reveal) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - TestServer::spawn_with_args(&bitcoin_rpc_server, &[]).assert_response_regex( + TestServer::spawn_with_args(&core, &[]).assert_response_regex( format!("/tx/{reveal}"), format!(".*

        Transaction .*

        .*(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let id0 = output.inscriptions[0].id; let id1 = output.inscriptions[1].id; let reveal = output.reveal; - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/tx/{reveal}"), format!(".*

        Transaction .*

        .*
        Output {reveal}:0.*Inscription 0.*
        location
        \s*
        {reveal}:0:0
        .*", @@ -179,15 +179,15 @@ fn inscription_page_after_send() { let txid = CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stdout_regex(".*") .run_and_deserialize_output::() .txid; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{inscription}"), format!( r".*

        Inscription 0

        .*
        address
        \s*
        bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv
        .*
        location
        \s*
        {txid}:0:0
        .*", @@ -197,16 +197,16 @@ fn inscription_page_after_send() { #[test] fn inscription_content() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let response = ord_rpc_server.request(format!("/content/{inscription}")); + let response = ord.request(format!("/content/{inscription}")); assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -243,29 +243,29 @@ fn inscription_metadata() { ]); ciborium::ser::into_writer(&cbor_map, &mut encoded_metadata).unwrap(); - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let inscription_id = CommandBuilder::new( "wallet inscribe --fee-rate 1 --json-metadata metadata.json --file foo.txt", ) .write("foo.txt", "FOO") .write("metadata.json", metadata) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::() + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() .inscriptions .first() .unwrap() .id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let response = ord_rpc_server.request(format!("/r/metadata/{inscription_id}")); + let response = ord.request(format!("/r/metadata/{inscription_id}")); assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -280,24 +280,23 @@ fn inscription_metadata() { #[test] fn recursive_inscription_endpoint() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --file foo.txt") .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let inscription = output.inscriptions.first().unwrap(); - let response = ord_rpc_server.request(format!("/r/inscription/{}", inscription.id)); + let response = ord.request(format!("/r/inscription/{}", inscription.id)); assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -311,7 +310,7 @@ fn recursive_inscription_endpoint() { pretty_assert_eq!( inscription_recursive_json, api::InscriptionRecursive { - charms: vec!["coin".into(), "uncommon".into()], + charms: vec![Charm::Coin, Charm::Uncommon], content_type: Some("text/plain;charset=utf-8".to_string()), content_length: Some(3), fee: 138, @@ -332,14 +331,14 @@ fn recursive_inscription_endpoint() { #[test] fn inscriptions_page() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( "/inscriptions", format!( ".*

        All Inscriptions

        @@ -353,33 +352,33 @@ fn inscriptions_page() { #[test] fn inscriptions_page_is_sorted() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let mut regex = String::new(); for _ in 0..8 { - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); regex.insert_str(0, &format!(".*
        .*")); } - ord_rpc_server.assert_response_regex("/inscriptions", ®ex); + ord.assert_response_regex("/inscriptions", ®ex); } #[test] fn inscriptions_page_has_next_and_previous() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (a, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - let (b, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - let (c, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (a, _) = inscribe(&core, &ord); + let (b, _) = inscribe(&core, &ord); + let (c, _) = inscribe(&core, &ord); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{b}"), format!( ".*

        Inscription 1

        .* @@ -394,9 +393,9 @@ fn inscriptions_page_has_next_and_previous() { #[test] fn expected_sat_time_is_rounded() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( + TestServer::spawn_with_args(&core, &[]).assert_response_regex( "/sat/2099999997689999", r".*
        timestamp
        \(expected\)
        .*", ); @@ -404,16 +403,16 @@ fn expected_sat_time_is_rounded() { #[test] fn missing_credentials() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("--bitcoin-rpc-username foo server") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_exit_code(1) .expected_stderr("error: no bitcoin RPC password specified\n") .run_and_extract_stdout(); CommandBuilder::new("--bitcoin-rpc-password bar server") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .expected_exit_code(1) .expected_stderr("error: no bitcoin RPC username specified\n") .run_and_extract_stdout(); @@ -421,11 +420,11 @@ fn missing_credentials() { #[test] fn all_endpoints_in_recursive_directory_return_json() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - bitcoin_rpc_server.mine_blocks(2); + core.mine_blocks(2); - let ord_server = TestServer::spawn_with_args(&bitcoin_rpc_server, &[]); + let ord_server = TestServer::spawn_with_args(&core, &[]); assert_eq!( ord_server.request("/r/blockheight").json::().unwrap(), @@ -454,11 +453,11 @@ fn all_endpoints_in_recursive_directory_return_json() { #[test] fn sat_recursive_endpoints_without_sat_index_return_404() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let server = TestServer::spawn_with_args(&bitcoin_rpc_server, &[""]); + let server = TestServer::spawn_with_args(&core, &[""]); assert_eq!( server.request("/r/sat/5000000000").status(), @@ -473,43 +472,42 @@ fn sat_recursive_endpoints_without_sat_index_return_404() { #[test] fn inscription_transactions_are_stored_with_transaction_index() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-transactions"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-transactions"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (_inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_inscription, reveal) = inscribe(&core, &ord); - let coinbase = bitcoin_rpc_server.tx(1, 0).txid(); + let coinbase = core.tx(1, 0).txid(); assert_eq!( - ord_rpc_server.request(format!("/tx/{reveal}")).status(), + ord.request(format!("/tx/{reveal}")).status(), StatusCode::OK, ); assert_eq!( - ord_rpc_server.request(format!("/tx/{coinbase}")).status(), + ord.request(format!("/tx/{coinbase}")).status(), StatusCode::OK, ); - bitcoin_rpc_server.clear_state(); + core.clear_state(); assert_eq!( - ord_rpc_server.request(format!("/tx/{reveal}")).status(), + ord.request(format!("/tx/{reveal}")).status(), StatusCode::OK, ); assert_eq!( - ord_rpc_server.request(format!("/tx/{coinbase}")).status(), + ord.request(format!("/tx/{coinbase}")).status(), StatusCode::NOT_FOUND, ); } #[test] fn run_no_sync() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let port = TcpListener::bind("127.0.0.1:0") .unwrap() @@ -520,14 +518,14 @@ fn run_no_sync() { let tempdir = Arc::new(TempDir::new().unwrap()); let builder = CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}",)) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .temp_dir(tempdir.clone()); let mut command = builder.command(); let mut child = command.spawn().unwrap(); - rpc_server.mine_blocks(1); + core.mine_blocks(1); for attempt in 0.. { if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockheight")) { @@ -549,14 +547,14 @@ fn run_no_sync() { let builder = CommandBuilder::new(format!( "server --no-sync --address 127.0.0.1 --http-port {port}", )) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .temp_dir(tempdir); let mut command = builder.command(); let mut child = command.spawn().unwrap(); - rpc_server.mine_blocks(2); + core.mine_blocks(2); for attempt in 0.. { if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockheight")) { @@ -578,7 +576,7 @@ fn run_no_sync() { #[test] fn authentication() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let port = TcpListener::bind("127.0.0.1:0") .unwrap() @@ -589,7 +587,7 @@ fn authentication() { let builder = CommandBuilder::new(format!( " --server-username foo --server-password bar server --address 127.0.0.1 --http-port {port}" )) - .bitcoin_rpc_server(&rpc_server); + .core(&core); let mut command = builder.command(); @@ -619,3 +617,82 @@ fn authentication() { child.kill().unwrap(); } + +#[cfg(unix)] +#[test] +fn ctrl_c() { + use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, + }; + + let core = mockcore::spawn(); + + let port = TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port(); + + let tempdir = Arc::new(TempDir::new().unwrap()); + + core.mine_blocks(3); + + let mut spawn = CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}")) + .temp_dir(tempdir.clone()) + .core(&core) + .spawn(); + + for attempt in 0.. { + if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockcount")) { + if response.status() == 200 || response.text().unwrap() == *"3" { + break; + } + } + + if attempt == 100 { + panic!("Server did not respond to status check",); + } + + thread::sleep(Duration::from_millis(50)); + } + + signal::kill( + Pid::from_raw(spawn.child.id().try_into().unwrap()), + Signal::SIGINT, + ) + .unwrap(); + + let mut buffer = String::new(); + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!( + buffer, + "Shutting down gracefully. Press again to shutdown immediately.\n" + ); + + spawn.child.wait().unwrap(); + + CommandBuilder::new(format!( + "server --no-sync --address 127.0.0.1 --http-port {port}" + )) + .temp_dir(tempdir) + .core(&core) + .spawn(); + + for attempt in 0.. { + if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockcount")) { + if response.status() == 200 || response.text().unwrap() == *"3" { + break; + } + } + + if attempt == 100 { + panic!("Server did not respond to status check",); + } + + thread::sleep(Duration::from_millis(50)); + } +} diff --git a/tests/supply.rs b/tests/supply.rs index 9e5f396af2..6c6366831b 100644 --- a/tests/supply.rs +++ b/tests/supply.rs @@ -1,10 +1,10 @@ -use {super::*, ord::subcommand::supply::Output}; +use super::*; #[test] fn genesis() { assert_eq!( - CommandBuilder::new("supply").run_and_deserialize_output::(), - Output { + CommandBuilder::new("supply").run_and_deserialize_output::(), + Supply { supply: 2099999997690000, first: 0, last: 2099999997689999, diff --git a/tests/test_server.rs b/tests/test_server.rs index 03781b33e0..e351418f9c 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -15,19 +15,16 @@ pub(crate) struct TestServer { } impl TestServer { - pub(crate) fn spawn(bitcoin_rpc_server: &test_bitcoincore_rpc::Handle) -> Self { - Self::spawn_with_server_args(bitcoin_rpc_server, &[], &[]) + pub(crate) fn spawn(core: &mockcore::Handle) -> Self { + Self::spawn_with_server_args(core, &[], &[]) } - pub(crate) fn spawn_with_args( - bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, - ord_args: &[&str], - ) -> Self { - Self::spawn_with_server_args(bitcoin_rpc_server, ord_args, &[]) + pub(crate) fn spawn_with_args(core: &mockcore::Handle, ord_args: &[&str]) -> Self { + Self::spawn_with_server_args(core, ord_args, &[]) } pub(crate) fn spawn_with_server_args( - bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, + core: &mockcore::Handle, ord_args: &[&str], ord_server_args: &[&str], ) -> Self { @@ -44,8 +41,8 @@ impl TestServer { .port(); let (settings, server) = parse_ord_server_args(&format!( - "ord --bitcoin-rpc-url {} --cookie-file {} --bitcoin-data-dir {} --data-dir {} {} server {} --http-port {port} --address 127.0.0.1", - bitcoin_rpc_server.url(), + "ord --bitcoin-rpc-url {} --cookie-file {} --bitcoin-data-dir {} --datadir {} {} server {} --http-port {port} --address 127.0.0.1", + core.url(), cookiefile.to_str().unwrap(), tempdir.path().display(), tempdir.path().display(), @@ -76,7 +73,7 @@ impl TestServer { } Self { - bitcoin_rpc_url: bitcoin_rpc_server.url(), + bitcoin_rpc_url: core.url(), ord_server_handle, port, tempdir, @@ -87,14 +84,18 @@ impl TestServer { format!("http://127.0.0.1:{}", self.port).parse().unwrap() } + #[track_caller] pub(crate) fn assert_response_regex(&self, path: impl AsRef, regex: impl AsRef) { self.sync_server(); - + let path = path.as_ref(); let response = reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_regex_match!(response.text().unwrap(), regex.as_ref()); + let status = response.status(); + assert_eq!(status, StatusCode::OK, "bad status for {path}: {status}"); + let text = response.text().unwrap(); + assert_regex_match!(text, regex.as_ref()); } + #[track_caller] pub(crate) fn assert_response(&self, path: impl AsRef, expected_response: &str) { self.sync_server(); let response = reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap(); @@ -128,21 +129,9 @@ impl TestServer { pub(crate) fn sync_server(&self) { let client = Client::new(&self.bitcoin_rpc_url, Auth::None).unwrap(); let chain_block_count = client.get_block_count().unwrap() + 1; - - for i in 0.. { - let response = reqwest::blocking::get(self.url().join("/blockcount").unwrap()).unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let ord_height = response.text().unwrap().parse::().unwrap(); - - if ord_height >= chain_block_count { - break; - } else if i == 20 { - panic!("index failed to synchronize with chain"); - } - thread::sleep(Duration::from_millis(50)); - } + let response = reqwest::blocking::get(self.url().join("/update").unwrap()).unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert!(response.text().unwrap().parse::().unwrap() >= chain_block_count); } } diff --git a/tests/wallet.rs b/tests/wallet.rs index 91cdd81c66..44dd41bb7c 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -2,14 +2,19 @@ use super::*; mod authentication; mod balance; +mod batch_command; mod cardinals; mod create; mod dump; mod inscribe; mod inscriptions; +mod mint; mod outputs; mod receive; mod restore; +#[cfg(unix)] +mod resume; mod sats; +mod selection; mod send; mod transactions; diff --git a/tests/wallet/authentication.rs b/tests/wallet/authentication.rs index d639ac1a43..a7d377a267 100644 --- a/tests/wallet/authentication.rs +++ b/tests/wallet/authentication.rs @@ -2,31 +2,31 @@ use {super::*, ord::subcommand::wallet::balance::Output}; #[test] fn authentication() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args( - &bitcoin_rpc_server, + let ord = TestServer::spawn_with_server_args( + &core, &["--server-username", "foo", "--server-password", "bar"], &[], ); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); assert_eq!( CommandBuilder::new("--server-username foo --server-password bar wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::() .cardinal, 0 ); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); assert_eq!( CommandBuilder::new("--server-username foo --server-password bar wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(), Output { cardinal: 50 * COIN_VALUE, diff --git a/tests/wallet/balance.rs b/tests/wallet/balance.rs index 120029a895..c5d5ea4001 100644 --- a/tests/wallet/balance.rs +++ b/tests/wallet/balance.rs @@ -1,30 +1,30 @@ -use {super::*, ord::subcommand::wallet::balance::Output}; +use super::*; #[test] fn wallet_balance() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); assert_eq!( CommandBuilder::new("wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::() + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() .cardinal, 0 ); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); assert_eq!( CommandBuilder::new("wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - Output { + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { cardinal: 50 * COIN_VALUE, ordinal: 0, runic: None, @@ -36,18 +36,18 @@ fn wallet_balance() { #[test] fn inscribed_utxos_are_deducted_from_cardinal() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); assert_eq!( CommandBuilder::new("wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - Output { + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { cardinal: 0, ordinal: 0, runic: None, @@ -56,14 +56,14 @@ fn inscribed_utxos_are_deducted_from_cardinal() { } ); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); + inscribe(&core, &ord); assert_eq!( CommandBuilder::new("wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - Output { + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { cardinal: 100 * COIN_VALUE - 10_000, ordinal: 10_000, runic: None, @@ -75,21 +75,18 @@ fn inscribed_utxos_are_deducted_from_cardinal() { #[test] fn runic_utxos_are_deducted_from_cardinal() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - Output { + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { cardinal: 0, ordinal: 0, runic: Some(0), @@ -98,37 +95,61 @@ fn runic_utxos_are_deducted_from_cardinal() { } ); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + let rune = Rune(RUNE); + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + premine: "1000".parse().unwrap(), + rune: SpacedRune { rune, spacers: 1 }, + supply: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - Output { - cardinal: 100 * COIN_VALUE - 10_000, - ordinal: 0, + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 50 * COIN_VALUE * 8 - 20_000, + ordinal: 10000, runic: Some(10_000), - runes: Some(vec![(Rune(RUNE), 1000)].into_iter().collect()), - total: 100 * COIN_VALUE, + runes: Some( + vec![(SpacedRune { rune, spacers: 1 }, 1000)] + .into_iter() + .collect() + ), + total: 50 * COIN_VALUE * 8, } ); } #[test] fn unsynced_wallet_fails_with_unindexed_output() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); assert_eq!( CommandBuilder::new("wallet balance") - .ord_rpc_server(&ord_rpc_server) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .run_and_deserialize_output::(), - Output { + .ord(&ord) + .core(&core) + .run_and_deserialize_output::(), + Balance { cardinal: 50 * COIN_VALUE, ordinal: 0, runic: None, @@ -137,21 +158,20 @@ fn unsynced_wallet_fails_with_unindexed_output() { } ); - let no_sync_ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &["--no-sync"]); + let no_sync_ord = TestServer::spawn_with_server_args(&core, &[], &["--no-sync"]); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); + inscribe(&core, &ord); CommandBuilder::new("wallet balance") - .ord_rpc_server(&no_sync_ord_rpc_server) - .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord(&no_sync_ord) + .core(&core) .expected_exit_code(1) - .expected_stderr("error: wallet failed to synchronize with ord server\n") + .expected_stderr("error: wallet failed to synchronize with `ord server` after 20 attempts\n") .run_and_extract_stdout(); CommandBuilder::new("wallet --no-sync balance") - .ord_rpc_server(&no_sync_ord_rpc_server) - .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord(&no_sync_ord) + .core(&core) .expected_exit_code(1) .stderr_regex(r"error: output in wallet but not in ord server: [[:xdigit:]]{64}:\d+.*") .run_and_extract_stdout(); diff --git a/tests/wallet/batch_command.rs b/tests/wallet/batch_command.rs new file mode 100644 index 0000000000..2b8293f910 --- /dev/null +++ b/tests/wallet/batch_command.rs @@ -0,0 +1,2623 @@ +use {super::*, ord::subcommand::wallet::send, pretty_assertions::assert_eq}; + +fn receive(core: &mockcore::Handle, ord: &TestServer) -> Address { + let address = CommandBuilder::new("wallet receive") + .core(core) + .ord(ord) + .run_and_deserialize_output::() + .addresses + .into_iter() + .next() + .unwrap(); + + address.require_network(core.state().network).unwrap() +} + +#[test] +fn batch_inscribe_fails_if_batchfile_has_no_inscriptions() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("batch.yaml", "mode: shared-output\ninscriptions: []\n") + .core(&core) + .ord(&ord) + .stderr_regex(".*batchfile must contain at least one inscription.*") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_can_create_one_inscription() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n metadata: 123\n metaprotocol: foo", + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + let request = ord.request(format!("/content/{}", output.inscriptions[0].id)); + + assert_eq!(request.status(), 200); + assert_eq!( + request.headers().get("content-type").unwrap(), + "text/plain;charset=utf-8" + ); + assert_eq!(request.text().unwrap(), "Hello World"); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + r".*
        metadata
        \s*
        \n 123\n
        .*
        metaprotocol
        \s*
        foo
        .*", + ); +} + +#[test] +fn batch_inscribe_with_multiple_inscriptions() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + let request = ord.request(format!("/content/{}", output.inscriptions[0].id)); + assert_eq!(request.status(), 200); + assert_eq!( + request.headers().get("content-type").unwrap(), + "text/plain;charset=utf-8" + ); + assert_eq!(request.text().unwrap(), "Hello World"); + + let request = ord.request(format!("/content/{}", output.inscriptions[1].id)); + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/png"); + + let request = ord.request(format!("/content/{}", output.inscriptions[2].id)); + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); +} + +#[test] +fn batch_inscribe_with_multiple_inscriptions_with_parent() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --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: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + r".*
        parents
        \s*
        .*
        .*", + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + r".*
        parents
        \s*
        .*
        .*", + ); + + let request = ord.request(format!("/content/{}", output.inscriptions[2].id)); + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); +} + +#[test] +fn batch_inscribe_respects_dry_run_flag() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml --dry-run") + .write("inscription.txt", "Hello World") + .write( + "batch.yaml", + "mode: shared-output\ninscriptions:\n- file: inscription.txt\n", + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert!(core.mempool().is_empty()); + + let request = ord.request(format!("/content/{}", output.inscriptions[0].id)); + + assert_eq!(request.status(), 404); +} + +#[test] +fn batch_in_same_output_but_different_satpoints() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --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\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + let outpoint = output.inscriptions[0].location.outpoint; + for (i, inscription) in output.inscriptions.iter().enumerate() { + assert_eq!( + inscription.location, + SatPoint { + outpoint, + offset: u64::try_from(i).unwrap() * 10_000, + } + ); + } + + core.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        location
        .*
        {}:10000
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        location
        .*
        {}:20000
        .*", + outpoint + ), + ); + + ord.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_in_same_output_with_non_default_postage() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --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\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + let outpoint = output.inscriptions[0].location.outpoint; + + for (i, inscription) in output.inscriptions.iter().enumerate() { + assert_eq!( + inscription.location, + SatPoint { + outpoint, + offset: u64::try_from(i).unwrap() * 777, + } + ); + } + + core.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        location
        .*
        {}:777
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        location
        .*
        {}:1554
        .*", + outpoint + ), + ); + + ord.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_in_separate_outputs_with_parent() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --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: separate-outputs\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + let mut outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + outpoints.sort(); + outpoints.dedup(); + assert_eq!(outpoints.len(), output.inscriptions.len()); + + core.mine_blocks(1); + + let output_1 = output.inscriptions[0].location.outpoint; + let output_2 = output.inscriptions[1].location.outpoint; + let output_3 = output.inscriptions[2].location.outpoint; + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        10000
        .*.*
        location
        .*
        {}:0
        .*", + output_1 + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        10000
        .*.*
        location
        .*
        {}:0
        .*", + output_2 + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        10000
        .*.*
        location
        .*
        {}:0
        .*", + output_3 + ), + ); +} + +#[test] +fn batch_in_separate_outputs_with_parent_and_non_default_postage() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --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: separate-outputs\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + + let mut outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + outpoints.sort(); + outpoints.dedup(); + assert_eq!(outpoints.len(), output.inscriptions.len()); + + core.mine_blocks(1); + + let output_1 = output.inscriptions[0].location.outpoint; + let output_2 = output.inscriptions[1].location.outpoint; + let output_3 = output.inscriptions[2].location.outpoint; + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        777
        .*.*
        location
        .*
        {}:0
        .*", + output_1 + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        777
        .*.*
        location
        .*
        {}:0
        .*", + output_2 + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        777
        .*.*
        location
        .*
        {}:0
        .*", + output_3 + ), + ); +} + +#[test] +fn batch_inscribe_fails_if_invalid_network_destination_address() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("batch.yaml", "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + .core(&core) + .ord(&ord) + .stderr_regex("error: address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 belongs to network bitcoin which is different from required regtest\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", "") + .write("batch.yaml", "mode: shared-output\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") + .run_and_extract_stdout(); + + CommandBuilder::new("wallet batch --fee-rate 2.1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", "") + .write("batch.yaml", "mode: same-sat\nsat: 5000000000\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_works_with_some_destinations_set_and_others_not() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "\ +mode: separate-outputs +inscriptions: +- file: inscription.txt + destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 +- file: tulip.png +- file: meow.wav + destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k +", + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + ".* +
        address
        +
        bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
        .*", + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + ".* +
        address
        +
        {}
        .*", + core.state().change_addresses[0], + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + ".* +
        address
        +
        bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
        .*", + ); +} + +#[test] +fn batch_same_sat() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet batch --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" + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + core.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.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 core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("wallet batch --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") + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + core.mine_blocks(1); + + let txid = output.inscriptions[0].location.outpoint.txid; + + ord.assert_response_regex( + format!("/inscription/{}", parent_id), + format!( + r".*
        location
        .*
        {}:0:0
        .*", + txid + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        location
        .*
        {}:1:0
        .*", + txid + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        location
        .*
        {}:1:0
        .*", + txid + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        location
        .*
        {}:1:0
        .*", + txid + ), + ); + + ord.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_satpoint_and_reinscription() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let inscription_id = output.inscriptions[0].id; + let satpoint = output.inscriptions[0].location; + + CommandBuilder::new("wallet batch --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\nsatpoint: {}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) + ) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .stderr_regex(".*error: sat at .*:0:0 already inscribed.*") + .run_and_extract_stdout(); + + let output = CommandBuilder::new("wallet batch --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\nsatpoint: {}\nreinscribe: true\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert_eq!( + output.inscriptions[0].location, + output.inscriptions[1].location + ); + assert_eq!( + output.inscriptions[1].location, + output.inscriptions[2].location + ); + + core.mine_blocks(1); + + let outpoint = output.inscriptions[0].location.outpoint; + + ord.assert_response_regex( + format!("/inscription/{}", inscription_id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[0].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[1].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", output.inscriptions[2].id), + format!( + r".*
        location
        .*
        {}:0
        .*", + outpoint + ), + ); + + ord.assert_response_regex( + format!("/output/{}", output.inscriptions[0].location.outpoint), + format!(r".*.*.*.*.*.*.*.*.*", inscription_id, output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), + ); +} + +#[test] +fn batch_inscribe_with_sat_argument_with_parent() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let parent_output = + CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + assert_eq!(core.descriptors().len(), 3); + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("--index-sats wallet batch --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") + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + ord.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 core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("wallet batch --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" + ) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: neither `sat` nor `satpoint` can be set in `same-sat` mode\n") + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_with_satpoint() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + let txid = core.mine_blocks(1)[0].txdata[0].txid(); + + let output = CommandBuilder::new("wallet batch --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\nsatpoint: {txid}:0:55555\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", ) + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + ord.assert_response_regex( + "/sat/5000055555", + format!( + ".*.*.*.*", + output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id + ), + ); +} + +#[test] +fn batch_inscribe_with_fee_rate() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(2); + + let set_fee_rate = 1.0; + + let output = CommandBuilder::new(format!("--index-sats wallet batch --fee-rate {set_fee_rate} --batch batch.yaml")) + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + let commit_tx = &core.mempool()[0]; + let mut fee = 0; + for input in &commit_tx.input { + fee += core + .get_utxo_amount(&input.previous_output) + .unwrap() + .to_sat(); + } + for output in &commit_tx.output { + fee -= output.value; + } + let fee_rate = fee as f64 / commit_tx.vsize() as f64; + pretty_assert_eq!(fee_rate, set_fee_rate); + + let reveal_tx = &core.mempool()[1]; + let mut fee = 0; + for input in &reveal_tx.input { + fee += &commit_tx.output[input.previous_output.vout as usize].value; + } + for output in &reveal_tx.output { + fee -= output.value; + } + let fee_rate = fee as f64 / reveal_tx.vsize() as f64; + pretty_assert_eq!(fee_rate, set_fee_rate); + + assert_eq!( + ord::FeeRate::try_from(set_fee_rate) + .unwrap() + .fee(commit_tx.vsize() + reveal_tx.vsize()) + .to_sat(), + output.total_fees + ); +} + +#[test] +fn batch_inscribe_with_delegate_inscription() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (delegate, _) = inscribe(&core, &ord); + + let inscribe = CommandBuilder::new("wallet batch --fee-rate 1.0 --batch batch.yaml") + .write("inscription.txt", "INSCRIPTION") + .write( + "batch.yaml", + format!( + "mode: shared-output +inscriptions: +- delegate: {delegate} + file: inscription.txt +" + ), + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + ord.assert_response_regex( + format!("/inscription/{}", inscribe.inscriptions[0].id), + format!(r#".*
        delegate
        \s*
        {delegate}
        .*"#,), + ); + + ord.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); +} + +#[test] +fn batch_inscribe_with_non_existent_delegate_inscription() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; + + CommandBuilder::new("wallet batch --fee-rate 1.0 --batch batch.yaml") + .write("hello.txt", "Hello, world!") + .write( + "batch.yaml", + format!( + "mode: shared-output +inscriptions: +- delegate: {delegate} + file: hello.txt +" + ), + ) + .core(&core) + .ord(&ord) + .expected_stderr(format!("error: delegate {delegate} does not exist\n")) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn batch_inscribe_with_satpoints_with_parent() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let parent_output = + CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") + .write("parent.png", [1; 520]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let txids = core + .mine_blocks(3) + .iter() + .map(|block| block.txdata[0].txid()) + .collect::>(); + + let satpoint_1 = SatPoint { + outpoint: OutPoint { + txid: txids[0], + vout: 0, + }, + offset: 0, + }; + + let satpoint_2 = SatPoint { + outpoint: OutPoint { + txid: txids[1], + vout: 0, + }, + offset: 0, + }; + + let satpoint_3 = SatPoint { + outpoint: OutPoint { + txid: txids[2], + vout: 0, + }, + offset: 0, + }; + + let sat_1 = serde_json::from_str::( + &ord + .json_request(format!("/output/{}", satpoint_1.outpoint)) + .text() + .unwrap(), + ) + .unwrap() + .sat_ranges + .unwrap()[0] + .0; + + let sat_2 = serde_json::from_str::( + &ord + .json_request(format!("/output/{}", satpoint_2.outpoint)) + .text() + .unwrap(), + ) + .unwrap() + .sat_ranges + .unwrap()[0] + .0; + + let sat_3 = serde_json::from_str::( + &ord + .json_request(format!("/output/{}", satpoint_3.outpoint)) + .text() + .unwrap(), + ) + .unwrap() + .sat_ranges + .unwrap()[0] + .0; + + let parent_id = parent_output.inscriptions[0].id; + + let output = CommandBuilder::new("--index-sats wallet batch --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!( + r#" +mode: satpoints +parent: {parent_id} +inscriptions: +- file: inscription.txt + satpoint: {} +- file: tulip.png + satpoint: {} +- file: meow.wav + satpoint: {} +"#, + satpoint_1, satpoint_2, satpoint_3 + ), + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + ord.assert_response_regex( + format!("/inscription/{}", parent_id), + format!( + r".*
        location
        .*
        {}:0:0
        .*", + output.reveal + ), + ); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + + let outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + + assert_eq!(outpoints.len(), output.inscriptions.len()); + + let inscription_1 = &output.inscriptions[0]; + let inscription_2 = &output.inscriptions[1]; + let inscription_3 = &output.inscriptions[2]; + + ord.assert_response_regex( + format!("/inscription/{}", inscription_1.id), + format!(r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", + 50 * COIN_VALUE, + sat_1, + inscription_1.location, + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", inscription_2.id), + format!(r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", + 50 * COIN_VALUE, + sat_2, + inscription_2.location + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", inscription_3.id), + format!(r".*
        parents
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", + 50 * COIN_VALUE, + sat_3, + inscription_3.location + ), + ); +} + +#[test] +fn batch_inscribe_with_satpoints_with_different_sizes() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + let address_1 = receive(&core, &ord); + let address_2 = receive(&core, &ord); + let address_3 = receive(&core, &ord); + + core.mine_blocks(3); + + let outpoint_1 = OutPoint { + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_1} 25btc" + )) + .core(&core) + .ord(&ord) + .stdout_regex(r".*") + .run_and_deserialize_output::() + .txid, + vout: 0, + }; + + core.mine_blocks(1); + + let outpoint_2 = OutPoint { + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_2} 1btc" + )) + .core(&core) + .ord(&ord) + .stdout_regex(r".*") + .run_and_deserialize_output::() + .txid, + vout: 0, + }; + + core.mine_blocks(1); + + let outpoint_3 = OutPoint { + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_3} 3btc" + )) + .core(&core) + .ord(&ord) + .stdout_regex(r".*") + .run_and_deserialize_output::() + .txid, + vout: 0, + }; + + core.mine_blocks(1); + + let satpoint_1 = SatPoint { + outpoint: outpoint_1, + offset: 0, + }; + + let satpoint_2 = SatPoint { + outpoint: outpoint_2, + offset: 0, + }; + + let satpoint_3 = SatPoint { + outpoint: outpoint_3, + offset: 0, + }; + + let output_1 = serde_json::from_str::( + &ord + .json_request(format!("/output/{}", satpoint_1.outpoint)) + .text() + .unwrap(), + ) + .unwrap(); + assert_eq!(output_1.value, 25 * COIN_VALUE); + + let output_2 = serde_json::from_str::( + &ord + .json_request(format!("/output/{}", satpoint_2.outpoint)) + .text() + .unwrap(), + ) + .unwrap(); + assert_eq!(output_2.value, COIN_VALUE); + + let output_3 = serde_json::from_str::( + &ord + .json_request(format!("/output/{}", satpoint_3.outpoint)) + .text() + .unwrap(), + ) + .unwrap(); + assert_eq!(output_3.value, 3 * COIN_VALUE); + + let sat_1 = output_1.sat_ranges.unwrap()[0].0; + let sat_2 = output_2.sat_ranges.unwrap()[0].0; + let sat_3 = output_3.sat_ranges.unwrap()[0].0; + + let output = CommandBuilder::new("--index-sats wallet batch --fee-rate 1 --batch batch.yaml") + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 5]) + .write("meow.wav", [0; 2]) + .write( + "batch.yaml", + format!( + r#" +mode: satpoints +inscriptions: +- file: inscription.txt + satpoint: {} +- file: tulip.png + satpoint: {} +- file: meow.wav + satpoint: {} +"#, + satpoint_1, satpoint_2, satpoint_3 + ), + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + for inscription in &output.inscriptions { + assert_eq!(inscription.location.offset, 0); + } + + let outpoints = output + .inscriptions + .iter() + .map(|inscription| inscription.location.outpoint) + .collect::>(); + + assert_eq!(outpoints.len(), output.inscriptions.len()); + + let inscription_1 = &output.inscriptions[0]; + let inscription_2 = &output.inscriptions[1]; + let inscription_3 = &output.inscriptions[2]; + + ord.assert_response_regex( + format!("/inscription/{}", inscription_1.id), + format!( + r".*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", + 25 * COIN_VALUE, + sat_1, + inscription_1.location + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", inscription_2.id), + format!( + r".*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", + COIN_VALUE, + sat_2, + inscription_2.location + ), + ); + + ord.assert_response_regex( + format!("/inscription/{}", inscription_3.id), + format!( + r".*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", + 3 * COIN_VALUE, + sat_3, + inscription_3.location + ), + ); +} + +#[test] +fn batch_can_etch_rune() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let rune = SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }; + + let batch = batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.output.inscriptions[0].id; + + let request = ord.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord.assert_response_regex( + format!("/inscription/{parent}"), + r".*
        rune
        \s*
        AAAAAAAAAAAAA
        .*", + ); + + ord.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
        parent
        \s*
        {parent}
        .*" + ), + ); + + let destination = batch + .output + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap(); + + assert!(core.state().is_wallet_address(&destination)); + + let reveal = core.tx_by_id(batch.output.reveal); + + assert_eq!( + reveal.input[0].sequence, + Sequence::from_height(Runestone::COMMIT_INTERVAL) + ); + + let Artifact::Runestone(runestone) = Runestone::decipher(&reveal).unwrap() else { + panic!(); + }; + + let pointer = reveal.output.len() - 2; + + assert_eq!(runestone.pointer, Some(pointer.try_into().unwrap())); + + assert_eq!( + reveal.output[pointer].script_pubkey, + destination.script_pubkey(), + ); + + assert_eq!( + CommandBuilder::new("--regtest wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 44999980000, + ordinal: 10000, + runic: Some(10000), + runes: Some(vec![(rune, 1000)].into_iter().collect()), + total: 450 * COIN_VALUE, + } + ); +} + +#[test] +fn batch_can_etch_rune_without_premine() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let rune = SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }; + + let batch = batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune, + supply: "1000".parse().unwrap(), + premine: "0".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + amount: "1000".parse().unwrap(), + height: None, + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.output.inscriptions[0].id; + + let request = ord.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord.assert_response_regex( + format!("/inscription/{parent}"), + r".*
        rune
        \s*
        AAAAAAAAAAAAA
        .*", + ); + + ord.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
        parent
        \s*
        {parent}
        .*" + ), + ); + + assert_eq!(batch.output.rune.unwrap().destination, None); + + let reveal = core.tx_by_id(batch.output.reveal); + + assert_eq!( + reveal.input[0].sequence, + Sequence::from_height(Runestone::COMMIT_INTERVAL) + ); + + assert_eq!( + CommandBuilder::new("--regtest wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 44999990000, + ordinal: 10000, + runic: Some(0), + runes: Some(default()), + total: 450 * COIN_VALUE, + } + ); +} + +#[test] +fn batch_inscribe_can_etch_rune_with_offset() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let batch = batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "10000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 9, + amount: "1000".parse().unwrap(), + offset: Some(batch::Range { + start: Some(10), + end: Some(20), + }), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.output.inscriptions[0].id; + + let request = ord.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord.assert_response_regex( + format!("/inscription/{parent}"), + r".*
        rune
        \s*
        AAAAAAAAAAAAA
        .*", + ); + + ord.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
        parent
        \s*
        {parent}
        .*" + ), + ); + + assert!(core.state().is_wallet_address( + &batch + .output + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn batch_inscribe_can_etch_rune_with_height() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let batch = batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "10000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 9, + amount: "1000".parse().unwrap(), + height: Some(batch::Range { + start: Some(10), + end: Some(20), + }), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.output.inscriptions[0].id; + + let request = ord.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord.assert_response_regex( + format!("/inscription/{parent}"), + r".*
        rune
        \s*
        AAAAAAAAAAAAA
        .*", + ); + + ord.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
        parent
        \s*
        {parent}
        .*" + ), + ); + + assert!(core.state().is_wallet_address( + &batch + .output + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn etch_existing_rune_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + etch(&core, &ord, Rune(RUNE)); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 1, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_reserved_rune_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune::reserved(0, 0), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + supply: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_sub_minimum_rune_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: rune is less than minimum for next block: A < ZZQYZPATYGGX\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_requires_rune_index() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: etching runes requires index created with `--index-runes`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_divisibility_over_maximum_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 39, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: must be less than or equal 38\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_mintable_overflow_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: default(), + premine: default(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 2, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "340282366920938463463374607431768211455".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `terms.count` * `terms.amount` over maximum\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_mintable_plus_premine_overflow_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: default(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "340282366920938463463374607431768211455".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `premine` + `terms.count` * `terms.amount` over maximum\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn incorrect_supply_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `supply` not equal to `premine` + `terms.count` * `terms.amount`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_offset_interval_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: Some(2), + }), + amount: "1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `terms.offset.end` must be greater than `terms.offset.start`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_height_interval_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + end: Some(2), + start: Some(2), + }), + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `terms.height.end` must be greater than `terms.height.start`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn invalid_start_height_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + end: None, + start: Some(0), + }), + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr( + "error: `terms.height.start` must be greater than the reveal transaction block height of 8\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn invalid_end_height_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + start: None, + end: Some(0), + }), + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr( + "error: `terms.height.end` must be greater than the reveal transaction block height of 8\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_supply_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "0".parse().unwrap(), + premine: "0".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `supply` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_cap_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 0, + height: None, + amount: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `terms.cap` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_amount_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: None, + amount: "0".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: `terms.amount` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn oversize_runestone_error() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(6402364363415443603228541259936211926 - 1), + spacers: 0b00000111_11111111_11111111_11111111, + }, + supply: u128::MAX.to_string().parse().unwrap(), + premine: (u128::MAX - 1).to_string().parse().unwrap(), + symbol: '\u{10FFFF}', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + offset: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + amount: "1".parse().unwrap(), + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .expected_stderr("error: runestone greater than maximum OP_RETURN size: 104 > 82\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn oversize_runestones_are_allowed_with_no_limit() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new( + "--regtest --index-runes wallet batch --fee-rate 0 --dry-run --no-limit --batch batch.yaml", + ) + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(6402364363415443603228541259936211926 - 1), + spacers: 0b00000111_11111111_11111111_11111111, + }, + supply: u128::MAX.to_string().parse().unwrap(), + premine: (u128::MAX - 1).to_string().parse().unwrap(), + symbol: '\u{10FFFF}', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + offset: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + amount: "1".parse().unwrap(), + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); +} + +#[cfg(unix)] +#[test] +fn batch_inscribe_errors_if_pending_etchings() { + use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, + }; + + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let batchfile = batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + ..default() + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }; + + let tempdir = Arc::new(TempDir::new().unwrap()); + + { + let mut spawn = + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .temp_dir(tempdir.clone()) + .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap()) + .write("inscription.jpeg", "inscription") + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_regex_match!( + buffer, + "Waiting for rune commitment [[:xdigit:]]{64} to mature…\n" + ); + + core.mine_blocks(1); + + signal::kill( + Pid::from_raw(spawn.child.id().try_into().unwrap()), + Signal::SIGINT, + ) + .unwrap(); + + buffer.clear(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!( + buffer, + "Shutting down gracefully. Press again to shutdown immediately.\n" + ); + + spawn.child.wait().unwrap(); + } + + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .temp_dir(tempdir) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr( + "error: rune `AAAAAAAAAAAAA` has pending etching, resume with `ord wallet resume`\n", + ) + .run_and_extract_stdout(); +} diff --git a/tests/wallet/cardinals.rs b/tests/wallet/cardinals.rs index f33896cb5e..31daae0bc5 100644 --- a/tests/wallet/cardinals.rs +++ b/tests/wallet/cardinals.rs @@ -5,22 +5,22 @@ use { #[test] fn cardinals() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - inscribe(&bitcoin_rpc_server, &ord_rpc_server); + inscribe(&core, &ord); let all_outputs = CommandBuilder::new("wallet outputs") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); let cardinal_outputs = CommandBuilder::new("wallet cardinals") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(all_outputs.len() - cardinal_outputs.len(), 1); diff --git a/tests/wallet/create.rs b/tests/wallet/create.rs index f0ed37ee20..f34cc1e4e9 100644 --- a/tests/wallet/create.rs +++ b/tests/wallet/create.rs @@ -2,21 +2,21 @@ use {super::*, ord::subcommand::wallet::create::Output}; #[test] fn create() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - assert!(!rpc_server.wallets().contains("ord")); + assert!(!core.wallets().contains("ord")); CommandBuilder::new("wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); - assert!(rpc_server.wallets().contains("ord")); + assert!(core.wallets().contains("ord")); } #[test] fn seed_phrases_are_twelve_words_long() { let Output { mnemonic, .. } = CommandBuilder::new("wallet create") - .bitcoin_rpc_server(&test_bitcoincore_rpc::spawn()) + .core(&mockcore::spawn()) .run_and_deserialize_output(); assert_eq!(mnemonic.word_count(), 12); @@ -24,56 +24,54 @@ fn seed_phrases_are_twelve_words_long() { #[test] fn wallet_creates_correct_mainnet_taproot_descriptor() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); - assert_eq!(rpc_server.descriptors().len(), 2); + assert_eq!(core.descriptors().len(), 2); assert_regex_match!( - &rpc_server.descriptors()[0], + &core.descriptors()[0], r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xprv[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}" ); assert_regex_match!( - &rpc_server.descriptors()[1], + &core.descriptors()[1], r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xprv[[:alnum:]]*/1/\*\)#[[:alnum:]]{8}" ); } #[test] fn wallet_creates_correct_test_network_taproot_descriptor() { - let rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Signet) - .build(); + let core = mockcore::builder().network(Network::Signet).build(); CommandBuilder::new("--chain signet wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); - assert_eq!(rpc_server.descriptors().len(), 2); + assert_eq!(core.descriptors().len(), 2); assert_regex_match!( - &rpc_server.descriptors()[0], + &core.descriptors()[0], r"tr\(\[[[:xdigit:]]{8}/86'/1'/0'\]tprv[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}" ); assert_regex_match!( - &rpc_server.descriptors()[1], + &core.descriptors()[1], r"tr\(\[[[:xdigit:]]{8}/86'/1'/0'\]tprv[[:alnum:]]*/1/\*\)#[[:alnum:]]{8}" ); } #[test] fn detect_wrong_descriptors() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); - rpc_server.import_descriptor("wpkh([aslfjk])#a23ad2l".to_string()); + core.import_descriptor("wpkh([aslfjk])#a23ad2l".to_string()); CommandBuilder::new("wallet transactions") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .stderr_regex( r#"error: wallet "ord" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`\n"#, ) @@ -83,13 +81,13 @@ fn detect_wrong_descriptors() { #[test] fn create_with_different_name() { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - assert!(!rpc_server.wallets().contains("inscription-wallet")); + assert!(!core.wallets().contains("inscription-wallet")); CommandBuilder::new("wallet --name inscription-wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output::(); - assert!(rpc_server.wallets().contains("inscription-wallet")); + assert!(core.wallets().contains("inscription-wallet")); } diff --git a/tests/wallet/dump.rs b/tests/wallet/dump.rs index 29ce0a190d..f8fdca5b68 100644 --- a/tests/wallet/dump.rs +++ b/tests/wallet/dump.rs @@ -2,18 +2,18 @@ use super::*; #[test] fn dumped_descriptors_match_wallet_descriptors() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let output = CommandBuilder::new("wallet dump") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stderr_regex(".*") .run_and_deserialize_output::(); - assert!(bitcoin_rpc_server + assert!(core .descriptors() .iter() .zip(output.descriptors.iter()) @@ -22,26 +22,26 @@ fn dumped_descriptors_match_wallet_descriptors() { #[test] fn dumped_descriptors_restore() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let output = CommandBuilder::new("wallet dump") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stderr_regex(".*") .run_and_deserialize_output::(); - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("wallet restore --from descriptor") .stdin(serde_json::to_string(&output).unwrap().as_bytes().to_vec()) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_extract_stdout(); - assert!(bitcoin_rpc_server + assert!(core .descriptors() .iter() .zip(output.descriptors.iter()) @@ -50,26 +50,26 @@ fn dumped_descriptors_restore() { #[test] fn dump_and_restore_descriptors_with_minify() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let output = CommandBuilder::new("--minify wallet dump") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stderr_regex(".*") .run_and_deserialize_output::(); - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("wallet restore --from descriptor") .stdin(serde_json::to_string(&output).unwrap().as_bytes().to_vec()) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_extract_stdout(); - assert!(bitcoin_rpc_server + assert!(core .descriptors() .iter() .zip(output.descriptors.iter()) diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 877de7edb2..35179ea919 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1,25 +1,25 @@ use { super::*, - ord::subcommand::wallet::{create, inscriptions, receive, send}, + ord::subcommand::wallet::{create, inscriptions, receive}, std::ops::Deref, }; #[test] fn inscribe_creates_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 0); + assert_eq!(core.descriptors().len(), 0); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + assert_eq!(core.descriptors().len(), 3); - let request = ord_rpc_server.request(format!("/content/{inscription}")); + let request = ord.request(format!("/content/{inscription}")); assert_eq!(request.status(), 200); assert_eq!( @@ -31,42 +31,42 @@ fn inscribe_creates_inscriptions() { #[test] fn inscribe_works_with_huge_expensive_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = core.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new(format!( "wallet inscribe --file foo.txt --satpoint {txid}:0:0 --fee-rate 10" )) .write("foo.txt", [0; 350_000]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); } #[test] fn metaprotocol_appears_on_inscription_page() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = core.mine_blocks(1)[0].txdata[0].txid(); let inscribe = CommandBuilder::new(format!( "wallet inscribe --file foo.txt --metaprotocol foo --satpoint {txid}:0:0 --fee-rate 10" )) .write("foo.txt", [0; 350_000]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{}", inscribe.inscriptions[0].id), r".*
        metaprotocol
        \s*
        foo
        .*", ); @@ -74,51 +74,51 @@ fn metaprotocol_appears_on_inscription_page() { #[test] fn inscribe_fails_if_bitcoin_core_is_too_old() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder().version(230000).build(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::builder().version(230000).build(); + let ord = TestServer::spawn(&core); CommandBuilder::new("wallet inscribe --file hello.txt --fee-rate 1") .write("hello.txt", "HELLOWORLD") .expected_exit_code(1) .expected_stderr("error: Bitcoin Core 24.0.0 or newer required, current version is 23.0.0\n") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_extract_stdout(); } #[test] fn inscribe_no_backup() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 2); + assert_eq!(core.descriptors().len(), 2); CommandBuilder::new("wallet inscribe --file hello.txt --no-backup --fee-rate 1") .write("hello.txt", "HELLOWORLD") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 2); + assert_eq!(core.descriptors().len(), 2); } #[test] fn inscribe_unknown_file_extension() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new("wallet inscribe --file pepe.xyz --fee-rate 1") .write("pepe.xyz", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .stderr_regex(r"error: unsupported file extension `\.xyz`, supported extensions: apng .*\n") .run_and_extract_stdout(); @@ -126,18 +126,16 @@ fn inscribe_unknown_file_extension() { #[test] fn inscribe_exceeds_chain_limit() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Signet) - .build(); + let core = mockcore::builder().network(Network::Signet).build(); - let ord_rpc_server = TestServer::spawn_with_args(&bitcoin_rpc_server, &["--signet"]); + let ord = TestServer::spawn_with_args(&core, &["--signet"]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new("--chain signet wallet inscribe --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 1025]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr( "error: content size of 1025 bytes exceeds 1024 byte limit for signet inscriptions\n", @@ -147,58 +145,54 @@ fn inscribe_exceeds_chain_limit() { #[test] fn regtest_has_no_content_size_limit() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new("--chain regtest wallet inscribe --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 1025]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stdout_regex(".*") .run_and_extract_stdout(); } #[test] fn mainnet_has_no_content_size_limit() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Bitcoin) - .build(); + let core = mockcore::builder().network(Network::Bitcoin).build(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new("wallet inscribe --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 1025]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stdout_regex(".*") .run_and_extract_stdout(); } #[test] fn inscribe_does_not_use_inscribed_sats_as_cardinal_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 100); + core.mine_blocks_with_subsidy(1, 100); CommandBuilder::new( "wallet inscribe --file degenerate.png --fee-rate 1" ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .write("degenerate.png", [1; 100]) .expected_exit_code(1) .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") @@ -207,23 +201,23 @@ fn inscribe_does_not_use_inscribed_sats_as_cardinal_utxos() { #[test] fn refuse_to_reinscribe_sats() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (_, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_, reveal) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 100); + core.mine_blocks_with_subsidy(1, 100); CommandBuilder::new(format!( "wallet inscribe --satpoint {reveal}:0:0 --file hello.txt --fee-rate 1" )) .write("hello.txt", "HELLOWORLD") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr(format!("error: sat at {reveal}:0:0 already inscribed\n")) .run_and_extract_stdout(); @@ -231,12 +225,12 @@ fn refuse_to_reinscribe_sats() { #[test] fn refuse_to_inscribe_already_inscribed_utxo() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, reveal) = inscribe(&core, &ord); let output = OutPoint { txid: reveal, @@ -247,8 +241,8 @@ fn refuse_to_inscribe_already_inscribed_utxo() { "wallet inscribe --satpoint {output}:55555 --file hello.txt --fee-rate 1" )) .write("hello.txt", "HELLOWORLD") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr(format!( "error: utxo {output} with sat {output}:0 already inscribed with the following inscriptions:\n{inscription}\n", @@ -258,55 +252,58 @@ fn refuse_to_inscribe_already_inscribed_utxo() { #[test] fn inscribe_with_optional_satpoint_arg() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = core.mine_blocks(1)[0].txdata[0].txid(); - let Inscribe { inscriptions, .. } = CommandBuilder::new(format!( + let Batch { inscriptions, .. } = CommandBuilder::new(format!( "wallet inscribe --file foo.txt --satpoint {txid}:0:10000 --fee-rate 1" )) .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); let inscription = inscriptions[0].id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( "/sat/5000010000", format!(".*.*"), ); - ord_rpc_server.assert_response_regex(format!("/content/{inscription}",), "FOO"); + ord.assert_response_regex(format!("/content/{inscription}",), "FOO"); + + ord.assert_response_regex( + format!("/inscription/{}", Sat(5000010000).name()), + ".*Inscription 0.*", + ); } #[test] fn inscribe_with_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("--index-sats wallet inscribe --file degenerate.png --fee-rate 2.0") .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let tx1 = &bitcoin_rpc_server.mempool()[0]; + let tx1 = &core.mempool()[0]; let mut fee = 0; for input in &tx1.input { - fee += bitcoin_rpc_server + fee += core .get_utxo_amount(&input.previous_output) .unwrap() .to_sat(); @@ -319,7 +316,7 @@ fn inscribe_with_fee_rate() { pretty_assert_eq!(fee_rate, 2.0); - let tx2 = &bitcoin_rpc_server.mempool()[1]; + let tx2 = &core.mempool()[1]; let mut fee = 0; for input in &tx2.input { fee += &tx1.output[input.previous_output.vout as usize].value; @@ -342,26 +339,25 @@ fn inscribe_with_fee_rate() { #[test] fn inscribe_with_commit_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new( "--index-sats wallet inscribe --file degenerate.png --commit-fee-rate 2.0 --fee-rate 1", ) .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let tx1 = &bitcoin_rpc_server.mempool()[0]; + let tx1 = &core.mempool()[0]; let mut fee = 0; for input in &tx1.input { - fee += bitcoin_rpc_server + fee += core .get_utxo_amount(&input.previous_output) .unwrap() .to_sat(); @@ -374,7 +370,7 @@ fn inscribe_with_commit_fee_rate() { pretty_assert_eq!(fee_rate, 2.0); - let tx2 = &bitcoin_rpc_server.mempool()[1]; + let tx2 = &core.mempool()[1]; let mut fee = 0; for input in &tx2.input { fee += &tx1.output[input.previous_output.vout as usize].value; @@ -390,79 +386,79 @@ fn inscribe_with_commit_fee_rate() { #[test] fn inscribe_with_wallet_named_foo() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); CommandBuilder::new("wallet --name foo create") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new("wallet --name foo inscribe --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); } #[test] fn inscribe_with_dry_run_flag() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let inscribe = CommandBuilder::new("wallet inscribe --dry-run --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); assert!(inscribe.commit_psbt.is_some()); assert!(inscribe.reveal_psbt.is_some()); - assert!(bitcoin_rpc_server.mempool().is_empty()); + assert!(core.mempool().is_empty()); let inscribe = CommandBuilder::new("wallet inscribe --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); assert!(inscribe.commit_psbt.is_none()); assert!(inscribe.reveal_psbt.is_none()); - assert_eq!(bitcoin_rpc_server.mempool().len(), 2); + assert_eq!(core.mempool().len(), 2); } #[test] fn inscribe_with_dry_run_flag_fees_increase() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let total_fee_dry_run = CommandBuilder::new("wallet inscribe --dry-run --file degenerate.png --fee-rate 1") .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::() + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() .total_fees; let total_fee_normal = CommandBuilder::new("wallet inscribe --dry-run --file degenerate.png --fee-rate 1.1") .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::() + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() .total_fees; assert!(total_fee_dry_run < total_fee_normal); @@ -470,30 +466,32 @@ fn inscribe_with_dry_run_flag_fees_increase() { #[test] fn inscribe_to_specific_destination() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let destination = CommandBuilder::new("wallet receive") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + let addresses = CommandBuilder::new("wallet receive") + .core(&core) + .ord(&ord) .run_and_deserialize_output::() - .address; + .addresses; + + let destination = addresses.first().unwrap(); let txid = CommandBuilder::new(format!( "wallet inscribe --destination {} --file degenerate.png --fee-rate 1", destination.clone().assume_checked() )) .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::() + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() .reveal; - let reveal_tx = &bitcoin_rpc_server.mempool()[1]; // item 0 is the commit, item 1 is the reveal. + let reveal_tx = &core.mempool()[1]; // item 0 is the commit, item 1 is the reveal. assert_eq!(reveal_tx.txid(), txid); assert_eq!( reveal_tx.output.first().unwrap().script_pubkey, @@ -503,19 +501,19 @@ fn inscribe_to_specific_destination() { #[test] fn inscribe_to_address_on_different_network() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new( "wallet inscribe --destination tb1qsgx55dp6gn53tsmyjjv4c2ye403hgxynxs0dnm --file degenerate.png --fee-rate 1" ) .write("degenerate.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .stderr_regex("error: address tb1qsgx55dp6gn53tsmyjjv4c2ye403hgxynxs0dnm belongs to network testnet which is different from required bitcoin\n") .run_and_extract_stdout(); @@ -523,40 +521,41 @@ fn inscribe_to_address_on_different_network() { #[test] fn inscribe_with_no_limit() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let four_megger = std::iter::repeat(0).take(4_000_000).collect::>(); - CommandBuilder::new("wallet inscribe --no-limit degenerate.png --fee-rate 1") - .write("degenerate.png", four_megger) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server); + let one_megger = std::iter::repeat(0).take(1_000_000).collect::>(); + CommandBuilder::new("wallet inscribe --no-limit --file degenerate.png --fee-rate 1") + .write("degenerate.png", one_megger) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); } #[test] fn inscribe_works_with_postage() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(1); + create_wallet(&core, &ord); + core.mine_blocks(1); CommandBuilder::new("wallet inscribe --file foo.txt --postage 5btc --fee-rate 10".to_string()) .write("foo.txt", [0; 350]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let inscriptions = CommandBuilder::new("wallet inscriptions".to_string()) .write("foo.txt", [0; 350]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); pretty_assert_eq!(inscriptions[0].postage, 5 * COIN_VALUE); @@ -564,12 +563,12 @@ fn inscribe_works_with_postage() { #[test] fn inscribe_with_non_existent_parent_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let parent_id = "0000000000000000000000000000000000000000000000000000000000000000i0"; @@ -577,8 +576,8 @@ fn inscribe_with_non_existent_parent_inscription() { "wallet inscribe --fee-rate 1.0 --parent {parent_id} --file child.png" )) .write("child.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_stderr(format!("error: parent {parent_id} does not exist\n")) .expected_exit_code(1) .run_and_extract_stdout(); @@ -586,24 +585,24 @@ fn inscribe_with_non_existent_parent_inscription() { #[test] fn inscribe_with_parent_inscription_and_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + assert_eq!(core.descriptors().len(), 3); let parent_id = parent_output.inscriptions[0].id; - let commit_tx = &bitcoin_rpc_server.mempool()[0]; - let reveal_tx = &bitcoin_rpc_server.mempool()[1]; + let commit_tx = &core.mempool()[0]; + let reveal_tx = &core.mempool()[1]; assert_eq!( ord::FeeRate::try_from(5.0) @@ -613,21 +612,21 @@ fn inscribe_with_parent_inscription_and_fee_rate() { parent_output.total_fees ); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let child_output = CommandBuilder::new(format!( "wallet inscribe --fee-rate 7.3 --parent {parent_id} --file child.png" )) .write("child.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 4); + assert_eq!(core.descriptors().len(), 4); assert_eq!(parent_id, child_output.parent.unwrap()); - let commit_tx = &bitcoin_rpc_server.mempool()[0]; - let reveal_tx = &bitcoin_rpc_server.mempool()[1]; + let commit_tx = &core.mempool()[0]; + let reveal_tx = &core.mempool()[1]; assert_eq!( ord::FeeRate::try_from(7.3) @@ -637,9 +636,9 @@ fn inscribe_with_parent_inscription_and_fee_rate() { child_output.total_fees ); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{}", child_output.parent.unwrap()), format!( ".*
        children
        .*
        .*", @@ -647,10 +646,10 @@ fn inscribe_with_parent_inscription_and_fee_rate() { ), ); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{}", child_output.inscriptions[0].id), format!( - ".*
        parent
        .*
        .*", + ".*
        parents
        .*
        .*", child_output.parent.unwrap() ), ); @@ -658,27 +657,26 @@ fn inscribe_with_parent_inscription_and_fee_rate() { #[test] fn reinscribe_with_flag() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 0); + assert_eq!(core.descriptors().len(), 0); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let inscribe = CommandBuilder::new("wallet inscribe --file tulip.png --fee-rate 5.0 ") .write("tulip.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + assert_eq!(core.descriptors().len(), 3); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[2].txid(); + let txid = core.mine_blocks(1)[0].txdata[2].txid(); - let request = ord_rpc_server.request(format!("/content/{}", inscribe.inscriptions[0].id)); + let request = ord.request(format!("/content/{}", inscribe.inscriptions[0].id)); assert_eq!(request.status(), 200); @@ -686,16 +684,16 @@ fn reinscribe_with_flag() { "wallet inscribe --file orchid.png --fee-rate 1.1 --reinscribe --satpoint {txid}:0:0" )) .write("orchid.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let request = ord_rpc_server.request(format!("/content/{}", reinscribe.inscriptions[0].id)); + let request = ord.request(format!("/content/{}", reinscribe.inscriptions[0].id)); assert_eq!(request.status(), 200); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/sat/{}", 50 * COIN_VALUE), format!( ".*
        inscriptions
        .*
        .*.*", @@ -704,8 +702,8 @@ fn reinscribe_with_flag() { ); let inscriptions = CommandBuilder::new("wallet inscriptions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); assert_eq!(inscriptions[0].inscription, inscribe.inscriptions[0].id); @@ -714,28 +712,28 @@ fn reinscribe_with_flag() { #[test] fn with_reinscribe_flag_but_not_actually_a_reinscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new("wallet inscribe --file tulip.png --fee-rate 5.0 ") .write("tulip.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let coinbase = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let coinbase = core.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new(format!( "wallet inscribe --file orchid.png --fee-rate 1.1 --reinscribe --satpoint {coinbase}:0:0" )) .write("orchid.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .stderr_regex("error: reinscribe flag set but this would not be a reinscription.*") .run_and_extract_stdout(); @@ -743,31 +741,31 @@ fn with_reinscribe_flag_but_not_actually_a_reinscription() { #[test] fn try_reinscribe_without_flag() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let reveal_txid = CommandBuilder::new("wallet inscribe --file tulip.png --fee-rate 5.0 ") .write("tulip.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::() + .core(&core) + .ord(&ord) + .run_and_deserialize_output::() .reveal; - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + assert_eq!(core.descriptors().len(), 3); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new(format!( "wallet inscribe --file orchid.png --fee-rate 1.1 --satpoint {reveal_txid}:0:0" )) .write("orchid.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .stderr_regex(format!( "error: sat at {reveal_txid}:0:0 already inscribed.*" @@ -777,26 +775,26 @@ fn try_reinscribe_without_flag() { #[test] fn no_metadata_appears_on_inscription_page_if_no_metadata_is_passed() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let Inscribe { inscriptions, .. } = + let Batch { inscriptions, .. } = CommandBuilder::new("wallet inscribe --fee-rate 1 --file content.png") .write("content.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); let inscription = inscriptions[0].id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - assert!(!ord_rpc_server + assert!(!ord .request(format!("/inscription/{inscription}"),) .text() .unwrap() @@ -805,28 +803,28 @@ fn no_metadata_appears_on_inscription_page_if_no_metadata_is_passed() { #[test] fn json_metadata_appears_on_inscription_page() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let Inscribe { inscriptions, .. } = CommandBuilder::new( + let Batch { inscriptions, .. } = CommandBuilder::new( "wallet inscribe --fee-rate 1 --json-metadata metadata.json --file content.png", ) .write("content.png", [1; 520]) .write("metadata.json", r#"{"foo": "bar", "baz": 1}"#) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); let inscription = inscriptions[0].id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{inscription}"), ".*
        metadata
        .*
        foo
        bar
        baz
        1
        .*", ); @@ -834,14 +832,14 @@ fn json_metadata_appears_on_inscription_page() { #[test] fn cbor_metadata_appears_on_inscription_page() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let Inscribe { inscriptions, .. } = CommandBuilder::new( + let Batch { inscriptions, .. } = CommandBuilder::new( "wallet inscribe --fee-rate 1 --cbor-metadata metadata.cbor --file content.png", ) .write("content.png", [1; 520]) @@ -851,15 +849,15 @@ fn cbor_metadata_appears_on_inscription_page() { 0xA2, 0x63, b'f', b'o', b'o', 0x63, b'b', b'a', b'r', 0x63, b'b', b'a', b'z', 0x01, ], ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); let inscription = inscriptions[0].id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{inscription}"), ".*
        metadata
        .*
        foo
        bar
        baz
        1
        .*", ); @@ -867,18 +865,18 @@ fn cbor_metadata_appears_on_inscription_page() { #[test] fn error_message_when_parsing_json_metadata_is_reasonable() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new( "wallet inscribe --fee-rate 1 --json-metadata metadata.json --file content.png", ) .write("content.png", [1; 520]) .write("metadata.json", "{") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stderr_regex(".*failed to parse JSON metadata.*") .expected_exit_code(1) .run_and_extract_stdout(); @@ -886,511 +884,39 @@ fn error_message_when_parsing_json_metadata_is_reasonable() { #[test] fn error_message_when_parsing_cbor_metadata_is_reasonable() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new( "wallet inscribe --fee-rate 1 --cbor-metadata metadata.cbor --file content.png", ) .write("content.png", [1; 520]) .write("metadata.cbor", [0x61]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stderr_regex(".*failed to parse CBOR metadata.*") .expected_exit_code(1) .run_and_extract_stdout(); } -#[test] -fn batch_inscribe_fails_if_batchfile_has_no_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("batch.yaml", "mode: shared-output\ninscriptions: []\n") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stderr_regex(".*batchfile must contain at least one inscription.*") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_can_create_one_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n metadata: 123\n metaprotocol: foo", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); - - assert_eq!(request.status(), 200); - assert_eq!( - request.headers().get("content-type").unwrap(), - "text/plain;charset=utf-8" - ); - assert_eq!(request.text().unwrap(), "Hello World"); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - r".*
        metadata
        \s*
        \n 123\n
        .*
        metaprotocol
        \s*
        foo
        .*", - ); -} - -#[test] -fn batch_inscribe_with_multiple_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --batch batch.yaml --fee-rate 55") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); - assert_eq!(request.status(), 200); - assert_eq!( - request.headers().get("content-type").unwrap(), - "text/plain;charset=utf-8" - ); - assert_eq!(request.text().unwrap(), "Hello World"); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[1].id)); - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "image/png"); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); -} - -#[test] -fn batch_inscribe_with_multiple_inscriptions_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - 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!("parent: {parent_id}\nmode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - r".*
        parent
        \s*
        .*
        .*", - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - r".*
        parent
        \s*
        .*
        .*", - ); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[2].id)); - assert_eq!(request.status(), 200); - assert_eq!(request.headers().get("content-type").unwrap(), "audio/wav"); -} - -#[test] -fn batch_inscribe_respects_dry_run_flag() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml --dry-run") - .write("inscription.txt", "Hello World") - .write( - "batch.yaml", - "mode: shared-output\ninscriptions:\n- file: inscription.txt\n", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert!(bitcoin_rpc_server.mempool().is_empty()); - - let request = ord_rpc_server.request(format!("/content/{}", output.inscriptions[0].id)); - - assert_eq!(request.status(), 404); -} - -#[test] -fn batch_in_same_output_but_different_satpoints() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - 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: shared-output\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - let outpoint = output.inscriptions[0].location.outpoint; - for (i, inscription) in output.inscriptions.iter().enumerate() { - assert_eq!( - inscription.location, - SatPoint { - outpoint, - offset: u64::try_from(i).unwrap() * 10_000, - } - ); - } - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint = output.inscriptions[0].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        location
        .*
        {}:10000
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        location
        .*
        {}:20000
        .*", - outpoint - ), - ); - - ord_rpc_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_in_same_output_with_non_default_postage() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - 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: shared-output\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - let outpoint = output.inscriptions[0].location.outpoint; - - for (i, inscription) in output.inscriptions.iter().enumerate() { - assert_eq!( - inscription.location, - SatPoint { - outpoint, - offset: u64::try_from(i).unwrap() * 777, - } - ); - } - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint = output.inscriptions[0].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        location
        .*
        {}:777
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        location
        .*
        {}:1554
        .*", - outpoint - ), - ); - - ord_rpc_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_in_separate_outputs_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - 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!("parent: {parent_id}\nmode: separate-outputs\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - let mut outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - outpoints.sort(); - outpoints.dedup(); - assert_eq!(outpoints.len(), output.inscriptions.len()); - - bitcoin_rpc_server.mine_blocks(1); - - let output_1 = output.inscriptions[0].location.outpoint; - let output_2 = output.inscriptions[1].location.outpoint; - let output_3 = output.inscriptions[2].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        10000
        .*.*
        location
        .*
        {}:0
        .*", - output_1 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        10000
        .*.*
        location
        .*
        {}:0
        .*", - output_2 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        10000
        .*.*
        location
        .*
        {}:0
        .*", - output_3 - ), - ); -} - -#[test] -fn batch_in_separate_outputs_with_parent_and_non_default_postage() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); - - 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!("parent: {parent_id}\nmode: separate-outputs\npostage: 777\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - - let mut outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - outpoints.sort(); - outpoints.dedup(); - assert_eq!(outpoints.len(), output.inscriptions.len()); - - bitcoin_rpc_server.mine_blocks(1); - - let output_1 = output.inscriptions[0].location.outpoint; - let output_2 = output.inscriptions[1].location.outpoint; - let output_3 = output.inscriptions[2].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        777
        .*.*
        location
        .*
        {}:0
        .*", - output_1 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        777
        .*.*
        location
        .*
        {}:0
        .*", - output_2 - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        777
        .*.*
        location
        .*
        {}:0
        .*", - output_3 - ), - ); -} - #[test] fn inscribe_does_not_pick_locked_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let coinbase_tx = &bitcoin_rpc_server.mine_blocks(1)[0].txdata[0]; + let coinbase_tx = &core.mine_blocks(1)[0].txdata[0]; let outpoint = OutPoint::new(coinbase_tx.txid(), 0); - bitcoin_rpc_server.lock(outpoint); + core.lock(outpoint); CommandBuilder::new("wallet inscribe --file hello.txt --fee-rate 1") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .write("hello.txt", "HELLOWORLD") .expected_exit_code(1) .stderr_regex("error: wallet contains no cardinal utxos\n") @@ -1399,26 +925,26 @@ fn inscribe_does_not_pick_locked_utxos() { #[test] fn inscribe_can_compress() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let Inscribe { inscriptions, .. } = + let Batch { inscriptions, .. } = CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) .write("foo.txt", [0; 350_000]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); let inscription = inscriptions[0].id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.sync_server(); + ord.sync_server(); let client = reqwest::blocking::Client::builder() .brotli(false) @@ -1427,7 +953,7 @@ fn inscribe_can_compress() { let response = client .get( - ord_rpc_server + ord .url() .join(format!("/content/{inscription}",).as_ref()) .unwrap(), @@ -1448,7 +974,7 @@ fn inscribe_can_compress() { let response = client .get( - ord_rpc_server + ord .url() .join(format!("/content/{inscription}",).as_ref()) .unwrap(), @@ -1462,26 +988,26 @@ fn inscribe_can_compress() { #[test] fn inscriptions_are_not_compressed_if_no_space_is_saved_by_compression() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let Inscribe { inscriptions, .. } = + let Batch { inscriptions, .. } = CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) .write("foo.txt", "foo") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output(); let inscription = inscriptions[0].id; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.sync_server(); + ord.sync_server(); let client = reqwest::blocking::Client::builder() .brotli(false) @@ -1490,7 +1016,7 @@ fn inscriptions_are_not_compressed_if_no_space_is_saved_by_compression() { let response = client .get( - ord_rpc_server + ord .url() .join(format!("/content/{inscription}",).as_ref()) .unwrap(), @@ -1503,623 +1029,107 @@ fn inscriptions_are_not_compressed_if_no_space_is_saved_by_compression() { } #[test] -fn batch_inscribe_fails_if_invalid_network_destination_address() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); +fn inscribe_with_sat_arg() { + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(2); - CommandBuilder::new("--regtest wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("batch.yaml", "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stderr_regex("error: address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 belongs to network bitcoin which is different from required regtest\n") - .expected_exit_code(1) - .run_and_extract_stdout(); + let Batch { inscriptions, .. } = CommandBuilder::new( + "--index-sats wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1", + ) + .write("foo.txt", "FOO") + .core(&core) + .ord(&ord) + .run_and_deserialize_output(); + + let inscription = inscriptions[0].id; + + core.mine_blocks(1); + + ord.assert_response_regex( + "/sat/5010000000", + format!(".*.*"), + ); + + ord.assert_response_regex(format!("/content/{inscription}",), "FOO"); } #[test] -fn batch_inscribe_fails_with_shared_output_or_same_sat_and_destination_set() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); +fn inscribe_with_sat_arg_fails_if_no_index_or_not_found() { + let core = mockcore::spawn(); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - bitcoin_rpc_server.mine_blocks(1); + create_wallet(&core, &ord); - CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", "") - .write("batch.yaml", "mode: shared-output\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + CommandBuilder::new("wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1") + .write("foo.txt", "FOO") + .core(&core) + .ord(&ord) .expected_exit_code(1) - .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") + .expected_stderr("error: ord index must be built with `--index-sats` to use `--sat`\n") .run_and_extract_stdout(); - CommandBuilder::new("wallet inscribe --fee-rate 2.1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", "") - .write("batch.yaml", "mode: same-sat\nsat: 5000000000\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + CommandBuilder::new("--index-sats wallet inscribe --sat 5000000000 --file foo.txt --fee-rate 1") + .write("foo.txt", "FOO") + .core(&core) + .ord(&TestServer::spawn_with_server_args( + &core, + &["--index-sats"], + &[], + )) .expected_exit_code(1) - .stderr_regex("error: individual inscription destinations cannot be set in `shared-output` or `same-sat` mode\n") + .expected_stderr("error: could not find sat `5000000000` in wallet outputs\n") .run_and_extract_stdout(); } #[test] -fn batch_inscribe_works_with_some_destinations_set_and_others_not() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --batch batch.yaml --fee-rate 55") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png\n- file: meow.wav\n destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); +fn server_can_decompress_brotli() { + let core = mockcore::spawn(); - assert_eq!(bitcoin_rpc_server.descriptors().len(), 3); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - ".* -
        address
        -
        bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
        .*", - ); + create_wallet(&core, &ord); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - ".* -
        address
        -
        {}
        .*", - bitcoin_rpc_server.change_addresses()[0] - ), - ); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - ".* -
        address
        -
        bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k
        .*", - ); -} + let Batch { inscriptions, .. } = + CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) + .write("foo.txt", [0; 350_000]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output(); -#[test] -fn batch_same_sat() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let inscription = inscriptions[0].id; - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + core.mine_blocks(1); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + ord.sync_server(); - bitcoin_rpc_server.mine_blocks(1); + let client = reqwest::blocking::Client::builder() + .brotli(false) + .build() + .unwrap(); - 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" + let response = client + .get( + ord + .url() + .join(format!("/content/{inscription}",).as_ref()) + .unwrap(), ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .send() + .unwrap(); - assert_eq!( - output.inscriptions[0].location, - output.inscriptions[1].location - ); - assert_eq!( - output.inscriptions[1].location, - output.inscriptions[2].location - ); + assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); - bitcoin_rpc_server.mine_blocks(1); + let test_server = TestServer::spawn_with_server_args(&core, &[], &["--decompress"]); - let outpoint = output.inscriptions[0].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_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 bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_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") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 - ); - - bitcoin_rpc_server.mine_blocks(1); - - let txid = output.inscriptions[0].location.outpoint.txid; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", parent_id), - format!( - r".*
        location
        .*
        {}:0:0
        .*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        location
        .*
        {}:1:0
        .*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        location
        .*
        {}:1:0
        .*", - txid - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        location
        .*
        {}:1:0
        .*", - txid - ), - ); - - ord_rpc_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_satpoint_and_reinscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let inscription_id = output.inscriptions[0].id; - let satpoint = output.inscriptions[0].location; - - 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\nsatpoint: {}\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .stderr_regex(".*error: sat at .*:0:0 already inscribed.*") - .run_and_extract_stdout(); - - 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\nsatpoint: {}\nreinscribe: true\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", satpoint) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_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 - ); - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint = output.inscriptions[0].location.outpoint; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[0].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[1].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", output.inscriptions[2].id), - format!( - r".*
        location
        .*
        {}:0
        .*", - outpoint - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/output/{}", output.inscriptions[0].location.outpoint), - format!(r".*.*.*.*.*.*.*.*.*", inscription_id, output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id), - ); -} - -#[test] -fn inscribe_with_sat_arg() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_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") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output(); - - let inscription = inscriptions[0].id; - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - "/sat/5010000000", - format!(".*.*"), - ); - - ord_rpc_server.assert_response_regex(format!("/content/{inscription}",), "FOO"); -} - -#[test] -fn inscribe_with_sat_arg_fails_if_no_index_or_not_found() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - CommandBuilder::new("wallet inscribe --file foo.txt --sat 5010000000 --fee-rate 1") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: ord 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") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&TestServer::spawn_with_server_args( - &bitcoin_rpc_server, - &["--index-sats"], - &[], - )) - .expected_exit_code(1) - .expected_stderr("error: could not find sat `5000000000` in wallet outputs\n") - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_sat_argument_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = - CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - assert_eq!(bitcoin_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") - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.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 bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_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" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: neither `sat` nor `satpoint` can be set in `same-sat` mode\n") - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_satpoint() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); - - 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\nsatpoint: {txid}:0:55555\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n", ) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - "/sat/5000055555", - format!( - ".*.*.*.*", - output.inscriptions[0].id, output.inscriptions[1].id, output.inscriptions[2].id - ), - ); -} - -#[test] -fn batch_inscribe_with_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(2); - - let set_fee_rate = 1.0; - - let output = CommandBuilder::new(format!("--index-sats wallet inscribe --fee-rate {set_fee_rate} --batch batch.yaml")) - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 555]) - .write("meow.wav", [0; 2048]) - .write( - "batch.yaml", - "mode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - let commit_tx = &bitcoin_rpc_server.mempool()[0]; - let mut fee = 0; - for input in &commit_tx.input { - fee += bitcoin_rpc_server - .get_utxo_amount(&input.previous_output) - .unwrap() - .to_sat(); - } - for output in &commit_tx.output { - fee -= output.value; - } - let fee_rate = fee as f64 / commit_tx.vsize() as f64; - pretty_assert_eq!(fee_rate, set_fee_rate); - - let reveal_tx = &bitcoin_rpc_server.mempool()[1]; - let mut fee = 0; - for input in &reveal_tx.input { - fee += &commit_tx.output[input.previous_output.vout as usize].value; - } - for output in &reveal_tx.output { - fee -= output.value; - } - let fee_rate = fee as f64 / reveal_tx.vsize() as f64; - pretty_assert_eq!(fee_rate, set_fee_rate); - - assert_eq!( - ord::FeeRate::try_from(set_fee_rate) - .unwrap() - .fee(commit_tx.vsize() + reveal_tx.vsize()) - .to_sat(), - output.total_fees - ); -} - -#[test] -fn server_can_decompress_brotli() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let Inscribe { inscriptions, .. } = - CommandBuilder::new("wallet inscribe --compress --file foo.txt --fee-rate 1".to_string()) - .write("foo.txt", [0; 350_000]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output(); - - let inscription = inscriptions[0].id; - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.sync_server(); - - let client = reqwest::blocking::Client::builder() - .brotli(false) - .build() - .unwrap(); - - let response = client - .get( - ord_rpc_server - .url() - .join(format!("/content/{inscription}",).as_ref()) - .unwrap(), - ) - .send() - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_ACCEPTABLE); - - let test_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &["--decompress"]); - - test_server.sync_server(); + test_server.sync_server(); let client = reqwest::blocking::Client::builder() .brotli(false) @@ -2142,458 +1152,86 @@ fn server_can_decompress_brotli() { #[test] fn file_inscribe_with_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (delegate, _) = inscribe(&core, &ord); let inscribe = CommandBuilder::new(format!( "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file inscription.txt" )) .write("inscription.txt", "INSCRIPTION") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{}", inscribe.inscriptions[0].id), format!(r#".*
        delegate
        \s*
        {delegate}
        .*"#,), ); - ord_rpc_server.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); + ord.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); } #[test] -fn file_inscribe_with_non_existent_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); +fn inscription_with_delegate_returns_effective_content_type() { + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + create_wallet(&core, &ord); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + core.mine_blocks(1); + let (delegate, _) = inscribe(&core, &ord); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; - - CommandBuilder::new(format!( - "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file child.png" + let inscribe = CommandBuilder::new(format!( + "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file meow.wav" )) - .write("child.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr(format!("error: delegate {delegate} does not exist\n")) - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + .write("meow.wav", [0; 2048]) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (delegate, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let inscription_id = inscribe.inscriptions[0].id; + let json_response = ord.json_request(format!("/inscription/{}", inscription_id)); - let inscribe = CommandBuilder::new("wallet inscribe --fee-rate 1.0 --batch batch.yaml") - .write("inscription.txt", "INSCRIPTION") - .write( - "batch.yaml", - format!( - "mode: shared-output -inscriptions: -- delegate: {delegate} - file: inscription.txt -" - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); + let inscription_json: api::Inscription = + serde_json::from_str(&json_response.text().unwrap()).unwrap(); + assert_regex_match!(inscription_json.address.unwrap(), r"bc1p.*"); - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscribe.inscriptions[0].id), - format!(r#".*
        delegate
        \s*
        {delegate}
        .*"#,), + assert_eq!(inscription_json.content_type, Some("audio/wav".to_string())); + assert_eq!( + inscription_json.effective_content_type, + Some("text/plain;charset=utf-8".to_string()) ); - - ord_rpc_server.assert_response(format!("/content/{}", inscribe.inscriptions[0].id), "FOO"); } #[test] -fn batch_inscribe_with_non_existent_delegate_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); +fn file_inscribe_with_non_existent_delegate_inscription() { + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let delegate = "0000000000000000000000000000000000000000000000000000000000000000i0"; - CommandBuilder::new("wallet inscribe --fee-rate 1.0 --batch batch.yaml") - .write("hello.txt", "Hello, world!") - .write( - "batch.yaml", - format!( - "mode: shared-output -inscriptions: -- delegate: {delegate} - file: hello.txt -" - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr(format!("error: delegate {delegate} does not exist\n")) - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn batch_inscribe_with_satpoints_with_parent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let parent_output = - CommandBuilder::new("--index-sats wallet inscribe --fee-rate 5.0 --file parent.png") - .write("parent.png", [1; 520]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let txids = bitcoin_rpc_server - .mine_blocks(3) - .iter() - .map(|block| block.txdata[0].txid()) - .collect::>(); - - let satpoint_1 = SatPoint { - outpoint: OutPoint { - txid: txids[0], - vout: 0, - }, - offset: 0, - }; - - let satpoint_2 = SatPoint { - outpoint: OutPoint { - txid: txids[1], - vout: 0, - }, - offset: 0, - }; - - let satpoint_3 = SatPoint { - outpoint: OutPoint { - txid: txids[2], - vout: 0, - }, - offset: 0, - }; - - let sat_1 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_1.outpoint)) - .text() - .unwrap(), - ) - .unwrap() - .sat_ranges - .unwrap()[0] - .0; - - let sat_2 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_2.outpoint)) - .text() - .unwrap(), - ) - .unwrap() - .sat_ranges - .unwrap()[0] - .0; - - let sat_3 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_3.outpoint)) - .text() - .unwrap(), - ) - .unwrap() - .sat_ranges - .unwrap()[0] - .0; - - 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!( - r#" -mode: satpoints -parent: {parent_id} -inscriptions: -- file: inscription.txt - satpoint: {} -- file: tulip.png - satpoint: {} -- file: meow.wav - satpoint: {} -"#, - satpoint_1, satpoint_2, satpoint_3 - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", parent_id), - format!( - r".*
        location
        .*
        {}:0:0
        .*", - output.reveal - ), - ); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - - let outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - - assert_eq!(outpoints.len(), output.inscriptions.len()); - - let inscription_1 = output.inscriptions[0]; - let inscription_2 = output.inscriptions[1]; - let inscription_3 = output.inscriptions[2]; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_1.id), - format!(r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", - 50 * COIN_VALUE, - sat_1, - inscription_1.location, - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_2.id), - format!(r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", - 50 * COIN_VALUE, - sat_2, - inscription_2.location - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_3.id), - format!(r".*
        parent
        \s*
        .*{parent_id}.*
        .*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", - 50 * COIN_VALUE, - sat_3, - inscription_3.location - ), - ); -} - -#[test] -fn batch_inscribe_with_satpoints_with_different_sizes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(3); - - let outpoint_1 = OutPoint { - txid: CommandBuilder::new( - "--index-sats wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 25btc", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::() - .txid, - vout: 0, - }; - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint_2 = OutPoint { - txid: CommandBuilder::new( - "--index-sats wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::() - .txid, - vout: 0, - }; - - bitcoin_rpc_server.mine_blocks(1); - - let outpoint_3 = OutPoint { - txid: CommandBuilder::new( - "--index-sats wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 3btc", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::() - .txid, - vout: 0, - }; - - bitcoin_rpc_server.mine_blocks(1); - - let satpoint_1 = SatPoint { - outpoint: outpoint_1, - offset: 0, - }; - - let satpoint_2 = SatPoint { - outpoint: outpoint_2, - offset: 0, - }; - - let satpoint_3 = SatPoint { - outpoint: outpoint_3, - offset: 0, - }; - - let output_1 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_1.outpoint)) - .text() - .unwrap(), - ) - .unwrap(); - assert_eq!(output_1.value, 25 * COIN_VALUE); - - let output_2 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_2.outpoint)) - .text() - .unwrap(), - ) - .unwrap(); - assert_eq!(output_2.value, COIN_VALUE); - - let output_3 = serde_json::from_str::( - &ord_rpc_server - .json_request(format!("/output/{}", satpoint_3.outpoint)) - .text() - .unwrap(), - ) - .unwrap(); - assert_eq!(output_3.value, 3 * COIN_VALUE); - - let sat_1 = output_1.sat_ranges.unwrap()[0].0; - let sat_2 = output_2.sat_ranges.unwrap()[0].0; - let sat_3 = output_3.sat_ranges.unwrap()[0].0; - - let output = CommandBuilder::new("--index-sats wallet inscribe --fee-rate 1 --batch batch.yaml") - .write("inscription.txt", "Hello World") - .write("tulip.png", [0; 5]) - .write("meow.wav", [0; 2]) - .write( - "batch.yaml", - format!( - r#" -mode: satpoints -inscriptions: -- file: inscription.txt - satpoint: {} -- file: tulip.png - satpoint: {} -- file: meow.wav - satpoint: {} -"#, - satpoint_1, satpoint_2, satpoint_3 - ), - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - for inscription in &output.inscriptions { - assert_eq!(inscription.location.offset, 0); - } - - let outpoints = output - .inscriptions - .iter() - .map(|inscription| inscription.location.outpoint) - .collect::>(); - - assert_eq!(outpoints.len(), output.inscriptions.len()); - - let inscription_1 = output.inscriptions[0]; - let inscription_2 = output.inscriptions[1]; - let inscription_3 = output.inscriptions[2]; - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_1.id), - format!( - r".*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", - 25 * COIN_VALUE, - sat_1, - inscription_1.location - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_2.id), - format!( - r".*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", - COIN_VALUE, - sat_2, - inscription_2.location - ), - ); - - ord_rpc_server.assert_response_regex( - format!("/inscription/{}", inscription_3.id), - format!( - r".*
        value
        .*
        {}
        .*
        sat
        .*
        .*{}.*
        .*
        location
        .*
        {}
        .*", - 3 * COIN_VALUE, - sat_3, - inscription_3.location - ), - ); + CommandBuilder::new(format!( + "wallet inscribe --fee-rate 1.0 --delegate {delegate} --file child.png" + )) + .write("child.png", [1; 520]) + .core(&core) + .ord(&ord) + .expected_stderr(format!("error: delegate {delegate} does not exist\n")) + .expected_exit_code(1) + .run_and_extract_stdout(); } diff --git a/tests/wallet/inscriptions.rs b/tests/wallet/inscriptions.rs index 9c10ed1924..054ed4e4b5 100644 --- a/tests/wallet/inscriptions.rs +++ b/tests/wallet/inscriptions.rs @@ -5,19 +5,19 @@ use { #[test] fn inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, reveal) = inscribe(&core, &ord); let output = CommandBuilder::new("wallet inscriptions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output.len(), 1); @@ -28,28 +28,30 @@ fn inscriptions() { format!("https://ordinals.com/inscription/{inscription}") ); - let address = CommandBuilder::new("wallet receive") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + let addresses = CommandBuilder::new("wallet receive") + .core(&core) + .ord(&ord) .run_and_deserialize_output::() - .address; + .addresses; + + let destination = addresses.first().unwrap(); let txid = CommandBuilder::new(format!( "wallet send --fee-rate 1 {} {inscription}", - address.assume_checked() + destination.clone().assume_checked() )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(0) .stdout_regex(".*") .run_and_deserialize_output::() .txid; - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("wallet inscriptions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output.len(), 1); @@ -59,26 +61,26 @@ fn inscriptions() { #[test] fn inscriptions_includes_locked_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, reveal) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - bitcoin_rpc_server.lock(OutPoint { + core.lock(OutPoint { txid: reveal, vout: 0, }); let output = CommandBuilder::new("wallet inscriptions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output.len(), 1); @@ -88,44 +90,46 @@ fn inscriptions_includes_locked_utxos() { #[test] fn inscriptions_with_postage() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); let output = CommandBuilder::new("wallet inscriptions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].postage, 10000); - let address = CommandBuilder::new("wallet receive") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + let addresses = CommandBuilder::new("wallet receive") + .core(&core) + .ord(&ord) .run_and_deserialize_output::() - .address; + .addresses; + + let destination = addresses.first().unwrap(); CommandBuilder::new(format!( "wallet send --fee-rate 1 {} {inscription}", - address.assume_checked() + destination.clone().assume_checked() )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(0) .stdout_regex(".*") .run_and_extract_stdout(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("wallet inscriptions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].postage, 9889); diff --git a/tests/wallet/mint.rs b/tests/wallet/mint.rs new file mode 100644 index 0000000000..74b1f01297 --- /dev/null +++ b/tests/wallet/mint.rs @@ -0,0 +1,251 @@ +use {super::*, ord::subcommand::wallet::mint}; + +#[test] +fn minting_rune_and_fails_if_after_end() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + core.mine_blocks(1); + + create_wallet(&core, &ord); + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 1, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "0".parse().unwrap(), + symbol: '¢', + supply: "111.1".parse().unwrap(), + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + amount: "111.1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let output = CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let balances = CommandBuilder::new("--regtest --index-runes balances") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + pretty_assert_eq!( + output.pile, + Pile { + amount: 1111, + divisibility: 1, + symbol: Some('¢'), + } + ); + + pretty_assert_eq!( + balances, + ord::subcommand::balances::Output { + runes: vec![( + output.rune, + vec![( + OutPoint { + txid: output.mint, + vout: 1 + }, + output.pile, + )] + .into_iter() + .collect() + ),] + .into_iter() + .collect(), + } + ); + + core.mine_blocks(1); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: rune AAAAAAAAAAAAA mint ended on block 11\n") + .run_and_extract_stdout(); +} + +#[test] +fn minting_rune_fails_if_not_mintable() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + create_wallet(&core, &ord); + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 1, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: rune AAAAAAAAAAAAA not mintable\n") + .run_and_extract_stdout(); +} + +#[test] +fn minting_rune_with_no_rune_index_fails() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]); + + core.mine_blocks(1); + + create_wallet(&core, &ord); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: `ord wallet etch` requires index created with `--index-runes` flag\n") + .run_and_extract_stdout(); +} + +#[test] +fn minting_rune_and_then_sending_works() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + core.mine_blocks(1); + + create_wallet(&core, &ord); + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "111".parse().unwrap(), + supply: "132".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(10), + start: None, + }), + amount: "21".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let balance = CommandBuilder::new("--chain regtest --index-runes wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert_eq!( + *balance.runes.unwrap().first_key_value().unwrap().1, + 111_u128 + ); + + let output = CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let balance = CommandBuilder::new("--chain regtest --index-runes wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert_eq!( + *balance.runes.unwrap().first_key_value().unwrap().1, + 132_u128 + ); + + pretty_assert_eq!( + output.pile, + Pile { + amount: 21, + divisibility: 0, + symbol: Some('¢'), + } + ); + + CommandBuilder::new(format!( + "--regtest --index-runes wallet send bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 5:{} --fee-rate 1", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); +} diff --git a/tests/wallet/outputs.rs b/tests/wallet/outputs.rs index cc7bd9c824..a606841277 100644 --- a/tests/wallet/outputs.rs +++ b/tests/wallet/outputs.rs @@ -2,19 +2,19 @@ use {super::*, ord::subcommand::wallet::outputs::Output}; #[test] fn outputs() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let coinbase_tx = &bitcoin_rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0]; + let coinbase_tx = &core.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0]; let outpoint = OutPoint::new(coinbase_tx.txid(), 0); let amount = coinbase_tx.output[0].value; let output = CommandBuilder::new("wallet outputs") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].output, outpoint); @@ -23,21 +23,21 @@ fn outputs() { #[test] fn outputs_includes_locked_outputs() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let coinbase_tx = &bitcoin_rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0]; + let coinbase_tx = &core.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0]; let outpoint = OutPoint::new(coinbase_tx.txid(), 0); let amount = coinbase_tx.output[0].value; - bitcoin_rpc_server.lock(outpoint); + core.lock(outpoint); let output = CommandBuilder::new("wallet outputs") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].output, outpoint); @@ -46,21 +46,21 @@ fn outputs_includes_locked_outputs() { #[test] fn outputs_includes_unbound_outputs() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let coinbase_tx = &bitcoin_rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0]; + let coinbase_tx = &core.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0]; let outpoint = OutPoint::new(coinbase_tx.txid(), 0); let amount = coinbase_tx.output[0].value; - bitcoin_rpc_server.lock(outpoint); + core.lock(outpoint); let output = CommandBuilder::new("wallet outputs") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].output, outpoint); diff --git a/tests/wallet/receive.rs b/tests/wallet/receive.rs index 448c19af52..210e32de57 100644 --- a/tests/wallet/receive.rs +++ b/tests/wallet/receive.rs @@ -2,15 +2,19 @@ use {super::*, ord::subcommand::wallet::receive}; #[test] fn receive() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); let output = CommandBuilder::new("wallet receive") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); - assert!(output.address.is_valid_for_network(Network::Bitcoin)); + assert!(output + .addresses + .first() + .unwrap() + .is_valid_for_network(Network::Bitcoin)); } diff --git a/tests/wallet/restore.rs b/tests/wallet/restore.rs index ba917da30e..66b594e580 100644 --- a/tests/wallet/restore.rs +++ b/tests/wallet/restore.rs @@ -3,40 +3,40 @@ use {super::*, ord::subcommand::wallet::create}; #[test] fn restore_generates_same_descriptors() { let (mnemonic, descriptors) = { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let create::Output { mnemonic, .. } = CommandBuilder::new("wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output(); - (mnemonic, rpc_server.descriptors()) + (mnemonic, core.descriptors()) }; - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new(["wallet", "restore", "--from", "mnemonic"]) .stdin(mnemonic.to_string().into()) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); - assert_eq!(rpc_server.descriptors(), descriptors); + assert_eq!(core.descriptors(), descriptors); } #[test] fn restore_generates_same_descriptors_with_passphrase() { let passphrase = "foo"; let (mnemonic, descriptors) = { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let create::Output { mnemonic, .. } = CommandBuilder::new(["wallet", "create", "--passphrase", passphrase]) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output(); - (mnemonic, rpc_server.descriptors()) + (mnemonic, core.descriptors()) }; - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new([ "wallet", @@ -47,31 +47,31 @@ fn restore_generates_same_descriptors_with_passphrase() { "mnemonic", ]) .stdin(mnemonic.to_string().into()) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); - assert_eq!(rpc_server.descriptors(), descriptors); + assert_eq!(core.descriptors(), descriptors); } #[test] fn restore_to_existing_wallet_fails() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let descriptors = bitcoin_rpc_server.descriptors(); + let descriptors = core.descriptors(); let output = CommandBuilder::new("wallet dump") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stderr_regex(".*") .run_and_deserialize_output::(); CommandBuilder::new("wallet restore --from descriptor") .stdin(serde_json::to_string(&output).unwrap().as_bytes().to_vec()) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: wallet `ord` already exists\n") .run_and_extract_stdout(); @@ -88,7 +88,7 @@ fn restore_to_existing_wallet_fails() { #[test] fn restore_with_wrong_descriptors_fails() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("wallet --name foo restore --from descriptor") .stdin(r#" @@ -135,7 +135,7 @@ fn restore_with_wrong_descriptors_fails() { } ] }"#.into()) - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .expected_exit_code(1) .expected_stderr("error: wallet \"foo\" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`\n") .run_and_extract_stdout(); @@ -143,11 +143,11 @@ fn restore_with_wrong_descriptors_fails() { #[test] fn restore_with_compact_works() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new("wallet restore --from descriptor") .stdin(r#"{"wallet_name":"foo","descriptors":[{"desc":"rawtr(cVMYXp8uf1yFU9AAY6NJu1twA2uT94mHQBGkfgqCCzp6RqiTWCvP)#tah5crv7","timestamp":1706047934,"active":false,"internal":null,"range":null,"next":null},{"desc":"rawtr(cVdVu6VRwYXsTPMiptqVYLcp7EtQi5sjxLzbPTSNwW6CkCxBbEFs)#5afaht8d","timestamp":1706047934,"active":false,"internal":null,"range":null,"next":null},{"desc":"tr([c0b9536d/86'/1'/0']tprv8fXhtVjj3vb7kgxKuiWXzcUsur44gbLbbtwxL4HKmpzkBNoMrYqbQhMe7MWhrZjLFc9RBpTRYZZkrS8HH1Q3SmD5DkfpjKqtd97q1JWfqzr/0/*)#dweuu0ww","timestamp":1706047839,"active":true,"internal":false,"range":[0,1000],"next":1},{"desc":"tr([c0b9536d/86'/1'/0']tprv8fXhtVjj3vb7kgxKuiWXzcUsur44gbLbbtwxL4HKmpzkBNoMrYqbQhMe7MWhrZjLFc9RBpTRYZZkrS8HH1Q3SmD5DkfpjKqtd97q1JWfqzr/1/*)#u6uap67k","timestamp":1706047839,"active":true,"internal":true,"range":[0,1013],"next":14}]}"#.into()) - .bitcoin_rpc_server(&bitcoin_rpc_server) + .core(&core) .expected_exit_code(0) .run_and_extract_stdout(); } @@ -155,29 +155,29 @@ fn restore_with_compact_works() { #[test] fn restore_with_blank_mnemonic_generates_same_descriptors() { let (mnemonic, descriptors) = { - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); let create::Output { mnemonic, .. } = CommandBuilder::new("wallet create") - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_deserialize_output(); - (mnemonic, rpc_server.descriptors()) + (mnemonic, core.descriptors()) }; - let rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); CommandBuilder::new(["wallet", "restore", "--from", "mnemonic"]) .stdin(mnemonic.to_string().into()) - .bitcoin_rpc_server(&rpc_server) + .core(&core) .run_and_extract_stdout(); - assert_eq!(rpc_server.descriptors(), descriptors); + assert_eq!(core.descriptors(), descriptors); } #[test] fn passphrase_conflicts_with_descriptor() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn(&bitcoin_rpc_server); + let core = mockcore::spawn(); + let ord = TestServer::spawn(&core); CommandBuilder::new([ "wallet", @@ -188,8 +188,8 @@ fn passphrase_conflicts_with_descriptor() { "supersecurepassword", ]) .stdin("".into()) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: descriptor does not take a passphrase\n") .run_and_extract_stdout(); diff --git a/tests/wallet/resume.rs b/tests/wallet/resume.rs new file mode 100644 index 0000000000..a160e00b9d --- /dev/null +++ b/tests/wallet/resume.rs @@ -0,0 +1,232 @@ +use { + super::*, + nix::{ + sys::signal::{self, Signal}, + unistd::Pid, + }, +}; + +#[test] +fn wallet_resume() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let batchfile = batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + ..default() + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }; + + let tempdir = Arc::new(TempDir::new().unwrap()); + + { + let mut spawn = + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .temp_dir(tempdir.clone()) + .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap()) + .write("inscription.jpeg", "inscription") + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_regex_match!(buffer, "Waiting for rune commitment .* to mature…\n"); + + core.mine_blocks(1); + + signal::kill( + Pid::from_raw(spawn.child.id().try_into().unwrap()), + Signal::SIGINT, + ) + .unwrap(); + + buffer.clear(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!( + buffer, + "Shutting down gracefully. Press again to shutdown immediately.\n" + ); + + spawn.child.wait().unwrap(); + } + + core.mine_blocks(6); + + let mut spawn = CommandBuilder::new("--regtest --index-runes wallet resume") + .temp_dir(tempdir) + .core(&core) + .ord(&ord) + .spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_regex_match!(buffer, "Waiting for rune commitment .* to mature…\n"); + + let output = spawn.run_and_deserialize_output::(); + + assert_eq!( + output + .etchings + .first() + .unwrap() + .rune + .clone() + .unwrap() + .rune + .rune, + Rune(RUNE) + ); + + assert!(output.etchings.first().unwrap().reveal_broadcast); +} + +#[test] +fn resume_suspended() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let batchfile = batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + ..default() + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }; + + let tempdir = Arc::new(TempDir::new().unwrap()); + + { + let mut spawn = + CommandBuilder::new("--regtest --index-runes wallet batch --fee-rate 0 --batch batch.yaml") + .temp_dir(tempdir.clone()) + .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap()) + .write("inscription.jpeg", "inscription") + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_regex_match!(buffer, "Waiting for rune commitment .* to mature…\n"); + + core.mine_blocks(1); + + signal::kill( + Pid::from_raw(spawn.child.id().try_into().unwrap()), + Signal::SIGINT, + ) + .unwrap(); + + buffer.clear(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!( + buffer, + "Shutting down gracefully. Press again to shutdown immediately.\n" + ); + + spawn.child.wait().unwrap(); + } + + let mut spawn = CommandBuilder::new("--regtest --index-runes wallet resume") + .temp_dir(tempdir) + .core(&core) + .ord(&ord) + .spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_regex_match!(buffer, "Waiting for rune commitment .* to mature…\n"); + + buffer.clear(); + + signal::kill( + Pid::from_raw(spawn.child.id().try_into().unwrap()), + Signal::SIGINT, + ) + .unwrap(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!( + buffer, + "Shutting down gracefully. Press again to shutdown immediately.\n" + ); + + buffer.clear(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!( + buffer, + "Suspending batch. Run `ord wallet resume` to continue.\n" + ); + + let output = spawn.run_and_deserialize_output::(); + + assert!(!output.etchings.first().unwrap().reveal_broadcast); +} diff --git a/tests/wallet/sats.rs b/tests/wallet/sats.rs index 3eea9b8b03..f25826fe65 100644 --- a/tests/wallet/sats.rs +++ b/tests/wallet/sats.rs @@ -5,15 +5,15 @@ use { #[test] fn requires_sat_index() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new("wallet sats") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: sats requires index created with `--index-sats` flag\n") .run_and_extract_stdout(); @@ -21,18 +21,17 @@ fn requires_sat_index() { #[test] fn sats() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let second_coinbase = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let second_coinbase = core.mine_blocks(1)[0].txdata[0].txid(); let output = CommandBuilder::new("--index-sats wallet sats") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].sat, 50 * COIN_VALUE); @@ -41,19 +40,18 @@ fn sats() { #[test] fn sats_from_tsv_success() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let second_coinbase = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let second_coinbase = core.mine_blocks(1)[0].txdata[0].txid(); let output = CommandBuilder::new("--index-sats wallet sats --tsv foo.tsv") .write("foo.tsv", "nvtcsezkbtg") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); assert_eq!(output[0].sat, "nvtcsezkbtg"); @@ -62,17 +60,16 @@ fn sats_from_tsv_success() { #[test] fn sats_from_tsv_parse_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new("--index-sats wallet sats --tsv foo.tsv") .write("foo.tsv", "===") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr( "error: failed to parse sat from string \"===\" on line 1: failed to parse sat `===`: invalid integer: invalid digit found in string\n", @@ -82,16 +79,15 @@ fn sats_from_tsv_parse_error() { #[test] fn sats_from_tsv_file_not_found() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new("--index-sats wallet sats --tsv foo.tsv") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .stderr_regex("error: I/O error reading `.*`\nbecause: .*\n") .run_and_extract_stdout(); diff --git a/tests/wallet/selection.rs b/tests/wallet/selection.rs new file mode 100644 index 0000000000..8dd6e8cd9e --- /dev/null +++ b/tests/wallet/selection.rs @@ -0,0 +1,210 @@ +use super::*; + +#[test] +fn inscribe_does_not_select_runic_utxos() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + etch(&core, &ord, Rune(RUNE)); + + drain(&core, &ord); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") + .write("foo.txt", "FOO") + .core(&core) + .ord(&ord) + .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 core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + etch(&core, &ord, Rune(RUNE)); + + drain(&core, &ord); + + CommandBuilder::new("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 600sat") + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} + +#[test] +fn send_satpoint_does_not_send_runic_utxos() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks_with_subsidy(1, 10000); + + let etched = etch(&core, &ord, Rune(RUNE)); + + CommandBuilder::new(format!( + " + --regtest + --index-runes + wallet + send + --fee-rate 1 + bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + {}:0 + ", + etched.output.rune.unwrap().location.unwrap() + )) + .core(&core) + .ord(&ord) + .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 core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[]); + + create_wallet(&core, &ord); + + etch(&core, &ord, Rune(RUNE)); + + let (id, _) = inscribe(&core, &ord); + + drain(&core, &ord); + + CommandBuilder::new( + format!( + " + --regtest + --index-runes + wallet + send + --postage 10000sat + --fee-rate 1 + bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + {id} + ")) + .core(&core) + .ord(&ord) + .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(); +} + +#[test] +fn mint_does_not_select_inscription() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + create_wallet(&core, &ord); + + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 1, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + supply: "2000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + amount: "1000".parse().unwrap(), + offset: None, + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + drain(&core, &ord); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 0 --rune {}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} + +#[test] +fn sending_rune_does_not_send_inscription() { + let core = mockcore::builder().network(Network::Regtest).build(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks_with_subsidy(1, 10000); + + let rune = Rune(RUNE); + + CommandBuilder::new("--chain regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") + .write("foo.txt", "FOO") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks_with_subsidy(1, 10000); + + pretty_assert_eq!( + CommandBuilder::new("--regtest --index-runes wallet balance") + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(), + Balance { + cardinal: 10000, + ordinal: 10000, + runic: Some(0), + runes: Some(BTreeMap::new()), + total: 20000, + } + ); + + etch(&core, &ord, rune); + + drain(&core, &ord); + + CommandBuilder::new(format!( + " + --chain regtest + --index-runes + wallet send + --fee-rate 0 + bcrt1pyrmadgg78e38ewfv0an8c6eppk2fttv5vnuvz04yza60qau5va0saknu8k + 1000:{rune} + ", + )) + .core(&core) + .ord(&ord) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index bcab8969e5..7ceeef62de 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -1,41 +1,35 @@ -use { - super::*, - base64::Engine, - bitcoin::psbt::Psbt, - ord::subcommand::wallet::{balance, create, send}, - std::collections::BTreeMap, -}; +use {super::*, base64::Engine, bitcoin::psbt::Psbt}; #[test] fn inscriptions_can_be_sent() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}", )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stdout_regex(r".*") - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); - let txid = bitcoin_rpc_server.mempool()[0].txid(); + let txid = core.mempool()[0].txid(); assert_eq!(txid, output.txid); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let send_txid = output.txid; - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{inscription}"), format!( ".*

        Inscription 0

        .*
        .* @@ -55,50 +49,50 @@ fn inscriptions_can_be_sent() { #[test] fn send_unknown_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = core.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {txid}i0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_stderr(format!("error: inscription {txid}i0 not found\n")) .expected_exit_code(1) .run_and_extract_stdout(); } #[test] -fn send_inscribed_sat() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); +fn send_inscribed_inscription() { + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}", )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let send_txid = output.txid; - ord_rpc_server.assert_response_regex( + ord.assert_response_regex( format!("/inscription/{inscription}"), format!( ".*

        Inscription 0

        .*
        location
        .*
        {send_txid}:0:0
        .*", @@ -106,42 +100,102 @@ fn send_inscribed_sat() { ); } +#[test] +fn send_uninscribed_sat() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + let sat = Sat(1); + + CommandBuilder::new(format!( + "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {}", + sat.name(), + )) + .core(&core) + .ord(&ord) + .expected_stderr(format!( + "error: could not find sat `{sat}` in wallet outputs\n" + )) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn send_inscription_by_sat() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (inscription, txid) = inscribe(&core, &ord); + + core.mine_blocks(1); + + let sat_list = sats(&core, &ord); + + let sat = sat_list.iter().find(|s| s.output.txid == txid).unwrap().sat; + + let address = "bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv"; + + let output = CommandBuilder::new(format!("wallet send --fee-rate 1 {address} {}", sat.name())) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let send_txid = output.txid; + + ord.assert_response_regex( + format!("/inscription/{inscription}"), + format!( + ".*

        Inscription 0

        .*
        address
        .*
        {address}
        .*
        location
        .*
        {send_txid}:0:0
        .*", + ), + ); +} + #[test] fn send_on_mainnnet_works_with_wallet_named_foo() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - let txid = bitcoin_rpc_server.mine_blocks(1)[0].txdata[0].txid(); + let txid = core.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new("wallet --name foo create") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); CommandBuilder::new(format!( "wallet --name foo send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); } #[test] fn send_addresses_must_be_valid_for_network() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder().build(); + let core = mockcore::builder().build(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks_with_subsidy(1, 1_000)[0].txdata[0].txid(); + let txid = core.mine_blocks_with_subsidy(1, 1_000)[0].txdata[0].txid(); CommandBuilder::new(format!( "wallet send --fee-rate 1 tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz {txid}:0:0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_stderr( "error: address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz belongs to network testnet which is different from required bitcoin\n", ) @@ -151,47 +205,47 @@ fn send_addresses_must_be_valid_for_network() { #[test] fn send_on_mainnnet_works_with_wallet_named_ord() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder().build(); + let core = mockcore::builder().build(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0].txid(); + let txid = core.mine_blocks_with_subsidy(1, 1_000_000)[0].txdata[0].txid(); let output = CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - assert_eq!(bitcoin_rpc_server.mempool()[0].txid(), output.txid); + assert_eq!(core.mempool()[0].txid(), output.txid); } #[test] fn send_does_not_use_inscribed_sats_as_cardinal_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let txid = bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10_000)[0].txdata[0].txid(); + let txid = core.mine_blocks_with_subsidy(1, 10_000)[0].txdata[0].txid(); CommandBuilder::new(format!( "wallet inscribe --satpoint {txid}:0:0 --file degenerate.png --fee-rate 0" )) .write("degenerate.png", [1; 100]) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let txid = bitcoin_rpc_server.mine_blocks_with_subsidy(1, 100)[0].txdata[0].txid(); + let txid = core.mine_blocks_with_subsidy(1, 100)[0].txdata[0].txid(); CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") .run_and_extract_stdout(); @@ -199,15 +253,15 @@ fn send_does_not_use_inscribed_sats_as_cardinal_utxos() { #[test] fn do_not_send_within_dust_limit_of_an_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (inscription, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, reveal) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = OutPoint { txid: reveal, @@ -217,8 +271,8 @@ fn do_not_send_within_dust_limit_of_an_inscription() { CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:329" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr(format!( "error: cannot send {output}:329 without also sending inscription {inscription} at {output}:0\n" @@ -228,15 +282,15 @@ fn do_not_send_within_dust_limit_of_an_inscription() { #[test] fn can_send_after_dust_limit_from_an_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (_, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_, reveal) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = OutPoint { txid: reveal, @@ -246,38 +300,45 @@ fn can_send_after_dust_limit_from_an_inscription() { CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:330" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); } #[test] fn splitting_merged_inscriptions_is_possible() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-sats"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(3); + core.mine_blocks(1); - let inscription = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); + let inscribe = CommandBuilder::new("wallet batch --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "INSCRIPTION") + .write( + "batch.yaml", + "\ +mode: shared-output - // merging 3 inscriptions into one utxo - let reveal_txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[ - (1, 0, 0, inscription.clone()), - (2, 0, 0, inscription.clone()), - (3, 0, 0, inscription.clone()), - ], - outputs: 1, - ..Default::default() - }); +inscriptions: +- file: inscription.txt +- file: inscription.txt +- file: inscription.txt +", + ) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + let reveal_txid = inscribe.reveal; - bitcoin_rpc_server.mine_blocks(1); + let destination = inscribe.inscriptions[0].destination.clone(); - let response = ord_rpc_server.json_request(format!("/output/{}:0", reveal_txid)); + core.mine_blocks(1); + + let response = ord.json_request(format!("/output/{}:0", reveal_txid)); assert_eq!(response.status(), StatusCode::OK); let output_json: api::Output = serde_json::from_str(&response.text().unwrap()).unwrap(); @@ -285,7 +346,7 @@ fn splitting_merged_inscriptions_is_possible() { pretty_assert_eq!( output_json, api::Output { - address: None, + address: Some(destination.clone()), inscriptions: vec![ InscriptionId { txid: reveal_txid, @@ -302,15 +363,11 @@ fn splitting_merged_inscriptions_is_possible() { ], indexed: true, runes: Vec::new(), - sat_ranges: Some(vec![ - (5000000000, 10000000000,), - (10000000000, 15000000000,), - (15000000000, 20000000000,), - ],), - script_pubkey: "".to_string(), + sat_ranges: Some(vec![(5_000_000_000, 5_000_030_000)]), + script_pubkey: destination.payload.script_pubkey().to_asm_string(), spent: false, transaction: reveal_txid.to_string(), - value: 3 * 50 * COIN_VALUE, + value: 30_000, } ); @@ -319,11 +376,11 @@ fn splitting_merged_inscriptions_is_possible() { "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", reveal_txid, )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr(format!( - "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:{}\n", 100 * COIN_VALUE + "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:20000\n", )) .run_and_extract_stdout(); @@ -332,50 +389,50 @@ fn splitting_merged_inscriptions_is_possible() { "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i2", reveal_txid, )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); // splitting second to last CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i1", reveal_txid, )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); // splitting send first CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", reveal_txid, )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); } #[test] fn inscriptions_cannot_be_sent_by_satpoint() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let (_, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_, reveal) = inscribe(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new(format!( "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {reveal}:0:0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_stderr("error: inscriptions must be sent by inscription ID\n") .expected_exit_code(1) .run_and_extract_stdout(); @@ -383,26 +440,26 @@ fn inscriptions_cannot_be_sent_by_satpoint() { #[test] fn send_btc_with_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new( "wallet send --fee-rate 13.3 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 2btc", ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let tx = &bitcoin_rpc_server.mempool()[0]; + let tx = &core.mempool()[0]; let mut fee = 0; for input in &tx.input { - fee += bitcoin_rpc_server + fee += core .get_utxo_amount(&input.previous_output) .unwrap() .to_sat(); @@ -429,22 +486,22 @@ fn send_btc_with_fee_rate() { #[test] fn send_btc_locks_inscriptions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (_, reveal) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (_, reveal) = inscribe(&core, &ord); CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - assert!(bitcoin_rpc_server.get_locked().contains(&OutPoint { + assert!(core.get_locked().contains(&OutPoint { txid: reveal, vout: 0, })) @@ -452,19 +509,17 @@ fn send_btc_locks_inscriptions() { #[test] fn send_btc_fails_if_lock_unspent_fails() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .fail_lock_unspent(true) - .build(); + let core = mockcore::builder().fail_lock_unspent(true).build(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_stderr("error: failed to lock UTXOs\n") .expected_exit_code(1) .run_and_extract_stdout(); @@ -472,27 +527,27 @@ fn send_btc_fails_if_lock_unspent_fails() { #[test] fn wallet_send_with_fee_rate() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); CommandBuilder::new(format!( "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let tx = &bitcoin_rpc_server.mempool()[0]; + let tx = &core.mempool()[0]; let mut fee = 0; for input in &tx.input { - fee += bitcoin_rpc_server + fee += core .get_utxo_amount(&input.previous_output) .unwrap() .to_sat(); @@ -508,21 +563,21 @@ fn wallet_send_with_fee_rate() { #[test] fn user_must_provide_fee_rate_to_send() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); CommandBuilder::new(format!( "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(2) .stderr_regex( ".*error: the following required arguments were not provided: @@ -533,27 +588,27 @@ fn user_must_provide_fee_rate_to_send() { #[test] fn wallet_send_with_fee_rate_and_target_postage() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + let (inscription, _) = inscribe(&core, &ord); CommandBuilder::new(format!( "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0 --postage 77000sat" )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - let tx = &bitcoin_rpc_server.mempool()[0]; + let tx = &core.mempool()[0]; let mut fee = 0; for input in &tx.input { - fee += bitcoin_rpc_server + fee += core .get_utxo_amount(&input.previous_output) .unwrap() .to_sat(); @@ -570,44 +625,78 @@ fn wallet_send_with_fee_rate_and_target_postage() { #[test] fn send_btc_does_not_send_locked_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + let core = mockcore::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let coinbase_tx = &bitcoin_rpc_server.mine_blocks(1)[0].txdata[0]; + let coinbase_tx = &core.mine_blocks(1)[0].txdata[0]; let outpoint = OutPoint::new(coinbase_tx.txid(), 0); - bitcoin_rpc_server.lock(outpoint); + core.lock(outpoint); CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .stderr_regex("error:.*") .run_and_extract_stdout(); } +#[test] +fn send_dry_run() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (inscription, _) = inscribe(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new(format!( + "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription} --dry-run", + )) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + assert!(core.mempool().is_empty()); + assert_eq!( + Psbt::deserialize( + &base64::engine::general_purpose::STANDARD + .decode(output.psbt) + .unwrap() + ) + .unwrap() + .fee() + .unwrap() + .to_sat(), + output.fee + ); + assert_eq!(output.outgoing, Outgoing::InscriptionId(inscription)); +} + #[test] fn sending_rune_that_has_not_been_etched_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - let coinbase_tx = &bitcoin_rpc_server.mine_blocks(1)[0].txdata[0]; + let coinbase_tx = &core.mine_blocks(1)[0].txdata[0]; let outpoint = OutPoint::new(coinbase_tx.txid(), 0); - bitcoin_rpc_server.lock(outpoint); + core.lock(outpoint); - CommandBuilder::new("--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + CommandBuilder::new("--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1:FOO") + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: rune `FOO` has not been etched\n") .run_and_extract_stdout(); @@ -615,23 +704,20 @@ fn sending_rune_that_has_not_been_etched_is_an_error() { #[test] fn sending_rune_with_excessive_precision_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + etch(&core, &ord, Rune(RUNE)); CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1.1{}", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1.1:{}", Rune(RUNE) )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: excessive precision\n") .run_and_extract_stdout(); @@ -639,67 +725,65 @@ fn sending_rune_with_excessive_precision_is_an_error() { #[test] fn sending_rune_with_insufficient_balance_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + etch(&core, &ord, Rune(RUNE)); CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1001{}", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1001:{}", Rune(RUNE) )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) - .expected_stderr("error: insufficient `AAAAAAAAAAAAA` balance, only 1000\u{00A0}¢ in wallet\n") + .expected_stderr("error: insufficient `AAAAAAAAAAAAA` balance, only 1000\u{A0}¢ in wallet\n") .run_and_extract_stdout(); } #[test] fn sending_rune_works() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + etch(&core, &ord, Rune(RUNE)); let output = CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000{}", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000:{}", Rune(RUNE) )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let balances = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![( OutPoint { txid: output.txid, vout: 2 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() @@ -712,42 +796,43 @@ fn sending_rune_works() { #[test] fn sending_spaced_rune_works() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + etch(&core, &ord, Rune(RUNE)); let output = CommandBuilder::new( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000A•AAAAAAAAAAAA", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000:A•AAAAAAAAAAAA", ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let balances = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![( OutPoint { txid: output.txid, vout: 2 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() @@ -760,65 +845,78 @@ fn sending_spaced_rune_works() { #[test] fn sending_rune_with_divisibility_works() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let rune = Rune(RUNE); - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 100 --symbol ¢", - rune, - ) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); + batch( + &core, + &ord, + batch::File { + etching: Some(batch::Etching { + divisibility: 1, + rune: SpacedRune { rune, spacers: 0 }, + premine: "1000".parse().unwrap(), + supply: "1000".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); let output = CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 10.1{}", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 10.1:{}", rune )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let balances = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); - assert_eq!( + pretty_assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![ ( OutPoint { txid: output.txid, vout: 1 }, - 899 + Pile { + amount: 9899, + divisibility: 1, + symbol: Some('¢') + }, ), ( OutPoint { txid: output.txid, vout: 2 }, - 101 + Pile { + amount: 101, + divisibility: 1, + symbol: Some('¢') + }, ) ] .into_iter() @@ -832,51 +930,56 @@ fn sending_rune_with_divisibility_works() { #[test] fn sending_rune_leaves_unspent_runes_in_wallet() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + etch(&core, &ord, Rune(RUNE)); let output = CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 750{}", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 750:{}", Rune(RUNE) )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let balances = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![ ( OutPoint { txid: output.txid, vout: 1 }, - 250 + Pile { + amount: 250, + divisibility: 0, + symbol: Some('¢') + }, ), ( OutPoint { txid: output.txid, vout: 2 }, - 750 + Pile { + amount: 750, + divisibility: 0, + symbol: Some('¢') + }, ) ] .into_iter() @@ -887,65 +990,72 @@ fn sending_rune_leaves_unspent_runes_in_wallet() { } ); - let tx = bitcoin_rpc_server.tx(3, 1); - - assert_eq!(tx.txid(), output.txid); + let tx = core.tx_by_id(output.txid); let address = Address::from_script(&tx.output[1].script_pubkey, Network::Regtest).unwrap(); - assert!(bitcoin_rpc_server - .change_addresses() - .iter() - .any(|change_address| change_address == &address)); + assert!(core.state().change_addresses.contains(&address)); } #[test] fn sending_rune_creates_transaction_with_expected_runestone() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + let etch = etch(&core, &ord, Rune(RUNE)); let output = CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 750{}", + " + --chain regtest + --index-runes + wallet + send + --fee-rate 1 + bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 750:{} + ", Rune(RUNE), )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let balances = CommandBuilder::new("--regtest --index-runes balances") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::(); assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( - Rune(RUNE), + SpacedRune::new(Rune(RUNE), 0), vec![ ( OutPoint { txid: output.txid, vout: 1 }, - 250 + Pile { + amount: 250, + divisibility: 0, + symbol: Some('¢') + }, ), ( OutPoint { txid: output.txid, vout: 2 }, - 750 + Pile { + amount: 750, + divisibility: 0, + symbol: Some('¢') + }, ) ] .into_iter() @@ -956,167 +1066,46 @@ fn sending_rune_creates_transaction_with_expected_runestone() { } ); - let tx = bitcoin_rpc_server.tx(3, 1); + let tx = core.tx_by_id(output.txid); - assert_eq!(tx.txid(), output.txid); - - assert_eq!( - Runestone::from_transaction(&tx).unwrap(), - Runestone { - default_output: None, + pretty_assert_eq!( + Runestone::decipher(&tx).unwrap(), + Artifact::Runestone(Runestone { + pointer: None, etching: None, edicts: vec![Edict { - id: RuneId { - height: 2, - index: 1 - } - .into(), + id: etch.id, amount: 750, output: 2 }], - burn: false, - claim: None, - }, + mint: None, + }), ); } #[test] fn error_messages_use_spaced_runes() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); + let core = mockcore::builder().network(Network::Regtest).build(); - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + etch(&core, &ord, Rune(RUNE)); CommandBuilder::new( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1001A•AAAAAAAAAAAA", + "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1001:A•AAAAAAAAAAAA", ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .expected_exit_code(1) - .expected_stderr("error: insufficient `A•AAAAAAAAAAAA` balance, only 1000\u{00A0}¢ in wallet\n") + .expected_stderr("error: insufficient `A•AAAAAAAAAAAA` balance, only 1000\u{A0}¢ in wallet\n") .run_and_extract_stdout(); - CommandBuilder::new("--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1F•OO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + CommandBuilder::new("--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1:F•OO") + .core(&core) + .ord(&ord) .expected_exit_code(1) .expected_stderr("error: rune `FOO` has not been etched\n") .run_and_extract_stdout(); } - -#[test] -fn sending_rune_does_not_send_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - let rune = Rune(RUNE); - - CommandBuilder::new("--chain regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - assert_eq!( - CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - balance::Output { - cardinal: 10000, - ordinal: 10000, - runic: Some(0), - runes: Some(BTreeMap::new()), - total: 20000, - } - ); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 0 --supply 1000 --symbol ¢", - rune - ) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - assert_eq!( - CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - balance::Output { - cardinal: 0, - ordinal: 10000, - runic: Some(10000), - runes: Some(vec![(rune, 1000)].into_iter().collect()), - total: 20000, - } - ); - - CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 0 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000{}", - rune - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .stderr_regex("error:.*") - .run_and_extract_stdout(); -} - -#[test] -fn send_dry_run() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription} --dry-run", - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert!(bitcoin_rpc_server.mempool().is_empty()); - assert_eq!( - Psbt::deserialize( - &base64::engine::general_purpose::STANDARD - .decode(output.psbt) - .unwrap() - ) - .unwrap() - .fee() - .unwrap() - .to_sat(), - output.fee - ); - assert_eq!(output.outgoing, Outgoing::InscriptionId(inscription)); -} diff --git a/tests/wallet/transactions.rs b/tests/wallet/transactions.rs index e728e58371..3af55aad3d 100644 --- a/tests/wallet/transactions.rs +++ b/tests/wallet/transactions.rs @@ -2,70 +2,66 @@ use {super::*, ord::subcommand::wallet::transactions::Output}; #[test] fn transactions() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); - assert!(bitcoin_rpc_server.loaded_wallets().is_empty()); + assert!(core.loaded_wallets().is_empty()); CommandBuilder::new("wallet transactions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); - assert_eq!(bitcoin_rpc_server.loaded_wallets().len(), 1); - assert_eq!(bitcoin_rpc_server.loaded_wallets().first().unwrap(), "ord"); + assert_eq!(core.loaded_wallets().len(), 1); + assert_eq!(core.loaded_wallets().first().unwrap(), "ord"); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("wallet transactions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); - assert_regex_match!(output[0].transaction.to_string(), "[[:xdigit:]]{64}"); assert_eq!(output[0].confirmations, 1); } #[test] fn transactions_with_limit() { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + let core = mockcore::spawn(); + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + create_wallet(&core, &ord); CommandBuilder::new("wallet transactions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .stdout_regex(".*") .run_and_extract_stdout(); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("wallet transactions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); - assert_regex_match!(output[0].transaction.to_string(), "[[:xdigit:]]{64}"); - assert_eq!(output[0].confirmations, 1); + assert_eq!(output.len(), 1); - bitcoin_rpc_server.mine_blocks(1); + core.mine_blocks(1); let output = CommandBuilder::new("wallet transactions") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); - assert_regex_match!(output[1].transaction.to_string(), "[[:xdigit:]]{64}"); - assert_eq!(output[1].confirmations, 2); + assert_eq!(output.len(), 2); let output = CommandBuilder::new("wallet transactions --limit 1") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) + .core(&core) + .ord(&ord) .run_and_deserialize_output::>(); - assert_regex_match!(output[0].transaction.to_string(), "[[:xdigit:]]{64}"); - assert_eq!(output[0].confirmations, 1); + assert_eq!(output.len(), 1); }