diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3335f2c7..117b8d755c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,150 @@ Changelog ========= +[0.18.5](https://github.com/ordinals/ord/releases/tag/0.18.5) - 2023-05-09 +-------------------------------------------------------------------------- + +### Added +- Allow specifying different output formats ([#3424](https://github.com/ordinals/ord/pull/3424) by [bingryan](https://github.com/bingryan)) +- Allow higher rpcworkqueue limit conf ([#3615](https://github.com/ordinals/ord/pull/3615) by [JeremyRubin](https://github.com/JeremyRubin)) +- Show progress bar for etching ([#3673](https://github.com/ordinals/ord/pull/3673) by [twosatsmaxi](https://github.com/twosatsmaxi)) + +### Fixed +- Update sat-hunting.md ([#3724](https://github.com/ordinals/ord/pull/3724) by [cryptoni9n](https://github.com/cryptoni9n)) +- Update runes.md docs ([#3681](https://github.com/ordinals/ord/pull/3681) by [hantuzun](https://github.com/hantuzun)) +- Patch some omissions in the Chinese translation ([#3694](https://github.com/ordinals/ord/pull/3694) by [shadowv0vshadow](https://github.com/shadowv0vshadow)) +- Bump rustfmt version 2018 to 2021 ([#3721](https://github.com/ordinals/ord/pull/3721) by [bingryan](https://github.com/bingryan)) + +[0.18.4](https://github.com/ordinals/ord/releases/tag/0.18.4) - 2023-05-02 +-------------------------------------------------------------------------- + +### Added +- Clarify that inscriptions must be served from URLs with path /content/ ([#3209](https://github.com/ordinals/ord/pull/3209) by [Vanniix](https://github.com/Vanniix)) + +### Changed +- Persist config files for ord env command ([#3715](https://github.com/ordinals/ord/pull/3715) by [twosatsmaxi](https://github.com/twosatsmaxi)) +- Do not show runic outputs in cardinals command ([#3656](https://github.com/ordinals/ord/pull/3656) by [raphjaph](https://github.com/raphjaph)) + +### Fixed +- Fix send runes ([#3484](https://github.com/ordinals/ord/pull/3484) by [raphjaph](https://github.com/raphjaph)) +- Allow longer request body for JSON API ([#3655](https://github.com/ordinals/ord/pull/3655) by [raphjaph](https://github.com/raphjaph)) +- Allow minting if mint begins next block ([#3659](https://github.com/ordinals/ord/pull/3659) by [casey](https://github.com/casey)) + +### Misc +- Add alt text to preview image ([#3713](https://github.com/ordinals/ord/pull/3713) by [losingle](https://github.com/losingle)) +- Remove duplicate endpoint from explorer.md ([#3716](https://github.com/ordinals/ord/pull/3716) by [cryptoni9n](https://github.com/cryptoni9n)) +- Use correct content type for .mjs inscriptions ([#3712](https://github.com/ordinals/ord/pull/3712) by [casey](https://github.com/casey)) +- Add support for mjs files ([#3653](https://github.com/ordinals/ord/pull/3653) by [elocremarc](https://github.com/elocremarc)) +- Fix typo on sat hunting page ([#3668](https://github.com/ordinals/ord/pull/3668) by [cryptoni9n](https://github.com/cryptoni9n)) +- Use contains_key instead of get / is_some ([#3705](https://github.com/ordinals/ord/pull/3705) by [knowmost](https://github.com/knowmost)) +- Update sat-hunting.md with how to transfer specific sats ([#3666](https://github.com/ordinals/ord/pull/3666) by [cryptoni9n](https://github.com/cryptoni9n)) +- Fix zh.po translations ([#3588](https://github.com/ordinals/ord/pull/3588) by [losingle](https://github.com/losingle)) +- Update sparrow-wallet.md --name flag update ([#3635](https://github.com/ordinals/ord/pull/3635) by [taha-abbasi](https://github.com/taha-abbasi)) + +[0.18.3](https://github.com/ordinals/ord/releases/tag/0.18.3) - 2023-04-19 +-------------------------------------------------------------------------- + +### Added +- Add `dry-run` flag to `resume` command ([#3592](https://github.com/ordinals/ord/pull/3592) by [felipelincoln](https://github.com/felipelincoln)) +- Add back runes balances API ([#3571](https://github.com/ordinals/ord/pull/3571) by [lugondev](https://github.com/lugondev)) +- Show premine percentage ([#3567](https://github.com/ordinals/ord/pull/3567) by [raphjaph](https://github.com/raphjaph)) +- Add default content proxy and decompress to env command ([#3509](https://github.com/ordinals/ord/pull/3509) by [jahvi](https://github.com/jahvi)) + +### Changed +- Resume cycles through all pending etchings ([#3566](https://github.com/ordinals/ord/pull/3566) by [raphjaph](https://github.com/raphjaph)) + +### Misc +- Check rune minimum at height before sending ([#3626](https://github.com/ordinals/ord/pull/3626) by [raphjaph](https://github.com/raphjaph)) +- Update recursion.md with consistant syntax ([#3585](https://github.com/ordinals/ord/pull/3585) by [zmeyer44](https://github.com/zmeyer44)) +- Add test Rune cannot be minted less than limit amount ([#3556](https://github.com/ordinals/ord/pull/3556) by [lugondev](https://github.com/lugondev)) +- Clear etching when rune commitment is spent ([#3618](https://github.com/ordinals/ord/pull/3618) by [felipelincoln](https://github.com/felipelincoln)) +- Remove timeout for wallet client ([#3621](https://github.com/ordinals/ord/pull/3621) by [raphjaph](https://github.com/raphjaph)) +- Remove duplicated word ([#3598](https://github.com/ordinals/ord/pull/3598) by [oxSaturn](https://github.com/oxSaturn)) +- Address runes review comments ([#3605](https://github.com/ordinals/ord/pull/3605) by [casey](https://github.com/casey)) +- Generate sample batch.yaml in env command ([#3530](https://github.com/ordinals/ord/pull/3530) by [twosatsmaxi](https://github.com/twosatsmaxi)) + +[0.18.2](https://github.com/ordinals/ord/releases/tag/0.18.2) - 2023-04-17 +-------------------------------------------------------------------------- + +### Migration +- Wallet databases are now stored in the `/wallets` subdirectory of the data + dir. To use old wallet databases with 0.18.2, move `.redb` files + into the `/wallets` subdirectory of the data dir. Currently, the only + information stored in wallet databases are pending etchings. + +### Changed +- Store wallets in /wallets subdir of data dir ([#3553](https://github.com/ordinals/ord/pull/3553) by [casey](https://github.com/casey)) +- Remove /runes/balances page ([#3555](https://github.com/ordinals/ord/pull/3555) by [lugondev](https://github.com/lugondev)) +- Forbid etching below rune activation height ([#3523](https://github.com/ordinals/ord/pull/3523) by [casey](https://github.com/casey)) + +### Added +- Add command to export BIP-329 labels for wallet outputs ([#3120](https://github.com/ordinals/ord/pull/3120) by [casey](https://github.com/casey)) +- Display etched runes on /block ([#3366](https://github.com/ordinals/ord/pull/3366) by [lugondev](https://github.com/lugondev)) +- Emit rune-related events ([#3219](https://github.com/ordinals/ord/pull/3219) by [felipelincoln](https://github.com/felipelincoln)) +- Lookup rune by number ([#3440](https://github.com/ordinals/ord/pull/3440) by [lugondev](https://github.com/lugondev)) +- Add runes pagination ([#3215](https://github.com/ordinals/ord/pull/3215) by [lugondev](https://github.com/lugondev)) + +### Misc +- Document turbo flag ([#3579](https://github.com/ordinals/ord/pull/3579) by [gmart7t2](https://github.com/gmart7t2)) +- Add open mint tests ([#3558](https://github.com/ordinals/ord/pull/3558) by [lugondev](https://github.com/lugondev)) +- Fix typos ([#3541](https://github.com/ordinals/ord/pull/3541) by [StevenMia](https://github.com/StevenMia)) +- Fix typo in zh.po ([#3540](https://github.com/ordinals/ord/pull/3540) by [blackj-x](https://github.com/blackj-x)) +- Lock runes commit output ([#3504](https://github.com/ordinals/ord/pull/3504) by [raphjaph](https://github.com/raphjaph)) +- Address runes review comments ([#3547](https://github.com/ordinals/ord/pull/3547) by [casey](https://github.com/casey)) +- Add Red Had build instructions to readme ([#3531](https://github.com/ordinals/ord/pull/3531) by [rongyi](https://github.com/rongyi)) +- Fix typo in recursion docs ([#3529](https://github.com/ordinals/ord/pull/3529) by [nix-eth](https://github.com/nix-eth)) +- Put rune higher on /inscription ([#3363](https://github.com/ordinals/ord/pull/3363) by [lugondev](https://github.com/lugondev)) + +[0.18.1](https://github.com/ordinals/ord/releases/tag/0.18.1) - 2023-04-11 +-------------------------------------------------------------------------- + +### Fixed +- Fix off-by-one in wallet when waiting for etching commitment to mature ([#3515](https://github.com/ordinals/ord/pull/3515) by [casey](https://github.com/casey)) + +[0.18.0](https://github.com/ordinals/ord/releases/tag/0.18.0) - 2023-04-10 +-------------------------------------------------------------------------- + +### Fixed +- Check etching commit confirmations correctly ([#3507](https://github.com/ordinals/ord/pull/3507) by [casey](https://github.com/casey)) + +### Added +- Add postage flag to mint command ([#3482](https://github.com/ordinals/ord/pull/3482) by [ynohtna92](https://github.com/ynohtna92)) +- Mint with destination ([#3497](https://github.com/ordinals/ord/pull/3497) by [ynohtna92](https://github.com/ynohtna92)) +- Add etching turbo flag ([#3511](https://github.com/ordinals/ord/pull/3511) by [casey](https://github.com/casey)) +- Allow inscribing without file ([#3451](https://github.com/ordinals/ord/pull/3451) by [raphjaph](https://github.com/raphjaph)) +- Add wallet batch outputs and inscriptions endpoints ([#3456](https://github.com/ordinals/ord/pull/3456) by [raphjaph](https://github.com/raphjaph)) + +### Changed +- Show decimal rune balances ([#3505](https://github.com/ordinals/ord/pull/3505) by [raphjaph](https://github.com/raphjaph)) + +### Misc +- Test that mints without a cap are unmintable ([#3495](https://github.com/ordinals/ord/pull/3495) by [lugondev](https://github.com/lugondev)) +- Bump ord crate required rust version to 1.76 ([#3512](https://github.com/ordinals/ord/pull/3512) by [casey](https://github.com/casey)) +- Updated rust-version to 1.74.0 ([#3492](https://github.com/ordinals/ord/pull/3492) by [tgscan-dev](https://github.com/tgscan-dev)) +- Better error message when bitcoind doesn't start ([#3500](https://github.com/ordinals/ord/pull/3500) by [twosatsmaxi](https://github.com/twosatsmaxi)) +- Fix typo in zh.po ([#3498](https://github.com/ordinals/ord/pull/3498) by [RandolphJiffy](https://github.com/RandolphJiffy)) +- Document allowed opcodes in runestones ([#3461](https://github.com/ordinals/ord/pull/3461) by [casey](https://github.com/casey)) +- Update data carriersize to match with ord ([#3506](https://github.com/ordinals/ord/pull/3506) by [twosatsmaxi](https://github.com/twosatsmaxi)) +- Fix maturation loop ([#3480](https://github.com/ordinals/ord/pull/3480) by [raphjaph](https://github.com/raphjaph)) +- Add rune logo and link to navbar ([#3442](https://github.com/ordinals/ord/pull/3442) by [lugondev](https://github.com/lugondev)) +- Add package necessary for Ubuntu ([#3462](https://github.com/ordinals/ord/pull/3462) by [petriuslima](https://github.com/petriuslima)) +- Update required Rust version in README ([#3466](https://github.com/ordinals/ord/pull/3466) by [cryptoni9n](https://github.com/cryptoni9n)) +- Fix typo in zh.po ([#3464](https://github.com/ordinals/ord/pull/3464) by [RandolphJiffy](https://github.com/RandolphJiffy)) +- Update testing.md ([#3463](https://github.com/ordinals/ord/pull/3463) by [gmart7t2](https://github.com/gmart7t2)) +- Update rune docs for Chinese version ([#3457](https://github.com/ordinals/ord/pull/3457) by [DrJingLee](https://github.com/DrJingLee)) +- Remove `etch` from error message ([#3449](https://github.com/ordinals/ord/pull/3449) by [ordinariusprof](https://github.com/ordinariusprof)) +- Fix deploy bitcoin.conf typo ([#3443](https://github.com/ordinals/ord/pull/3443) by [bitspill](https://github.com/bitspill)) +- Fix type in runes docs ([#3447](https://github.com/ordinals/ord/pull/3447) by [twosatsmaxi](https://github.com/twosatsmaxi)) + [0.17.1](https://github.com/ordinals/ord/releases/tag/0.17.1) - 2023-04-01 -------------------------------------------------------------------------- -## Fixed -- Ignore invalid script pubkeys (#3432) +### Fixed +- Ignore invalid script pubkeys ([#3432](https://github.com/ordinals/ord/pull/3432) by [casey](https://github.com/casey)) -## Misc -- Fix typo (#3429) -- Relax deployed Bitcoin Core relay rules (#3431) +### Misc +- Fix typo ([#3429](https://github.com/ordinals/ord/pull/3429) by [lugondev](https://github.com/lugondev)) +- Relax deployed Bitcoin Core relay rules ([#3431](https://github.com/ordinals/ord/pull/3431) by [casey](https://github.com/casey)) [0.17.0](https://github.com/ordinals/ord/releases/tag/0.17.0) - 2023-03-31 -------------------------------------------------------------------------- @@ -93,7 +228,7 @@ Changelog - 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)) +- Add test to choose 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)) diff --git a/Cargo.lock b/Cargo.lock index cf098c6f1c..29813d15bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,47 +64,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -112,9 +113,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" dependencies = [ "backtrace", ] @@ -175,22 +176,22 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.1", + "event-listener 5.3.0", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a9249d1447a85f95810c620abea82e001fe58a31713fcce614caf52499f905" +checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" dependencies = [ "brotli", "flate2", @@ -266,19 +267,19 @@ dependencies = [ [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -297,7 +298,7 @@ dependencies = [ "js-sys", "lazy_static", "log", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "thiserror", "wasm-bindgen", @@ -363,9 +364,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -429,7 +430,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "rustls 0.21.10", + "rustls 0.21.12", "rustls-pemfile", "tokio", "tokio-rustls", @@ -465,9 +466,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bech32" @@ -551,18 +552,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", - "fastrand 2.0.2", "futures-io", "futures-lite 2.3.0", "piper", - "tracing", ] [[package]] @@ -576,14 +575,14 @@ dependencies = [ "new_mime_guess", "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] name = "brotli" -version = "3.5.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -592,9 +591,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -612,9 +611,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -636,9 +635,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -660,9 +659,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -670,7 +669,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -719,7 +718,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", ] [[package]] @@ -731,7 +730,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -742,9 +741,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" @@ -758,9 +757,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -950,7 +949,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -972,14 +971,14 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core 0.20.8", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der-parser" @@ -1103,14 +1102,14 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "encode_unicode" @@ -1120,9 +1119,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if 1.0.0", ] @@ -1158,9 +1157,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1185,9 +1184,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", "parking", @@ -1206,11 +1205,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.3.0", "pin-project-lite", ] @@ -1231,15 +1230,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1356,7 +1355,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -1366,7 +1365,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d8a2499f0fecc0492eb3e47eab4e92da7875e1028ad2528f214ac3346ca04e" dependencies = [ "futures-io", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", ] @@ -1430,9 +1429,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1485,9 +1484,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1498,15 +1497,15 @@ dependencies = [ "indexmap 2.2.6", "slab", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.11", "tracing", ] [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if 1.0.0", "crunchy", @@ -1520,9 +1519,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" @@ -1634,7 +1633,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1711,7 +1710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -1765,6 +1764,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -1878,9 +1883,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libredox" @@ -1906,9 +1911,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2105,11 +2110,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2131,11 +2135,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -2144,9 +2147,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -2220,7 +2223,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -2249,13 +2252,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.17.1" +version = "0.18.5" dependencies = [ "anyhow", "async-trait", "axum", "axum-server", - "base64 0.22.0", + "base64 0.22.1", "bech32 0.11.0", "bip39", "bitcoin", @@ -2294,7 +2297,7 @@ dependencies = [ "reqwest", "rss", "rust-embed", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-acme", "serde", "serde-hex", @@ -2306,7 +2309,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", - "tokio-util 0.7.10", + "tokio-util 0.7.11", "tower-http", "unindent", "urlencoding", @@ -2340,7 +2343,7 @@ dependencies = [ [[package]] name = "ordinals" -version = "0.0.6" +version = "0.0.8" dependencies = [ "bitcoin", "derive_more", @@ -2414,7 +2417,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -2436,7 +2439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-io", ] @@ -2529,18 +2532,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] [[package]] name = "pulldown-cmark" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" +checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ "bitflags 2.5.0", "getopts", @@ -2551,9 +2554,9 @@ dependencies = [ [[package]] name = "pulldown-cmark-escape" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b" +checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" [[package]] name = "quick-xml" @@ -2567,9 +2570,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2638,9 +2641,9 @@ dependencies = [ [[package]] name = "redb" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1100a056c5dcdd4e5513d5333385223b26ef1bf92f31eb38f407e8c20549256" +checksum = "ed7508e692a49b6b2290b56540384ccae9b1fb4d77065640b165835b56ffe3bb" dependencies = [ "libc", ] @@ -2727,7 +2730,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-util 0.7.10", + "tokio-util 0.7.11", "tower-service", "url", "wasm-bindgen", @@ -2798,7 +2801,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.57", + "syn 2.0.61", "walkdir", ] @@ -2814,9 +2817,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" @@ -2852,9 +2855,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2865,9 +2868,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.8", @@ -2877,14 +2880,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki 0.102.3", "subtle", "zeroize", ] @@ -2913,7 +2916,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.11", "webpki-roots", "x509-parser", ] @@ -2929,9 +2932,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -2945,9 +2948,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -2956,15 +2959,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3023,11 +3026,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -3036,9 +3039,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -3046,15 +3049,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] @@ -3072,20 +3075,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "indexmap 2.2.6", "itoa", @@ -3117,11 +3120,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3135,14 +3138,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling 0.20.8", "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -3215,9 +3218,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3243,9 +3246,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -3288,9 +3291,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.57" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -3317,9 +3320,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.7" +version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -3358,36 +3361,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if 1.0.0", - "fastrand 2.0.2", - "rustix 0.38.32", + "fastrand 2.1.0", + "rustix 0.38.34", "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -3406,9 +3409,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -3451,7 +3454,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.48.0", ] @@ -3464,7 +3467,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", ] [[package]] @@ -3483,7 +3486,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", + "rustls 0.21.12", "tokio", ] @@ -3514,9 +3517,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", @@ -3524,7 +3527,6 @@ dependencies = [ "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -3570,7 +3572,7 @@ dependencies = [ "mime", "pin-project-lite", "tokio", - "tokio-util 0.7.10", + "tokio-util 0.7.11", "tower-layer", "tower-service", ] @@ -3651,9 +3653,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -3785,7 +3787,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -3819,7 +3821,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3864,11 +3866,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3884,7 +3886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3893,7 +3895,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3911,7 +3913,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3931,17 +3933,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "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", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3952,9 +3955,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3964,9 +3967,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3976,9 +3979,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3988,9 +3997,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -4000,9 +4009,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -4012,9 +4021,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -4024,9 +4033,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winreg" diff --git a/Cargo.toml b/Cargo.toml index 1a734153c6..4a9c092334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.17.1" +version = "0.18.5" license = "CC0-1.0" edition = "2021" autotests = false homepage = "https://github.com/ordinals/ord" repository = "https://github.com/ordinals/ord" autobins = false -rust-version = "1.67" +rust-version = "1.76.0" [package.metadata.deb] copyright = "The Ord Maintainers" @@ -27,7 +27,7 @@ bech32 = "0.11.0" bip39 = "2.0.0" bitcoin = { version = "0.30.1", features = ["rand"] } boilerplate = { version = "1.0.0", features = ["axum"] } -brotli = "3.4.0" +brotli = "5.0.0" chrono = { version = "0.4.19", features = ["serde"] } ciborium = "0.2.1" clap = { version = "4.4.2", features = ["derive"] } @@ -49,7 +49,7 @@ mime_guess = "2.0.4" miniscript = "10.0.0" mp4 = "0.14.0" ord-bitcoincore-rpc = "0.17.2" -ordinals = { version = "0.0.6", path = "crates/ordinals" } +ordinals = { version = "0.0.8", path = "crates/ordinals" } redb = "2.0.0" regex = "1.6.0" reqwest = { version = "0.11.23", features = ["blocking", "json"] } diff --git a/README.md b/README.md index 3ce34f8fec..3f4465ef7a 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,19 @@ command line. Building -------- -On Debian and Ubuntu, `ord` requires `libssl-dev` when building from source: +On Linux, `ord` requires `libssl-dev` when building from source. + +On Debian-derived Linux distributions, including Ubuntu: + +``` +sudo apt-get install pkg-config libssl-dev build-essential +``` + +On Red Hat-derived Linux distributions: ``` -sudo apt-get install pkg-config libssl-dev +yum install -y pkgconfig openssl-devel +yum groupinstall "Development Tools" ``` You'll also need Rust: @@ -120,7 +129,8 @@ cargo build --release Once built, the `ord` binary can be found at `./target/release/ord`. -`ord` requires `rustc` version 1.67.0 or later. Run `rustc --version` to ensure you have this version. Run `rustup update` to get the latest stable release. +`ord` requires `rustc` version 1.76.0 or later. Run `rustc --version` to ensure +you have this version. Run `rustup update` to get the latest stable release. ### Docker diff --git a/batch.yaml b/batch.yaml index 707d2ef76b..20c05b3831 100644 --- a/batch.yaml +++ b/batch.yaml @@ -45,6 +45,13 @@ etching: offset: start: 1000 end: 9000 + # future runes protocol changes may be opt-in. this may be for a variety of + # reasons, including that they make light client validation harder, or simply + # because they are too degenerate. + # + # setting `turbo` to `true` opts in to these future protocol changes, + # whatever they may be. + turbo: true # inscriptions to inscribe inscriptions: diff --git a/crates/mockcore/src/api.rs b/crates/mockcore/src/api.rs index 2ebe549ea6..8571ec3ea3 100644 --- a/crates/mockcore/src/api.rs +++ b/crates/mockcore/src/api.rs @@ -25,6 +25,12 @@ pub trait Api { verbose: bool, ) -> Result; + #[rpc(name = "getblockheaderinfo")] + fn get_block_header_info( + &self, + block_hash: BlockHash, + ) -> Result; + #[rpc(name = "getblockstats")] fn get_block_stats(&self, height: usize) -> Result; diff --git a/crates/mockcore/src/lib.rs b/crates/mockcore/src/lib.rs index d6fc17808c..d9b76f0489 100644 --- a/crates/mockcore/src/lib.rs +++ b/crates/mockcore/src/lib.rs @@ -138,12 +138,7 @@ pub struct TransactionTemplate<'a> { 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 { +#[derive(Serialize, Deserialize, Debug)]pub struct JsonOutPoint { txid: Txid, vout: u32, } diff --git a/crates/mockcore/src/server.rs b/crates/mockcore/src/server.rs index 4b28bdf0e0..bd62aab5b4 100644 --- a/crates/mockcore/src/server.rs +++ b/crates/mockcore/src/server.rs @@ -1,16 +1,7 @@ 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, }; @@ -158,6 +149,36 @@ impl Api for Server { } } + fn get_block_header_info( + &self, + block_hash: BlockHash, + ) -> Result { + let state = self.state(); + + let height = match state.hashes.iter().position(|hash| *hash == block_hash) { + Some(height) => height, + None => return Err(Self::not_found()), + }; + + Ok(GetBlockHeaderResult { + height, + hash: block_hash, + confirmations: 0, + version: Version::ONE, + version_hex: None, + merkle_root: TxMerkleNode::all_zeros(), + time: 0, + median_time: None, + nonce: 0, + bits: String::new(), + difficulty: 0.0, + chainwork: Vec::new(), + n_tx: 0, + previous_block_hash: None, + next_block_hash: None, + }) + } + fn get_block_stats(&self, height: usize) -> Result { let Some(block_hash) = self.state().hashes.get(height).cloned() else { return Err(Self::not_found()); @@ -232,28 +253,6 @@ impl Api for Server { vout: u32, _include_mempool: Option, ) -> Result, jsonrpc_core::Error> { -<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/server.rs - Ok( - self - .state() - .utxos - .get(&OutPoint { txid, vout }) - .map(|&value| GetTxOutResult { - bestblock: BlockHash::all_zeros(), - confirmations: 0, - value, - script_pub_key: GetRawTransactionResultVoutScriptPubKey { - asm: String::new(), - hex: Vec::new(), - req_sigs: None, - type_: None, - addresses: Vec::new(), - address: None, - }, - coinbase: false, - }), - ) -======== let state = self.state(); let Some(value) = state.utxos.get(&OutPoint { txid, vout }) else { @@ -284,7 +283,6 @@ impl Api for Server { }, value: *value, })) ->>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs } fn get_wallet_info(&self) -> Result { @@ -298,7 +296,7 @@ impl Api for Server { keypool_size: 0, keypool_size_hd_internal: 0, pay_tx_fee: Amount::from_sat(0), - private_keys_enabled: false, + private_keys_enabled: true, scanning: None, tx_count: 0, unconfirmed_balance: Amount::from_sat(0), @@ -446,7 +444,7 @@ impl Api for Server { if output_value > input_value { return Err(jsonrpc_core::Error { code: jsonrpc_core::ErrorCode::ServerError(-6), - message: "insufficent funds".into(), + message: "insufficient funds".into(), data: None, }); } @@ -509,9 +507,6 @@ 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 { @@ -536,7 +531,6 @@ impl Api for Server { } state.mempool.push(tx.clone()); ->>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/server.rs Ok(tx.txid().to_string()) } @@ -666,19 +660,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); + + let tx_height = state.txid_to_block_height.get(&txid); + + let confirmations = tx_height.map(|tx_height| current_height - tx_height); + + let blockhash = tx_height.map(|tx_height| state.hashes[usize::try_from(*tx_height).unwrap()]); 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), @@ -698,13 +692,8 @@ 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(), @@ -712,7 +701,7 @@ impl Api for Server { }, }) .collect(), - blockhash: None, + blockhash, confirmations, time: None, blocktime: None, @@ -785,11 +774,12 @@ impl Api for Server { } fn list_lock_unspent(&self) -> Result, jsonrpc_core::Error> { + let state = self.state(); Ok( - self - .state() + state .locked .iter() + .filter(|outpoint| state.utxos.contains_key(outpoint)) .map(|outpoint| (*outpoint).into()) .collect(), ) @@ -836,16 +826,7 @@ 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( @@ -910,7 +891,6 @@ impl Api for Server { vout: output.vout, txid: output.txid, }; - assert!(state.utxos.contains_key(&output)); assert!(state.locked.insert(output)); } diff --git a/crates/mockcore/src/state.rs b/crates/mockcore/src/state.rs index c317462d51..ff58957074 100644 --- a/crates/mockcore/src/state.rs +++ b/crates/mockcore/src/state.rs @@ -8,23 +8,6 @@ use { }; #[derive(Debug)] -<<<<<<<< HEAD:crates/test-bitcoincore-rpc/src/state.rs -pub(crate) struct State { - pub(crate) blocks: BTreeMap, - pub(crate) change_addresses: Vec
, - pub(crate) descriptors: Vec, - pub(crate) fail_lock_unspent: bool, - pub(crate) hashes: Vec, - pub(crate) loaded_wallets: BTreeSet, - pub(crate) locked: BTreeSet, - pub(crate) mempool: Vec, - pub(crate) network: Network, - pub(crate) nonce: u32, - pub(crate) transactions: BTreeMap, - pub(crate) utxos: BTreeMap, - pub(crate) version: usize, - pub(crate) wallets: BTreeSet, -======== pub struct State { pub blocks: BTreeMap, pub descriptors: Vec, @@ -42,7 +25,6 @@ pub struct State { pub receive_addresses: Vec
, pub change_addresses: Vec
, pub wallets: BTreeSet, ->>>>>>>> origin/ordzaar-master-0-17-1:crates/mockcore/src/state.rs } impl State { @@ -66,10 +48,7 @@ impl State { 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(), @@ -275,9 +254,11 @@ impl State { ); } - self.mempool.push(tx.clone()); + let txid = tx.txid(); - tx.txid() + self.mempool.push(tx); + + txid } pub(crate) fn mempool(&self) -> &[Transaction] { diff --git a/crates/ordinals/Cargo.toml b/crates/ordinals/Cargo.toml index 200ecd7bd9..fae0b8ccae 100644 --- a/crates/ordinals/Cargo.toml +++ b/crates/ordinals/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "ordinals" -version = "0.0.6" +version = "0.0.8" edition = "2021" description = "Library for interoperating with ordinals and inscriptions" homepage = "https://github.com/ordinals/ord" repository = "https://github.com/ordinals/ord" license = "CC0-1.0" -rust-version = "1.67" +rust-version = "1.74.0" [dependencies] bitcoin = { version = "0.30.1", features = ["rand"] } diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs index d03eda4cd8..c6e3f620ae 100644 --- a/crates/ordinals/src/cenotaph.rs +++ b/crates/ordinals/src/cenotaph.rs @@ -3,32 +3,6 @@ use super::*; #[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Default)] pub struct Cenotaph { pub etching: Option, - pub flaws: u32, + pub flaw: Option, 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/edict.rs b/crates/ordinals/src/edict.rs index 3bc6113884..2d8d35f752 100644 --- a/crates/ordinals/src/edict.rs +++ b/crates/ordinals/src/edict.rs @@ -13,6 +13,8 @@ impl Edict { return None; }; + // note that this allows `output == tx.output.len()`, which means to divide + // amount between all non-OP_RETURN outputs if output > u32::try_from(tx.output.len()).unwrap() { return None; } diff --git a/crates/ordinals/src/etching.rs b/crates/ordinals/src/etching.rs index eab0421848..773486ac6b 100644 --- a/crates/ordinals/src/etching.rs +++ b/crates/ordinals/src/etching.rs @@ -8,6 +8,7 @@ pub struct Etching { pub spacers: Option, pub symbol: Option, pub terms: Option, + pub turbo: bool, } impl Etching { diff --git a/crates/ordinals/src/flaw.rs b/crates/ordinals/src/flaw.rs index 69a2be27f8..69c152bf0b 100644 --- a/crates/ordinals/src/flaw.rs +++ b/crates/ordinals/src/flaw.rs @@ -1,6 +1,7 @@ use super::*; -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum Flaw { EdictOutput, EdictRuneId, @@ -14,25 +15,6 @@ pub enum Flaw { 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 { @@ -49,9 +31,3 @@ impl Display for Flaw { } } } - -impl From for u32 { - fn from(cenotaph: Flaw) -> Self { - cenotaph.flag() - } -} diff --git a/crates/ordinals/src/pile.rs b/crates/ordinals/src/pile.rs index d93cba20ba..4bbba23b08 100644 --- a/crates/ordinals/src/pile.rs +++ b/crates/ordinals/src/pile.rs @@ -9,7 +9,7 @@ pub struct Pile { impl Display for Pile { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let cutoff = 10u128.pow(self.divisibility.into()); + let cutoff = 10u128.checked_pow(self.divisibility.into()).unwrap(); let whole = self.amount / cutoff; let mut fractional = self.amount % cutoff; diff --git a/crates/ordinals/src/rune.rs b/crates/ordinals/src/rune.rs index d3cd3c49d2..90e26865cf 100644 --- a/crates/ordinals/src/rune.rs +++ b/crates/ordinals/src/rune.rs @@ -143,7 +143,7 @@ impl FromStr for Rune { let mut x = 0u128; for (i, c) in s.chars().enumerate() { if i > 0 { - x += 1; + x = x.checked_add(1).ok_or(Error::Range)?; } x = x.checked_mul(26).ok_or(Error::Range)?; match c { @@ -224,7 +224,11 @@ mod tests { fn from_str_error() { assert_eq!( "BCGDENLQRQWDSLRUGSNLBTMFIJAW".parse::().unwrap_err(), - Error::Range + Error::Range, + ); + assert_eq!( + "BCGDENLQRQWDSLRUGSNLBTMFIJAVX".parse::().unwrap_err(), + Error::Range, ); assert_eq!("x".parse::().unwrap_err(), Error::Character('x')); } diff --git a/crates/ordinals/src/rune_id.rs b/crates/ordinals/src/rune_id.rs index 0c82a0047a..c487db4c58 100644 --- a/crates/ordinals/src/rune_id.rs +++ b/crates/ordinals/src/rune_id.rs @@ -98,6 +98,7 @@ mod tests { #[test] fn delta() { let mut expected = [ + RuneId { block: 3, tx: 1 }, RuneId { block: 4, tx: 2 }, RuneId { block: 1, tx: 2 }, RuneId { block: 1, tx: 1 }, @@ -114,6 +115,7 @@ mod tests { RuneId { block: 1, tx: 2 }, RuneId { block: 2, tx: 0 }, RuneId { block: 3, tx: 1 }, + RuneId { block: 3, tx: 1 }, RuneId { block: 4, tx: 2 }, ] ); @@ -125,7 +127,7 @@ mod tests { previous = id; } - assert_eq!(deltas, [(1, 1), (0, 1), (1, 0), (1, 1), (1, 2)]); + assert_eq!(deltas, [(1, 1), (0, 1), (1, 0), (1, 1), (0, 0), (1, 2)]); let mut previous = RuneId::default(); let mut actual = Vec::new(); diff --git a/crates/ordinals/src/runestone.rs b/crates/ordinals/src/runestone.rs index 4744dd875e..7007d8d8cc 100644 --- a/crates/ordinals/src/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -20,29 +20,29 @@ enum Payload { impl Runestone { pub const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; - pub const COMMIT_INTERVAL: u16 = 6; + pub const COMMIT_CONFIRMATIONS: 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(), + flaw: Some(flaw), ..default() })); } None => return None, }; - let Some(integers) = Runestone::integers(&payload) else { + let Ok(integers) = Runestone::integers(&payload) else { return Some(Artifact::Cenotaph(Cenotaph { - flaws: Flaw::Varint.into(), + flaw: Some(Flaw::Varint), ..default() })); }; let Message { - mut flaws, + mut flaw, edicts, mut fields, } = Message::from_integers(transaction, &integers); @@ -83,6 +83,7 @@ impl Runestone { Tag::OffsetEnd.take(&mut fields, |[end_offset]| u64::try_from(end_offset).ok()), ), }), + turbo: Flag::Turbo.take(&mut flags), }); let mint = Tag::Mint.take(&mut fields, |[block, tx]| { @@ -98,20 +99,20 @@ impl Runestone { .map(|etching| etching.supply().is_none()) .unwrap_or_default() { - flaws |= Flaw::SupplyOverflow.flag(); + flaw.get_or_insert(Flaw::SupplyOverflow); } if flags != 0 { - flaws |= Flaw::UnrecognizedFlag.flag(); + flaw.get_or_insert(Flaw::UnrecognizedFlag); } if fields.keys().any(|tag| tag % 2 == 0) { - flaws |= Flaw::UnrecognizedEvenTag.flag(); + flaw.get_or_insert(Flaw::UnrecognizedEvenTag); } - if flaws != 0 { + if let Some(flaw) = flaw { return Some(Artifact::Cenotaph(Cenotaph { - flaws, + flaw: Some(flaw), mint, etching: etching.and_then(|etching| etching.rune), })); @@ -136,6 +137,10 @@ impl Runestone { Flag::Terms.set(&mut flags); } + if etching.turbo { + Flag::Turbo.set(&mut flags); + } + Tag::Flags.encode([flags], &mut payload); Tag::Rune.encode_option(etching.rune.map(|rune| rune.0), &mut payload); @@ -228,7 +233,7 @@ impl Runestone { None } - fn integers(payload: &[u8]) -> Option> { + fn integers(payload: &[u8]) -> Result, varint::Error> { let mut integers = Vec::new(); let mut i = 0; @@ -238,7 +243,7 @@ impl Runestone { i += length; } - Some(integers) + Ok(integers) } } @@ -446,7 +451,7 @@ mod tests { }) .unwrap(), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::Opcode.into(), + flaw: Some(Flaw::Opcode), ..default() }), ); @@ -470,7 +475,7 @@ mod tests { }) .unwrap(), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::Opcode.into(), + flaw: Some(Flaw::Opcode), ..default() }), ); @@ -618,7 +623,7 @@ mod tests { 0 ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedFlag.into(), + flaw: Some(Flaw::UnrecognizedFlag), ..default() }), ); @@ -631,7 +636,7 @@ mod tests { assert_eq!( decipher(integers), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.into(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -761,7 +766,7 @@ mod tests { }) .unwrap(), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::Varint.into(), + flaw: Some(Flaw::Varint), ..default() }), ); @@ -784,7 +789,7 @@ mod tests { 0, ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.into(), + flaw: Some(Flaw::UnrecognizedEvenTag), etching: Some(Rune(4)), ..default() }), @@ -843,7 +848,7 @@ mod tests { assert_eq!( decipher(&[Tag::Cenotaph.into(), 0, Tag::Body.into(), 1, 1, 2, 0]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -862,7 +867,7 @@ mod tests { 0 ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedFlag.flag(), + flaw: Some(Flaw::UnrecognizedFlag), ..default() }), ); @@ -873,7 +878,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 0, 1, 2, 0]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictRuneId.into(), + flaw: Some(Flaw::EdictRuneId), ..default() }), ); @@ -884,7 +889,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 1, 0, 0, 0, u64::MAX.into(), 0, 0, 0]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictRuneId.into(), + flaw: Some(Flaw::EdictRuneId), ..default() }), ); @@ -892,7 +897,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 0, 0, 0, u64::MAX.into(), 0, 0]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictRuneId.into(), + flaw: Some(Flaw::EdictRuneId), ..default() }), ); @@ -903,7 +908,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 2]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictOutput.into(), + flaw: Some(Flaw::EdictOutput), ..default() }), ); @@ -914,7 +919,7 @@ mod tests { assert_eq!( decipher(&[Tag::Flags.into(), 1, Tag::Flags.into()]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::TruncatedField.flag(), + flaw: Some(Flaw::TruncatedField), ..default() }), ); @@ -938,7 +943,7 @@ mod tests { }) } else { Artifact::Cenotaph(Cenotaph { - flaws: Flaw::TrailingIntegers.into(), + flaw: Some(Flaw::TrailingIntegers), ..default() }) } @@ -1074,7 +1079,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etching.mask() | Flag::Terms.mask(), + Flag::Etching.mask() | Flag::Terms.mask() | Flag::Turbo.mask(), Tag::Rune.into(), 4, Tag::Divisibility.into(), @@ -1110,17 +1115,18 @@ mod tests { output: 0, }], etching: Some(Etching { + divisibility: Some(1), + premine: Some(8), rune: Some(Rune(4)), + spacers: Some(5), + symbol: Some('a'), 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), + turbo: true, }), pointer: Some(0), mint: Some(RuneId::new(1, 1).unwrap()), @@ -1133,7 +1139,7 @@ mod tests { assert_eq!( decipher(&[Tag::Rune.into(), 4]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1230,7 +1236,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, u128::MAX, 1, 0, 0,]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictRuneId.flag(), + flaw: Some(Flaw::EdictRuneId), ..default() }), ); @@ -1241,7 +1247,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, 1, u128::MAX, 0, 0,]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictRuneId.flag(), + flaw: Some(Flaw::EdictRuneId), ..default() }), ); @@ -1433,6 +1439,7 @@ mod tests { offset: (Some(u32::MAX.into()), Some(u32::MAX.into())), height: (Some(u32::MAX.into()), Some(u32::MAX.into())), }), + turbo: true, premine: Some(u64::MAX.into()), rune: Some(Rune(u128::MAX)), symbol: Some('\u{10FFFF}'), @@ -1633,7 +1640,7 @@ mod tests { u128::from(u64::MAX) + 1, ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.into(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1704,13 +1711,14 @@ mod tests { amount: Some(14), offset: (Some(15), Some(16)), }), + turbo: true, }), mint: Some(RuneId::new(17, 18).unwrap()), pointer: Some(0), }, &[ Tag::Flags.into(), - Flag::Etching.mask() | Flag::Terms.mask(), + Flag::Etching.mask() | Flag::Terms.mask() | Flag::Turbo.mask(), Tag::Rune.into(), 9, Tag::Divisibility.into(), @@ -1754,12 +1762,13 @@ mod tests { case( Runestone { etching: Some(Etching { - premine: None, divisibility: None, - terms: None, - symbol: None, + premine: None, rune: Some(Rune(3)), spacers: None, + symbol: None, + terms: None, + turbo: false, }), ..default() }, @@ -1769,12 +1778,13 @@ mod tests { case( Runestone { etching: Some(Etching { - premine: None, divisibility: None, - terms: None, - symbol: None, + premine: None, rune: None, spacers: None, + symbol: None, + terms: None, + turbo: false, }), ..default() }, @@ -1820,7 +1830,7 @@ mod tests { assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 1, u128::from(u32::MAX) + 1]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::EdictOutput.flag(), + flaw: Some(Flaw::EdictOutput), ..default() }), ); @@ -1831,7 +1841,7 @@ mod tests { assert_eq!( decipher(&[Tag::Mint.into(), 1]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1842,7 +1852,7 @@ mod tests { assert_eq!( decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1853,7 +1863,7 @@ mod tests { assert_eq!( decipher(&[Tag::OffsetEnd.into(), u128::MAX]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1864,14 +1874,14 @@ mod tests { assert_eq!( decipher(&[Tag::Pointer.into(), 1]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); assert_eq!( decipher(&[Tag::Pointer.into(), u128::MAX]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1935,7 +1945,7 @@ mod tests { assert_eq!( decipher(&[Tag::OffsetEnd.into(), u128::MAX]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::UnrecognizedEvenTag.flag(), + flaw: Some(Flaw::UnrecognizedEvenTag), ..default() }), ); @@ -1976,7 +1986,7 @@ mod tests { u128::MAX ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::SupplyOverflow.into(), + flaw: Some(Flaw::SupplyOverflow), ..default() }), ); @@ -1991,7 +2001,7 @@ mod tests { u128::MAX / 2 + 1 ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::SupplyOverflow.into(), + flaw: Some(Flaw::SupplyOverflow), ..default() }), ); @@ -2008,7 +2018,7 @@ mod tests { u128::MAX ]), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::SupplyOverflow.into(), + flaw: Some(Flaw::SupplyOverflow), ..default() }), ); @@ -2089,9 +2099,88 @@ mod tests { }) .unwrap(), Artifact::Cenotaph(Cenotaph { - flaws: Flaw::InvalidScript.into(), + flaw: Some(Flaw::InvalidScript), ..default() }), ); } + + #[test] + fn all_pushdata_opcodes_are_valid() { + for i in 0..79 { + let mut script_pubkey = Vec::new(); + + script_pubkey.push(opcodes::all::OP_RETURN.to_u8()); + script_pubkey.push(Runestone::MAGIC_NUMBER.to_u8()); + script_pubkey.push(i); + + match i { + 0..=75 => { + for j in 0..i { + script_pubkey.push(if j % 2 == 0 { 1 } else { 0 }); + } + + if i % 2 == 1 { + script_pubkey.push(1); + script_pubkey.push(1); + } + } + 76 => { + script_pubkey.push(0); + } + 77 => { + script_pubkey.push(0); + script_pubkey.push(0); + } + 78 => { + script_pubkey.push(0); + script_pubkey.push(0); + script_pubkey.push(0); + script_pubkey.push(0); + } + _ => unreachable!(), + } + + assert_eq!( + Runestone::decipher(&Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: default(), + output: vec![TxOut { + script_pubkey: script_pubkey.into(), + value: 0, + },], + }) + .unwrap(), + Artifact::Runestone(Runestone::default()), + ); + } + } + + #[test] + fn all_non_pushdata_opcodes_are_invalid() { + for i in 79..=u8::MAX { + assert_eq!( + Runestone::decipher(&Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: default(), + output: vec![TxOut { + script_pubkey: vec![ + opcodes::all::OP_RETURN.to_u8(), + Runestone::MAGIC_NUMBER.to_u8(), + i + ] + .into(), + value: 0, + },], + }) + .unwrap(), + Artifact::Cenotaph(Cenotaph { + flaw: Some(Flaw::Opcode), + ..default() + }), + ); + } + } } diff --git a/crates/ordinals/src/runestone/flag.rs b/crates/ordinals/src/runestone/flag.rs index 06cf463054..66b2d60dc9 100644 --- a/crates/ordinals/src/runestone/flag.rs +++ b/crates/ordinals/src/runestone/flag.rs @@ -1,6 +1,7 @@ pub(super) enum Flag { Etching = 0, Terms = 1, + Turbo = 2, #[allow(unused)] Cenotaph = 127, } diff --git a/crates/ordinals/src/runestone/message.rs b/crates/ordinals/src/runestone/message.rs index b833169fbc..4eaf59e477 100644 --- a/crates/ordinals/src/runestone/message.rs +++ b/crates/ordinals/src/runestone/message.rs @@ -1,7 +1,7 @@ use super::*; pub(super) struct Message { - pub(super) flaws: u32, + pub(super) flaw: Option, pub(super) edicts: Vec, pub(super) fields: HashMap>, } @@ -10,7 +10,7 @@ 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; + let mut flaw = None; for i in (0..payload.len()).step_by(2) { let tag = payload[i]; @@ -19,17 +19,17 @@ impl Message { let mut id = RuneId::default(); for chunk in payload[i + 1..].chunks(4) { if chunk.len() != 4 { - flaws |= Flaw::TrailingIntegers.flag(); + flaw.get_or_insert(Flaw::TrailingIntegers); break; } let Some(next) = id.next(chunk[0], chunk[1]) else { - flaws |= Flaw::EdictRuneId.flag(); + flaw.get_or_insert(Flaw::EdictRuneId); break; }; let Some(edict) = Edict::from_integers(tx, next, chunk[2], chunk[3]) else { - flaws |= Flaw::EdictOutput.flag(); + flaw.get_or_insert(Flaw::EdictOutput); break; }; @@ -40,7 +40,7 @@ impl Message { } let Some(&value) = payload.get(i + 1) else { - flaws |= Flaw::TruncatedField.flag(); + flaw.get_or_insert(Flaw::TruncatedField); break; }; @@ -48,7 +48,7 @@ impl Message { } Self { - flaws, + flaw, edicts, fields, } diff --git a/crates/ordinals/src/varint.rs b/crates/ordinals/src/varint.rs index cef2086a5e..f6a226946a 100644 --- a/crates/ordinals/src/varint.rs +++ b/crates/ordinals/src/varint.rs @@ -8,11 +8,7 @@ pub fn encode_to_vec(mut n: u128, v: &mut Vec) { 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> { +pub fn decode(buffer: &[u8]) -> Result<(u128, usize), Error> { let mut n = 0u128; for (i, &byte) in buffer.iter().enumerate() { @@ -43,7 +39,7 @@ pub fn encode(n: u128) -> Vec { } #[derive(PartialEq, Debug)] -enum Error { +pub enum Error { Overlong, Overflow, Unterminated, @@ -69,7 +65,7 @@ mod tests { fn zero_round_trips_successfully() { let n = 0; let encoded = encode(n); - let (decoded, length) = try_decode(&encoded).unwrap(); + let (decoded, length) = decode(&encoded).unwrap(); assert_eq!(decoded, n); assert_eq!(length, encoded.len()); } @@ -78,7 +74,7 @@ mod tests { fn u128_max_round_trips_successfully() { let n = u128::MAX; let encoded = encode(n); - let (decoded, length) = try_decode(&encoded).unwrap(); + let (decoded, length) = decode(&encoded).unwrap(); assert_eq!(decoded, n); assert_eq!(length, encoded.len()); } @@ -88,7 +84,7 @@ mod tests { for i in 0..128 { let n = 1 << i; let encoded = encode(n); - let (decoded, length) = try_decode(&encoded).unwrap(); + let (decoded, length) = decode(&encoded).unwrap(); assert_eq!(decoded, n); assert_eq!(length, encoded.len()); } @@ -101,7 +97,7 @@ mod tests { for i in 0..129 { n = n << 1 | (i % 2); let encoded = encode(n); - let (decoded, length) = try_decode(&encoded).unwrap(); + let (decoded, length) = decode(&encoded).unwrap(); assert_eq!(decoded, n); assert_eq!(length, encoded.len()); } @@ -118,49 +114,49 @@ mod tests { 128, 0, ]; - assert_eq!(try_decode(&VALID), Ok((0, 19))); - assert_eq!(try_decode(&INVALID), Err(Error::Overlong)); + assert_eq!(decode(&VALID), Ok((0, 19))); + assert_eq!(decode(&INVALID), Err(Error::Overlong)); } #[test] fn varints_may_not_overflow_u128() { assert_eq!( - try_decode(&[ + 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(&[ + 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(&[ + 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(&[ + 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(&[ + 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(&[ + decode(&[ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 2, ]), @@ -170,6 +166,6 @@ mod tests { #[test] fn varints_must_be_terminated() { - assert_eq!(try_decode(&[128]), Err(Error::Unterminated)); + assert_eq!(decode(&[128]), Err(Error::Unterminated)); } } diff --git a/deploy/bitcoin.conf b/deploy/bitcoin.conf index b307bf5fe6..c68b57f2f2 100644 --- a/deploy/bitcoin.conf +++ b/deploy/bitcoin.conf @@ -1,5 +1,5 @@ datacarriersize=1000000 datadir=/var/lib/bitcoind maxmempool=1024 -mempoolfulrbf=1 +mempoolfullrbf=1 txindex=1 diff --git a/docs/po/zh.po b/docs/po/zh.po index ff5388eaf5..05e20a5a21 100644 --- a/docs/po/zh.po +++ b/docs/po/zh.po @@ -35,7 +35,8 @@ msgstr "委托" msgid "Metadata" msgstr "元数据" -#: src/SUMMARY.md:10 src/inscriptions/pointer.md:1 +#: src/SUMMARY.md:10 src/inscriptions/pointer.md:1 src/runes.md:132 +#: src/runes/specification.md:312 msgid "Pointer" msgstr "指针" @@ -51,85 +52,94 @@ msgstr "递归" msgid "Rendering" msgstr "渲染" -#: src/SUMMARY.md:14 +#: src/SUMMARY.md:14 src/runes.md:1 +msgid "Runes" +msgstr "符文|福文🧧" + +#: src/SUMMARY.md:15 src/inscriptions/delegate.md:8 +#: src/inscriptions/provenance.md:14 +msgid "Specification" +msgstr "规范" + +#: src/SUMMARY.md:16 msgid "FAQ" msgstr "常见问题" -#: src/SUMMARY.md:15 +#: src/SUMMARY.md:17 msgid "Contributing" msgstr "贡献" -#: src/SUMMARY.md:16 src/donate.md:1 +#: src/SUMMARY.md:18 src/donate.md:1 msgid "Donate" msgstr "捐赠" -#: src/SUMMARY.md:17 +#: src/SUMMARY.md:19 msgid "Guides" msgstr "指引" -#: src/SUMMARY.md:18 +#: src/SUMMARY.md:20 msgid "Explorer" msgstr "浏览器" -#: src/SUMMARY.md:19 src/guides/wallet.md:1 +#: src/SUMMARY.md:21 src/guides/wallet.md:1 msgid "Wallet" -msgstr "麻雀钱包" +msgstr "钱包" -#: src/SUMMARY.md:20 src/guides/batch-inscribing.md:1 +#: src/SUMMARY.md:22 src/guides/batch-inscribing.md:1 msgid "Batch Inscribing" msgstr "批量铸造" -#: src/SUMMARY.md:21 src/guides/collecting.md:1 +#: src/SUMMARY.md:23 src/guides/collecting.md:1 msgid "Collecting" msgstr "收藏" -#: src/SUMMARY.md:22 src/guides/sat-hunting.md:239 +#: src/SUMMARY.md:24 src/guides/sat-hunting.md:239 msgid "Sparrow Wallet" msgstr "麻雀钱包" -#: src/SUMMARY.md:23 src/guides/moderation.md:1 +#: src/SUMMARY.md:25 src/guides/moderation.md:1 msgid "Moderation" msgstr "调节" -#: src/SUMMARY.md:24 src/guides/reindexing.md:1 +#: src/SUMMARY.md:26 src/guides/reindexing.md:1 msgid "Reindexing" msgstr "重新索引" -#: src/SUMMARY.md:25 src/guides/sat-hunting.md:1 +#: src/SUMMARY.md:27 src/guides/sat-hunting.md:1 msgid "Sat Hunting" msgstr "猎聪" -#: src/SUMMARY.md:26 src/guides/settings.md:1 +#: src/SUMMARY.md:28 src/guides/settings.md:1 msgid "Settings" -msgstr "调试" +msgstr "设置" -#: src/SUMMARY.md:27 src/guides/teleburning.md:1 +#: src/SUMMARY.md:29 src/guides/teleburning.md:1 msgid "Teleburning" msgstr "燃烧传送" -#: src/SUMMARY.md:28 src/guides/testing.md:1 +#: src/SUMMARY.md:30 src/guides/testing.md:1 msgid "Testing" msgstr "调试" -#: src/SUMMARY.md:29 +#: src/SUMMARY.md:31 msgid "Bounties" msgstr "赏金" -#: src/SUMMARY.md:30 +#: src/SUMMARY.md:32 msgid "Bounty 0: 100,000 sats Claimed!" -msgstr "任务 0: 100,000 sats 完成!" +msgstr "赏金 0: 100,000 sats 完成!" -#: src/SUMMARY.md:31 +#: src/SUMMARY.md:33 msgid "Bounty 1: 200,000 sats Claimed!" -msgstr "任务 1: 200,000 sats 完成!" +msgstr "赏金 1: 200,000 sats 完成!" -#: src/SUMMARY.md:32 +#: src/SUMMARY.md:34 msgid "Bounty 2: 300,000 sats Claimed!" -msgstr "任务 2: 300,000 sats 完成!" +msgstr "赏金 2: 300,000 sats 完成!" -#: src/SUMMARY.md:33 +#: src/SUMMARY.md:35 msgid "Bounty 3: 400,000 sats" -msgstr "任务 3: 400,000 sats" +msgstr "赏金 3: 400,000 sats" #: src/introduction.md:4 msgid "" @@ -199,8 +209,8 @@ msgid "" "[inscriptions](guides/wallet.md), a curious species of digital artifact " "enabled by ordinal theory." msgstr "" -"当您准备好亲自动手时,一个好的起点是[铭文](inscriptions.md)这是一种由" -"序数理论支持的独特的数字文物。" +"当您准备好亲自动手时,一个好的起点是[铭文](inscriptions.md)这是一种由序数理论" +"支持的独特的数字文物。" #: src/introduction.md:35 msgid "Links" @@ -243,7 +253,8 @@ 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 "" @@ -269,9 +280,9 @@ msgid "" msgstr "" "序数是一种比特币的编号方案,允许跟踪和转移单个聪。这些数字被称作[序号]" "(https://ordinals.com)。比特币是按照它们被挖掘的顺序编号的,并从交易输入转移" -"到交易输出(遵循先进先出原则)。编号方案和传输方案都依赖于_顺序_,编号方案依" -"赖于比特币被挖掘的_顺序_,而传输方案依赖于交易输入和输出的_顺序_。因此得名,_" -"序数(Ordinals_。" +"到交易输出(遵循先进先出原则)。编号方案和传输方案都依赖于 _顺序_,编号方案依" +"赖于比特币被挖掘的 _顺序_,而传输方案依赖于交易输入和输出的 _顺序_。因此得名," +"_序数(Ordinals)_。" #: src/overview.md:13 msgid "" @@ -432,7 +443,7 @@ msgstr "`罕见`: 每一个难度调整周期的第一个聪" #: src/overview.md:82 msgid "`epic`: The first sat of each halving epoch" -msgstr "`史诗`: 每个减半周期的第一个聪h" +msgstr "`史诗`: 每个减半周期的第一个聪" #: src/overview.md:83 msgid "`legendary`: The first sat of each cycle" @@ -707,7 +718,7 @@ msgid "" "short and get longer, but then all the good, short names would be trapped in " "the unspendable genesis block." msgstr "" -"每个聪都有一个名字,由字母 _A_ 到 _Z_构成 随着聪被开采的时间越长,名字越短。 " +"每个聪都有一个名字,由字母 _A_ 到 _Z_ 构成 随着聪被开采的时间越长,名字越短。" "如果他们从短开始,然后变得更长,那么所有好的、短的名字都会被困在无法使用的创" "世块中。 " @@ -939,7 +950,7 @@ msgid "" "very nature." msgstr "" "数字文物的定义旨在从其特定的本质上反映NFT _应该_ 是什么, 有时是什么, 以及铭文" -"_始终_ 是什么 " +" _始终_ 是什么 " #: src/inscriptions.md:4 msgid "" @@ -1087,7 +1098,7 @@ msgstr "" #: src/inscriptions.md:79 msgid "Currently, there are six defined fields:" -msgstr "" +msgstr "现在有六个定义的字段" #: src/inscriptions.md:81 msgid "" @@ -1209,10 +1220,13 @@ msgid "Indices" msgstr "指数" #: src/inscriptions.md:121 src/inscriptions.md:124 +#: src/runes/specification.md:193 src/runes/specification.md:194 +#: src/runes/specification.md:332 src/runes/specification.md:414 msgid "0" msgstr "" #: src/inscriptions.md:121 src/inscriptions.md:123 +#: src/runes/specification.md:194 src/runes/specification.md:334 msgid "2" msgstr "" @@ -1221,6 +1235,12 @@ msgid "i0, i1" msgstr "" #: src/inscriptions.md:122 src/inscriptions.md:125 +#: src/runes/specification.md:174 src/runes/specification.md:175 +#: src/runes/specification.md:176 src/runes/specification.md:183 +#: src/runes/specification.md:185 src/runes/specification.md:186 +#: src/runes/specification.md:192 src/runes/specification.md:194 +#: src/runes/specification.md:195 src/runes/specification.md:333 +#: src/runes/specification.md:415 msgid "1" msgstr "" @@ -1229,6 +1249,8 @@ msgid "i2" msgstr "" #: src/inscriptions.md:123 src/inscriptions.md:124 +#: src/runes/specification.md:177 src/runes/specification.md:184 +#: src/runes/specification.md:193 src/runes/specification.md:335 msgid "3" msgstr "" @@ -1236,7 +1258,8 @@ msgstr "" msgid "i3, i4, i5" msgstr "" -#: src/inscriptions.md:125 +#: src/inscriptions.md:125 src/runes/specification.md:175 +#: src/runes/specification.md:186 src/runes/specification.md:195 msgid "4" msgstr "" @@ -1254,7 +1277,8 @@ msgid "" "order reveal transactions appear in blocks, and the order that reveal " "envelopes appear in those transactions." msgstr "" -"铭文被分配的铭文编号从零开始,首先按照揭示交易在区块中出现的顺序,以及揭示信封在这些交易中出现的顺序。" +"铭文被分配的铭文编号从零开始,首先按照揭示交易在区块中出现的顺序,以及揭示信" +"封在这些交易中出现的顺序。" #: src/inscriptions.md:134 msgid "" @@ -1263,8 +1287,9 @@ msgid "" "immediately spent to fees are numbered as if they appear last in the block " "in which they are revealed." msgstr "" -"由于在`ord`中的一个过往的错误,如果不改变大量的铭文编号就无法修复 " -"因此,那些被揭示出来然后立即用于支付费用的铭文,其编号就好像它们是在被揭示出来的区块中最后出现的一样。" +"由于在`ord`中的一个过往的错误,如果不改变大量的铭文编号就无法修复 因此,那些" +"被揭示出来然后立即用于支付费用的铭文,其编号就好像它们是在被揭示出来的区块中" +"最后出现的一样。" #: src/inscriptions.md:139 msgid "" @@ -1272,8 +1297,8 @@ msgid "" "counting down. Cursed inscriptions on and after the jubilee at block 824544 " "are vindicated, and are assigned positive inscription numbers." msgstr "" -"被诅咒的铭文从负一开始编号,依次递减。在区块824544及之后的朱比利(Jubilee)事件中," -"被诅咒的铭文得到了宽恕,并被分配了正数的铭文编号。" +"被诅咒的铭文从负一开始编号,依次递减。在区块824544及之后的朱比利(Jubilee)事" +"件中,被诅咒的铭文得到了宽恕,并被分配了正数的铭文编号。" #: src/inscriptions.md:143 msgid "Sandboxing" @@ -1306,22 +1331,24 @@ msgid "" "if the inscription is present in the wallet. This will only append an " "inscription to a sat, not change the initial inscription." msgstr "" +"如果钱包中存在铭文,之前铭刻的sats可以使用`--reinscribe`命令重新铭刻。" +"这只会在一个sat上附加一个铭文,而不会改变初始铭文。" #: src/inscriptions.md:160 msgid "" "Reinscribe with satpoint: `ord wallet inscribe --fee-rate --" "reinscribe --file --satpoint `" msgstr "" -"如果铭文存在于钱包中,之前铭刻的sats可以使用--reinscribe命令进行重新铭刻。" -"这将只会在一个sat上追加一个铭文,而不会改变最初的铭文。" +"如果铭文存在于钱包中,之前铭刻的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 `" +"在一个聪上再刻录铭文 (需要聪索引): `ord --index-sats wallet inscribe --fee-" +"rate --reinscribe --file --sat `" #: src/inscriptions/delegate.md:4 msgid "" @@ -1330,12 +1357,8 @@ msgid "" "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:" @@ -1347,8 +1370,8 @@ msgid "" "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错误" +"创建一个铭文D。请注意,在创建铭文I时,铭文D不必已经存在。它可以稍后被铭刻。在" +"铭文D被铭刻之前,对铭文I内容的请求将返回404错误" #: src/inscriptions/delegate.md:15 msgid "" @@ -1404,7 +1427,8 @@ 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) " +"代理字段的值使用与父字段相同的编码方式。有关铭文ID编码的更多示例,请参见" +"[provenance](provenance.md) " #: src/inscriptions/metadata.md:4 msgid "" @@ -1809,8 +1833,8 @@ msgid "" "them, recursive endpoints have backwards-compatibility guarantees not shared " "by `ord server`'s other endpoints. In particular:" msgstr "" -"由于对递归端点的更改可能会破坏依赖它们的铭文,递归端点具有向后兼容性保证," -"这是`ord server`的其他端点所不具备的。具体来说:" +"由于对递归端点的更改可能会破坏依赖它们的铭文,递归端点具有向后兼容性保证,这" +"是`ord server`的其他端点所不具备的。具体来说:" #: src/inscriptions/recursion.md:12 msgid "Recursive endpoints will not be removed" @@ -1827,8 +1851,7 @@ msgid "" "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 "" -"递归端点返回的对象字段将不会被重命名或更改类型。" +msgstr "递归端点返回的对象字段将不会被重命名或更改类型。" #: src/inscriptions/recursion.md:19 msgid "Recursion has a number of interesting use-cases:" @@ -1869,9 +1892,7 @@ msgstr "递归端点是" msgid "" "`/content/`: the content of the inscription with " "``" -msgstr "" -"`/content/`: 铭文的内容 " -"``" +msgstr "`/content/`: 铭文的内容 ``" #: src/inscriptions/recursion.md:36 msgid "`/r/blockhash/`: block hash at given block height." @@ -1889,8 +1910,7 @@ msgstr "`/blockheight`:最新区块高度。" msgid "" "`/r/blockinfo/`: block info. `` may be a block height or block " "hash." -msgstr "" -"`/r/blockinfo/`: 区块信息. `` 可能是区块高度或者区块哈希" +msgstr "`/r/blockinfo/`: 区块信息. `` 可能是区块高度或者区块哈希" #: src/inscriptions/recursion.md:40 msgid "`/r/blocktime`: UNIX time stamp of latest block." @@ -1905,310 +1925,1678 @@ msgid "" "`/r/children//`: the set of 100 child inscription ids " "on ``." msgstr "" -"`/r/children//`: 100个子铭文ID的合集 " -"on ``." +"`/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 +#: 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:57 +msgid "`/blockheight`: latest block height." +msgstr "`/blockheight`:最新区块高度。" + +#: src/inscriptions/recursion.md:58 +msgid "`/blockhash`: latest block hash." +msgstr "`/blockhash`:最新的块哈希。" + +#: src/inscriptions/recursion.md:59 +msgid "`/blockhash/`: block hash at given block height." +msgstr "`/blockhash/`:给定块高度的块哈希。" + +#: src/inscriptions/recursion.md:60 +msgid "`/blocktime`: UNIX time stamp of latest block." +msgstr "`/blocktime`:最新块的 UNIX 时间戳。" + +#: src/inscriptions/recursion.md:65 +msgid "`/r/blockhash/0`:" +msgstr "" + +#: src/inscriptions/recursion.md:67 +msgid "" +"```json\n" +"\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"\n" +"```" +msgstr "" + +#: src/inscriptions/recursion.md:71 +msgid "`/r/blockheight`:" +msgstr "" + +#: src/inscriptions/recursion.md:73 +msgid "" +"```json\n" +"777000\n" +"```" +msgstr "" + +#: src/inscriptions/recursion.md:77 +msgid "`/r/blockinfo/0`:" +msgstr "" + +#: src/inscriptions/recursion.md:79 +msgid "" +"```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 "" + +#: src/inscriptions/recursion.md:111 +msgid "`/r/blocktime`:" +msgstr "" + +#: src/inscriptions/recursion.md:113 +msgid "" +"```json\n" +"1700770905\n" +"```" +msgstr "" + +#: src/inscriptions/recursion.md:117 src/inscriptions/recursion.md:178 +msgid "" +"`/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/recursion.md:133 +msgid "" +"`r/" +"inscription/3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0`" +msgstr "" + +#: src/inscriptions/recursion.md:135 +msgid "" +"```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/recursion.md:152 +msgid "" +"`/r/" +"metadata/35b66389b44535861c44b2b18ed602997ee11db9a30d384ae89630c9fc6f011fi3`:" +msgstr "" + +#: src/inscriptions/recursion.md:154 +msgid "" +"```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/runes.md:4 +msgid "" +"Runes allow Bitcoin transactions to etch, mint, and transfer Bitcoin-native " +"digital commodities." +msgstr "" +"符文,又称福文🧧,允许比特币交易来刻画、铸造和转移比特币原生的数字商品。 " + +#: src/runes.md:7 +msgid "" +"Whereas every inscription is unique, every unit of a rune is the same. They " +"are interchangeable tokens, fit for a variety of purposes." +msgstr "" +"虽然每个铭文都是独一无二的,但每个符文的每个单位都是相同的。它们是可互换的代币,适用于多种用途。" + + +#: src/runes.md:10 src/runes/specification.md:20 +msgid "Runestones" +msgstr "符石" + +#: src/runes.md:13 +msgid "" +"Rune protocol messages, called runestones, are stored in Bitcoin transaction " +"outputs." +msgstr "" +"称为符石的符文协议消息,存储在比特币交易输出中。" + +#: src/runes.md:16 +msgid "" +"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." +msgstr "" +"符石输出的脚本公钥以一个OP_RETURN开始,接着是OP_13,然后是零个或多个数据推送。" +"这些数据推送被连接起来并解码成一系列128位整数,最终解析成一个符石。" + +#: src/runes.md:21 +msgid "A transaction may have at most one runestone." +msgstr " 一笔交易最多可以有一个符石。" + +#: src/runes.md:23 +msgid "" +"A runestone may etch a new rune, mint an existing rune, and transfer runes " +"from a transaction's inputs to its outputs." +msgstr "" +"符石可以刻画一个新的符文,铸造一个现有的符文,并将符文从交易的输入转移到输出。" + +#: src/runes.md:26 +msgid "A transaction output may hold balances of any number of runes." +msgstr "一个交易输出可以持有任意数量的符文余额。" + +#: src/runes.md:28 +msgid "" +"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 in " +"the 20th transaction of the 500th block is `500:20`." +msgstr "" +"符文通过ID来识别,ID由刻画符文的区块和该区块内刻画交易的索引组成,以文本形式表示为`BLOCK:TX`。" +"例如,在第500个区块的第20笔交易中铸造的符文的ID是`500:20`。" + +#: src/runes.md:33 +msgid "Etching" +msgstr "刻画" + +#: src/runes.md:36 +msgid "" +"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." +msgstr "" +"符文通过刻画而产生。刻画创建一个符文并设置其属性。一旦设置,这些属性即使对其刻画者来说也是不可变的。" + +#: src/runes.md:39 src/runes/specification.md:412 +msgid "Name" +msgstr "名字" + +#: src/runes.md:41 +msgid "" +"Names consist of the letters A through Z and are between one and twenty-" +"eight characters long. For example `UNCOMMONGOODS` is a rune name." +msgstr "" +"名称由A到Z的字母组成,长度在一到二十八个字符之间。例如`UNCOMMONGOODS`是一个符文名称。" + +#: src/runes.md:44 +msgid "" +"Names may contain spacers, represented as bullets, to aid readability. " +"`UNCOMMONGOODS` might be etched as `UNCOMMON•GOODS`." +msgstr "" +"名称可以包含空格符,表示为点符号,以帮助提高可读性。" +"`UNCOMMONGOODS` 可能被刻画为`UNCOMMON•GOODS`。" + +#: src/runes.md:47 +msgid "" +"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." +msgstr "" +"名称的唯一性不依赖于空格符。因此,即使空格符不同,也不能用与现有符文相同的字母序列来刻画一个符文。" + +#: src/runes.md:51 src/runes/specification.md:322 +#: src/runes/specification.md:330 +msgid "Divisibility" +msgstr "可分性" + +#: src/runes.md:53 +msgid "" +"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." +msgstr "" +"符文的可分性是指它可以被细分到多少个原子单位。可分性以符文数量中允许的小数点后数字位数来表示。" +"可分性为0的符文不能被分割。可分性为1的符文可以被分割成十个子单位,可分性为2的符文可以被分割成一百个," +"依此类推。" + +#: src/runes.md:59 src/runes/specification.md:357 +msgid "Symbol" +msgstr "符号" + +#: src/runes.md:61 +msgid "" +"A rune's currency symbol is a single Unicode code point, for example `$`, " +"`⧉`, or `🧿`, displayed after quantities of that rune." +msgstr "" +"符文的货币符号是一个单一的Unicode代码点,例如`$`、`⧉`或`🧿`,显示在该符文数量之后。 " + +#: src/runes.md:64 +msgid "" +"101 atomic units of a rune with divisibility 2 and symbol `🧿` would be " +"rendered as `1.01 🧿`." +msgstr "" +"具有可分性2和符号`🧿`的101个原子单位的符文将被渲染为`1.01 🧿`。" + +#: src/runes.md:67 +msgid "" +"If a rune does not have a symbol, the generic currency sign `¤`, also called " +"a scarab, should be used." +msgstr "" +" 如果符文没有符号,应使用通用货币符号`¤`,也称为圣甲虫。" + +#: src/runes.md:70 src/runes/specification.md:282 +msgid "Premine" +msgstr "预挖" + +#: src/runes.md:72 +msgid "" +"The etcher of a rune may optionally allocate to themselves units of the rune " +"being etched. This allocation is called a premine." +msgstr "" +" 刻画符文的人可以选择性地为自己分配被刻画的符文单位。这种分配称为预挖。" + +#: src/runes.md:75 +msgid "Terms" +msgstr "条款" + +#: src/runes.md:77 +msgid "" +"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." +msgstr "" +"符文可以有一个开放的铸造,允许任何人为自己创建和分配符文单位。开放铸造受到刻画时设置的条款的约束" + +#: src/runes.md:81 +msgid "" +"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." +msgstr "" +"只要铸造的所有条款都得到满足,铸造就是开放的,当其中任何一个不满足时,铸造就关闭了。" +"例如,铸造可能被限制在一个开始高度、一个结束高度和一个上限之间," +"并且在开始高度和结束高度之间或直到达到上限时开放。" + +#: src/runes.md:86 src/runes/specification.md:286 +msgid "Cap" +msgstr "上限" + +#: src/runes.md:88 +msgid "" +"The number of times a rune may be minted is its cap. A mint is closed once " +"the cap is reached." +msgstr "" +" 符文可以被铸造的次数是其上限。一旦达到上限,铸造就关闭了。" + +#: src/runes.md:91 src/runes/specification.md:290 +msgid "Amount" +msgstr "数量" + +#: src/runes.md:93 +msgid "Each mint transaction creates a fixed amount of new units of a rune." +msgstr " 每笔铸造交易创建一个固定数量的新符文单位。" + +#: src/runes.md:95 +msgid "Start Height" +msgstr "开始高度" + +#: src/runes.md:97 +msgid "A mint is open starting in the block with the given start height." +msgstr "铸造从给定开始高度的区块开始开放。" + +#: src/runes.md:99 +msgid "End Height" +msgstr "结束高度" + +#: src/runes.md:101 +msgid "" +"A rune may not be minted in or after the block with the given end height." +msgstr "符文不能在给定结束高度的区块之后被铸造。" + +#: src/runes.md:103 +msgid "Start Offset" +msgstr "起始偏移" + +#: src/runes.md:105 +msgid "" +"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." +msgstr "" +"铸造从其高度等于开始偏移加上刻画符文的区块的高度的区块开始开放。" + +#: src/runes.md:108 +msgid "End Offset" +msgstr "结束偏移" + +#: src/runes.md:110 +msgid "" +"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." +msgstr "" +" 符文不能在其高度等于结束偏移加上刻画符文的区块的高度的区块之后被铸造。" + +#: src/runes.md:113 src/runes/specification.md:470 +msgid "Minting" +msgstr "铸造" + +#: src/runes.md:116 +msgid "" +"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." +msgstr "" +"当符文的铸造是开放的时,任何人都可以创建一个铸造交易,根据铸造的条款创建一个固定数量的新符文单位。" + +#: src/runes.md:119 src/runes/specification.md:482 +msgid "Transferring" +msgstr "转移" + +#: src/runes.md:122 +msgid "" +"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." +msgstr "" +"当交易输入包含符文,或者通过预挖或铸造创建了新的符文时,这些符文被转移到该交易的输出。" +"交易的符石可能会改变输入符文转移到输出的方式。" + +#: src/runes.md:126 +msgid "Edicts" +msgstr "法令" + +#: src/runes.md:128 +msgid "" +"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." +msgstr "" +"符石可以包含任意数量的法令。法令由一个符文ID、一个数量和一个输出编号组成。" +"法令按顺序处理,将未分配的符文分配给输出。" + +#: src/runes.md:134 +msgid "" +"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." +msgstr "" +"在所有法令处理完毕后,剩余的未分配符文被转移到交易的第一个非OP_RETURN输出。" +"符石可以选择性地包含一个指针,指定一个替代的默认输出。" + +#: src/runes.md:138 +msgid "Burning" +msgstr "燃烧" + +#: src/runes.md:140 +msgid "" +"Runes may be burned by transferring them to an `OP_RETURN` output with an " +"edict or pointer." +msgstr "" +" 符文可以通过将它们转移到一个包含法令或指针的`OP_RETURN`输出来燃烧。" + +#: src/runes.md:143 src/runes/specification.md:370 +msgid "Cenotaphs" +msgstr "墓碑" + +#: src/runes.md:146 +msgid "" +"Runestones may be malformed for a number of reasons, including non-pushdata " +"opcodes in the runestone `OP_RETURN`, invalid varints, or unrecognized " +"runestone fields." +msgstr "" +"由于多种原因,符石可能会形成错误,包括符石`OP_RETURN`中的非推送数据操作码、" +"无效的变长整数或无法识别的符石字段。" + +#: src/runes.md:150 +msgid "" +"Malformed runestones are termed [cenotaphs](https://en.wikipedia.org/wiki/" +"Cenotaph)." +msgstr "" +" 形成错误的符石被称为[墓碑](https://en.wikipedia.org/wiki/Cenotaph)." + +#: src/runes.md:153 +msgid "" +"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." +msgstr "" +"输入到包含墓碑的交易的符文被燃烧。在包含墓碑的交易中刻画的符文被设置为不可铸造。" +"在包含墓碑的交易中的铸造计入铸造上限,但铸造的符文被燃烧。" + +#: src/runes.md:157 +msgid "" +"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." +msgstr "" +"墓碑是一种升级机制,允许符石被赋予新的语义,改变符文的创建和转移方式," +"同时不会误导未升级的客户端关于这些符文的位置,因为未升级的客户端会看到这些符文已经被燃烧。" + +#: src/runes/specification.md:1 +msgid "Runes Does Not Have a Specification" +msgstr "符文没有规范" + +#: src/runes/specification.md:4 +msgid "" +"The Runes reference implementation, `ord`, is the normative specification of " +"the Runes protocol." +msgstr "" +" 符文的参考实现,即`ord`,是符文协议的规范性规格说明。" + +#: src/runes/specification.md:7 +msgid "" +"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." +msgstr "" +"您在这里或其他地方阅读的内容,除了`ord`的代码之外,都不是规格说明。" +"这篇关于符文协议的描述是作为`ord`行为的指南提供的," +"而`ord`的代码本身应始终被查询以确认任何描述的正确性。" + +#: src/runes/specification.md:12 +msgid "" +"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." +msgstr "" +"如果由于`ord`中的一个错误,本文档与`ord`的实际行为出现偏差," +"并且改变`ord`的行为实际上是不切实际的,那么本文档将被修订以符合`ord`的实际行为。" + +#: src/runes/specification.md:16 +msgid "" +"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." +msgstr "" +"使用替代实现的用户需自担风险,强烈建议希望整合符文的服务使用`ord`本身来进行符文交易," +"并确定符文、铸币和余额的状态" + +#: src/runes/specification.md:23 +msgid "Rune protocol messages are termed \"runestones\"." +msgstr " 符文协议消息被称为 \"符石 \"。 " + +#: src/runes/specification.md:25 +msgid "" +"The Runes protocol activates on block 840,000. Runestones in earlier blocks " +"are ignored." +msgstr " 符文协议在区块840,000激活。早期区块中的符石将被忽略。" + +#: src/runes/specification.md:28 +msgid "Abstractly, runestones contain the following fields:" +msgstr " 抽象地,符石包含以下字段:1" + +#: src/runes/specification.md:30 src/runes/specification.md:209 +msgid "" +"```rust\n" +"struct Runestone {\n" +" edicts: Vec,\n" +" etching: Option,\n" +" mint: Option,\n" +" pointer: Option,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:39 +msgid "Runes are created by etchings:" +msgstr "符文是通过蚀刻创建的:" + +#: src/runes/specification.md:41 src/runes/specification.md:396 +msgid "" +"```rust\n" +"struct Etching {\n" +" divisibility: Option,\n" +" premine: Option,\n" +" rune: Option,\n" +" spacers: Option,\n" +" symbol: Option,\n" +" terms: Option,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:52 +msgid "Which may contain mint terms:" +msgstr "其中可能包含铸造术语:" + +#: src/runes/specification.md:54 +msgid "" +"```rust\n" +"struct Terms {\n" +" amount: Option,\n" +" cap: Option,\n" +" height: (Option, Option),\n" +" offset: (Option, Option),\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:63 src/runes/specification.md:484 +msgid "Runes are transferred by edict:" +msgstr "符文通过法令转移:" + +#: src/runes/specification.md:65 src/runes/specification.md:151 +#: src/runes/specification.md:486 +msgid "" +"```rust\n" +"struct Edict {\n" +" id: RuneId,\n" +" amount: u128,\n" +" output: u32,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:73 +msgid "" +"Rune IDs are encoded as the block height and transaction index of the " +"transaction in which the rune was etched:" +msgstr "" +"符文 ID 被编码为蚀刻符文的交易的区块高度和交易索引:" + +#: src/runes/specification.md:76 +msgid "" +"```rust\n" +"struct RuneId {\n" +" block: u64,\n" +" tx: u32,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:83 +msgid "Rune IDs are represented in text as `BLOCK:TX`." +msgstr "符文 ID 在文本中表示为`BLOCK:TX`。" + +#: src/runes/specification.md:85 +msgid "Rune names are encoded as modified base-26 integers:" +msgstr "符文名称被编码为修改后的 26 进制整数:" + +#: src/runes/specification.md:87 +msgid "" +"```rust\n" +"struct Rune(u128);\n" +"```" +msgstr "" + +#: src/runes/specification.md:91 +msgid "Deciphering" +msgstr "破译" + +#: src/runes/specification.md:93 +msgid "Runestones are deciphered from transactions with the following steps:" +msgstr "解读符石是通过以下步骤从交易中解码得到的:" + +#: src/runes/specification.md:95 +msgid "" +"Find the first transaction output whose script pubkey begins with `OP_RETURN " +"OP_13`." +msgstr "" +"查找第一个其脚本公钥以 `OP_RETURN` `OP_13` 开头的交易输出。" + +#: src/runes/specification.md:98 +msgid "Concatenate all following data pushes into a payload buffer." +msgstr "将所有后续数据推送连接到一个有效载荷缓冲区中。" + +#: src/runes/specification.md:100 +msgid "" +"Decode a sequence 128-bit [LEB128](https://en.wikipedia.org/wiki/LEB128) " +"integers from the payload buffer." +msgstr "" +"从有效载荷缓冲区解码一系列 128 位的 [LEB128](https://en.wikipedia.org/wiki/LEB128) 整数。" + +#: src/runes/specification.md:103 +msgid "Parse the sequence of integers into an untyped message." +msgstr "将整数序列解析为未类型化消息。" + +#: src/runes/specification.md:105 +msgid "Parse the untyped message into a runestone." +msgstr "将未类型化消息解析为符石。" + +#: src/runes/specification.md:107 +msgid "" +"Deciphering may produce a malformed runestone, termed a [cenotaph](https://" +"en.wikipedia.org/wiki/Cenotaph)." +msgstr "" +" 解读可能会产生一个格式错误的符石,称为[纪念碑](https://en.wikipedia.org/wiki/Cenotaph)。" + +#: src/runes/specification.md:110 +msgid "Locating the Runestone Output" +msgstr "定位符石输出" + +#: src/runes/specification.md:112 +msgid "" +"Outputs are searched for the first script pubkey that beings with `OP_RETURN " +"OP_13`. If deciphering fails, later matching outputs are not considered." +msgstr "" +"搜索第一个脚本公钥以 `OP_RETURN` `OP_13` 开头的输出。如果解读失败,不会考虑后续匹配的输出。" + +#: src/runes/specification.md:115 +msgid "Assembling the Payload Buffer" +msgstr "组装有效载荷缓冲区" + +#: src/runes/specification.md:117 +msgid "" +"The payload buffer is assembled by concatenating data pushes, after `OP_13`, in the matching script pubkey." +msgstr "" +"有效载荷缓冲区是通过将匹配的脚本 pubkey 中 `OP_13` 之后的数据推送连接起来而组装成的。" + +#: src/runes/specification.md:120 +msgid "" +"Data pushes are opcodes 0 through 78 inclusive. If a non-data push opcode is " +"encountered, i.e., any opcode equal to or greater than opcode 79, the " +"deciphered runestone is a cenotaph with no etching, mint, or edicts." +msgstr "" +"数据推送是操作码 0 到 78 之间的操作码。如果遇到大于或等于操作码 79 的操作码," +"则解密的符文是一个没有雕刻、铸造或法令的纪念碑。" + +#: src/runes/specification.md:121 +msgid "Decoding the Integer Sequence" +msgstr "解码整数序列" + +#: src/runes/specification.md:123 +msgid "" +"A sequence of 128-bit integers are decoded from the payload as LEB128 " +"varints." +msgstr "" +" 从有效载荷中解码一系列 128 位整数作为 LEB128 变长整数。" + +#: src/runes/specification.md:125 +msgid "" +"LEB128 varints are encoded as sequence of bytes, each of which has the most-" +"significant bit set, except for the last." +msgstr "" +" LEB128 变长整数被编码为一系列字节,每个字节的最高有效位都被设置,最后一个字节除外。" + +#: src/runes/specification.md:128 +msgid "" +"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." +msgstr "" +" 如果 LEB128 变长整数包含超过 18 个字节,会溢出一个 u128,或者是截断的," +"意味着在遇到未设置继续位的字节之前达到有效载荷缓冲区的末尾,解码的符石是没有铭刻、铸造或法令的纪念碑。" + +#: src/runes/specification.md:133 +msgid "Parsing the Message" +msgstr "解析消息" + +#: src/runes/specification.md:135 +msgid "The integer sequence is parsed into an untyped message:" +msgstr " 将整数序列解析为未类型化消息。" + +#: src/runes/specification.md:137 +msgid "" +"```rust\n" +"struct Message {\n" +" fields: Map>,\n" +" edicts: Vec,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:144 +msgid "" +"The integers are interpreted as a sequence of tag/value pairs, with " +"duplicate tags appending their value to the field value." +msgstr "" +"整数被解释为一系列标签/值对,重复的标签将其值附加到字段值上。" + +#: src/runes/specification.md:147 +msgid "" +"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." +msgstr "" +"如果遇到值为零的标签,则所有后续的整数都被解释为一系列四整数法令,每个法令包括一个符文ID块高度、符文ID交易索引、数量和输出。" + +#: src/runes/specification.md:159 +msgid "" +"Rune ID block heights and transaction indices in edicts are delta encoded." +msgstr "" +"法令中的符文ID块高度和交易索引采用增量编码。" + +#: src/runes/specification.md:161 +msgid "" +"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." +msgstr "" +"解码法令符文ID时,起始于基础块高度和交易索引均为零。在解码每个符文ID时," +"首先将编码的块高度增量加到基础块高度上。如果块高度增量为零,则下一个整数是交易索引增量。" +"如果块高度增量大于零,则下一个整数改为绝对交易索引。" + +#: src/runes/specification.md:167 +msgid "" +"This implies that edicts must first be sorted by rune ID before being " +"encoded in a runestone." +msgstr "" +" 这意味着在将法令编码进符石之前,必须先按符文ID对法令进行排序。" + +#: src/runes/specification.md:170 +msgid "For example, to encode the following edicts:" +msgstr " 例如,要编码以下法令:" + +#: src/runes/specification.md:172 src/runes/specification.md:181 +msgid "block" +msgstr "区块" + +#: src/runes/specification.md:172 src/runes/specification.md:181 +msgid "TX" +msgstr "" + +#: src/runes/specification.md:172 src/runes/specification.md:181 +#: src/runes/specification.md:190 +msgid "amount" +msgstr "数量" + +#: src/runes/specification.md:172 src/runes/specification.md:181 +#: src/runes/specification.md:190 +msgid "output" +msgstr "输出" + +#: src/runes/specification.md:174 src/runes/specification.md:176 +#: src/runes/specification.md:177 src/runes/specification.md:183 +#: src/runes/specification.md:184 src/runes/specification.md:185 +#: src/runes/specification.md:192 src/runes/specification.md:193 +msgid "10" +msgstr "" + +#: src/runes/specification.md:174 src/runes/specification.md:177 +#: src/runes/specification.md:183 src/runes/specification.md:184 +#: src/runes/specification.md:192 +msgid "5" +msgstr "" + +#: src/runes/specification.md:175 src/runes/specification.md:186 +#: src/runes/specification.md:422 +msgid "50" +msgstr "" + +#: src/runes/specification.md:175 src/runes/specification.md:186 +#: src/runes/specification.md:195 src/runes/specification.md:418 +msgid "25" +msgstr "" + +#: src/runes/specification.md:176 src/runes/specification.md:185 +msgid "7" +msgstr "" + +#: src/runes/specification.md:176 src/runes/specification.md:185 +#: src/runes/specification.md:194 +msgid "8" +msgstr "" + +#: src/runes/specification.md:179 +msgid "They are first sorted by block height and transaction index:" +msgstr "它们首先按区块高度和交易索引排序:" + +#: src/runes/specification.md:188 +msgid "And then delta encoded as:" +msgstr "然后 delta 编码为:" + +#: src/runes/specification.md:190 +msgid "block delta" +msgstr "" + +#: src/runes/specification.md:190 +msgid "TX delta" +msgstr "" + +#: src/runes/specification.md:195 +msgid "40" +msgstr "" + +#: src/runes/specification.md:197 +msgid "" +"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." +msgstr "" +"如果法令输出大于交易的输出数量,则遇到块为零且交易索引非零的法令符文 ID," +"或者字段被截断,意味着遇到没有值的标签,解码后的符文石是纪念碑 。" + +#: src/runes/specification.md:202 +msgid "" +"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." +msgstr "" +"请注意,如果这里制作了一个纪念碑,那么这个纪念碑并不是空的," +"意味着它包含了字段和法令,这可能包括一种蚀刻和铸币。" + +#: src/runes/specification.md:205 +msgid "Parsing the Runestone" +msgstr "解析符石" + +#: src/runes/specification.md:207 +msgid "The runestone:" +msgstr "符石" + +#: src/runes/specification.md:218 +msgid "Is parsed from the unsigned message using the following tags:" +msgstr "使用以下标签从未签名的消息中解析:" + +#: src/runes/specification.md:220 +msgid "" +"```rust\n" +"enum Tag {\n" +" Body = 0,\n" +" Flags = 2,\n" +" Rune = 4,\n" +" Premine = 6,\n" +" Cap = 8,\n" +" Amount = 10,\n" +" HeightStart = 12,\n" +" HeightEnd = 14,\n" +" OffsetStart = 16,\n" +" OffsetEnd = 18,\n" +" Mint = 20,\n" +" Pointer = 22,\n" +" Cenotaph = 126,\n" +"\n" +" Divisibility = 1,\n" +" Spacers = 3,\n" +" Symbol = 5,\n" +" Nop = 127,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:243 +msgid "" +"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." +msgstr "" +"请注意,标签按奇偶性分组,即奇数还是偶数。" +"无法识别的奇数标签将被忽略。无法识别的偶数标签会产生纪念碑。" + +#: src/runes/specification.md:246 +msgid "" +"All unused tags are reserved for use by the protocol, may be assigned at any " +"time, and must not be used." +msgstr "" +"所有未使用的标签都保留供协议使用,可以随时分配,并且不得使用。" + +#: src/runes/specification.md:249 +msgid "Body" +msgstr "主体" + +#: src/runes/specification.md:251 +msgid "" +"The `Body` tag marks the end of the runestone's fields, causing all " +"following integers to be interpreted as edicts." +msgstr "" +"`主体`标签标记了符石字段的结束,导致所有后续的整数被解释为法令。" + +#: src/runes/specification.md:254 +msgid "Flags" +msgstr "标记" + +#: src/runes/specification.md:256 +msgid "" +"The `Flag` field contains a bitmap of flags, whose position is `1 << " +"FLAG_VALUE`:" +msgstr "" +" `标记`字段包含一个标志的位图,其位置为 `1 << FLAG_VALUE`:" + +#: src/runes/specification.md:259 +msgid "" +"```rust\n" +"enum Flag {\n" +" Etching = 0,\n" +" Terms = 1,\n" +" Cenotaph = 127,\n" +"}\n" +"```" +msgstr "" + +#: src/runes/specification.md:267 +msgid "The `Etching` flag marks this transaction as containing an etching." +msgstr "`Etching`标志表示此交易包含蚀刻。" + +#: src/runes/specification.md:269 +msgid "" +"The `Terms` flag marks this transaction's etching as having open mint terms." +msgstr "`Terms`标志表示此交易的蚀刻具有开放的铸币条款。" + +#: src/runes/specification.md:275 +msgid "" +"The `Turbo` flag marks this transaction's etching as opting into future " +"protocol changes. These protocol changes may increase light client validation " +"costs, or just be highly degenerate." +msgstr "`Turbo`标记将此交易的蚀刻设置为选择未来协议可以更改。这些协议更改可能会增加轻客户端验证成本,或者仅仅是高度退化的。" + +#: src/runes/specification.md:271 +msgid "The `Cenotaph` flag is unrecognized." +msgstr "`Cenotaph`标志表示无法识别" + +#: src/runes/specification.md:273 +msgid "" +"If the value of the flags field after removing recognized flags is nonzero, " +"the runestone is a cenotaph." +msgstr "" +" 如果在移除已识别标志后,标志字段的值非零,则该符石为纪念碑。" + +#: src/runes/specification.md:276 +msgid "Rune" +msgstr "符文" + +#: src/runes/specification.md:278 +msgid "" +"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." +msgstr "" +"符文 `Rune` 字段包含正在蚀刻的符文的名称。如果设置了蚀刻`Etching`标志但省略了符文`Rune`字段," +"则会分配一个保留的符文名称。" + +#: src/runes/specification.md:284 +msgid "The `Premine` field contains the amount of premined runes." +msgstr " 预铸`Premine`字段包含预铸符文的数量。" + +#: src/runes/specification.md:288 +msgid "The `Cap` field contains the allowed number of mints." +msgstr "上限`Cap` 字段包含允许的铸币次数。" + +#: src/runes/specification.md:292 +msgid "" +"The `Amount` field contains the amount of runes each mint transaction " +"receives." +msgstr "" +"数量`Amount`字段包含每个铸币交易接收的符文数量。" + +#: src/runes/specification.md:294 +msgid "HeightStart and HeightEnd" +msgstr "起始高度和结束高度" + +#: src/runes/specification.md:296 +msgid "" +"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`." +msgstr "" +"`起始高度`和`结束高度`字段分别包含铸币的起始和结束的绝对区块高度。" +"铸币从具有`起始高度`的区块开始,并在具有`结束高度`的区块中关闭。" + +#: src/runes/specification.md:300 +msgid "OffsetStart and OffsetEnd" +msgstr "起始偏移和结束偏移" + +#: src/runes/specification.md:302 +msgid "" +"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`." +msgstr "" +"`起始偏移`和`结束偏移`字段包含铸币的起始和结束区块高度,相对于蚀刻被挖掘的区块。" +"铸币从高度为`起始偏移` + `蚀刻高度`的区块开始,并在高度为`结束偏移` + `蚀刻高度`的区块中关闭。" + +#: src/runes/specification.md:307 +msgid "Mint" +msgstr "铸造" + +#: src/runes/specification.md:309 +msgid "" +"The `Mint` field contains the Rune ID of the rune to be minted in this " +"transaction." +msgstr "" +" 铸造`Mint`字段包含此交易中将要铸造的符文的符文ID。" + +#: src/runes/specification.md:314 +msgid "" +"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." +msgstr "" +"指针`Pointer`字段包含应将未分配的符文通过法令转移至的输出索引。如果缺少指针`Pointer`字段," +"则未分配的符文将转移到第一个非`OP_RETURN`输出。" + +#: src/runes/specification.md:318 +msgid "Cenotaph" +msgstr "纪念碑" + +#: src/runes/specification.md:320 +msgid "The `Cenotaph` field is unrecognized." +msgstr "纪念碑`Cenotaph` 字段无法识别。" + +#: src/runes/specification.md:324 +msgid "" +"The `Divisibility` field, raised to the power of ten, is the number of " +"subunits in a super unit of runes." +msgstr "" +" 可分性`Divisibility`字段,提升十的幂次,是一个超级单位符文中的子单位数量。" + +#: src/runes/specification.md:327 +msgid "" +"For example, the amount `1234` of different runes with divisibility 0 " +"through 3 is displayed as follows:" +msgstr "" +" 例如,不同符文的数量`1234`,其可分性为0至3,显示如下:" + +#: src/runes/specification.md:330 src/runes/specification.md:348 +msgid "Display" +msgstr "显示" + +#: src/runes/specification.md:332 +msgid "1234" +msgstr "" + +#: src/runes/specification.md:333 +msgid "123.4" +msgstr "" + +#: src/runes/specification.md:334 +msgid "12.34" +msgstr "" + +#: src/runes/specification.md:335 +msgid "1.234" +msgstr "" + +#: src/runes/specification.md:337 src/runes/specification.md:348 +msgid "Spacers" +msgstr "间隔符" + +#: src/runes/specification.md:339 +msgid "" +"The `Spacers` field is a bitfield of `•` spacers that should be displayed " +"between the letters of the rune's name." +msgstr "" +" 间隔符`Spacers`字段是一个`•`位字段,用于表示符文名称字母之间是否应显示间隔符。" + +#: src/runes/specification.md:342 +msgid "" +"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." +msgstr "" +"位字段的第N个字段,从最不重要的位开始,决定从符文名称的左侧开始," +"是否在符文名称的第N个字符和第N+1个字符之间显示间隔符。" + +#: src/runes/specification.md:346 +msgid "For example, the rune name `AAAA` rendered with different spacers:" +msgstr " 例如,符文名`AAAA`在不同间隔符的渲染下:" + +#: src/runes/specification.md:350 +msgid "0b1" +msgstr "" + +#: src/runes/specification.md:350 +msgid "A•AAA" +msgstr "" + +#: src/runes/specification.md:351 +msgid "0b11" +msgstr "" + +#: src/runes/specification.md:351 +msgid "A•A•AA" +msgstr "" + +#: src/runes/specification.md:352 +msgid "0b10" +msgstr "" + +#: src/runes/specification.md:352 +msgid "AA•AA" +msgstr "" + +#: src/runes/specification.md:353 +msgid "0b111" +msgstr "" + +#: src/runes/specification.md:353 +msgid "A•A•A•A" +msgstr "" + +#: src/runes/specification.md:355 +msgid "Trailing spacers are ignored." +msgstr "尾随间隔符将被忽略。" + +#: src/runes/specification.md:359 +msgid "" +"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." +msgstr "" +"符号`Symbol`字段是符文货币符号的Unicode代码点,应在该符文金额之后显示。" +"如果符文没有货币符号,则应使用通用货币字符 `¤`。" + +#: src/runes/specification.md:363 +msgid "" +"For example, if the `Symbol` is `#` and the divisibility is 2, the amount of " +"`1234` units should be displayed as `12.34 #`." +msgstr "" +" 例如,如果`符号`是`#`,可分性为2,那么`1234`单位的金额应显示为12.34#。" + +#: src/runes/specification.md:366 +msgid "Nop" +msgstr "Nop" + +#: src/runes/specification.md:368 +msgid "The `Nop` field is unrecognized." +msgstr "`Nop`字段无法识别。" + +#: src/runes/specification.md:372 +msgid "Cenotaphs have the following effects:" +msgstr " 纪念碑具有以下效果: " + +#: src/runes/specification.md:374 +msgid "All runes input to a transaction containing a cenotaph are burned." +msgstr " 包含纪念碑的交易中的所有输入符文都将被销毁。 " + +#: src/runes/specification.md:376 +msgid "" +"If the runestone that produced the cenotaph contained an etching, the etched " +"rune has supply zero and is unmintable." +msgstr "" +" 如果产生纪念碑的符石包含蚀刻,那么蚀刻的符文供应量为零且无法铸造。 " + +#: src/runes/specification.md:379 +msgid "" +"If the runestone that produced the cenotaph is a mint, the mint counts " +"against the mint cap and the minted runes are burned." +msgstr "" +" 如果产生纪念碑的符石是铸币,那么铸币将计入铸币上限,且铸造的符文将被销毁。 " + +#: src/runes/specification.md:382 +msgid "" +"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." +msgstr "" +" 如果符石包含无法识别的偶数标签、无法识别的标志、输出编号大于输入数量的法令、" +"区块为零且交易索引非零的符文ID、格式错误的varint、符石输出脚本公钥中的非数据推送指令、" +"没有后续值的标签或不属于法令的尾随整数,则可能创建纪念碑。" + +#: src/runes/specification.md:388 +msgid "Executing the Runestone" +msgstr "执行符石" + +#: src/runes/specification.md:390 +msgid "" +"Runestones are executed in the order their transactions are included in " +"blocks." +msgstr "" +" 符石按照其交易被包含在区块中的顺序执行。" + +#: src/runes/specification.md:392 +msgid "Etchings" +msgstr "蚀刻" + +#: src/runes/specification.md:394 +msgid "A runestone may contain an etching:" +msgstr " 符石可能包含蚀刻: " + +#: src/runes/specification.md:407 +msgid "" +"`rune` is the name of the rune to be etched, encoded as modified base-26 " +"integer." +msgstr "" +" `rune`是要蚀刻的符文的名称,编码为修改后的26进制整数。" + +#: src/runes/specification.md:410 msgid "" -"`/r/metadata/`: JSON string containing the hex-encoded CBOR " -"metadata." +"Rune names consist of the letters A through Z, with the following encoding:" msgstr "" -"`/r/metadata/`: 包含十六进制编码的 CBOR 元数据 的 JSON 字符串" +"符文名称由字母A至Z组成,编码如下:" -#: src/inscriptions/recursion.md:45 -msgid "`/r/sat/`: the first 100 inscription ids on a sat." -msgstr "`/r/sat/`: 在一个Sats上的头100个铭文ID. +#: src/runes/specification.md:412 +msgid "Encoding" +msgstr "编码" -#: src/inscriptions/recursion.md:46 -msgid "" -"`/r/sat//`: the set of 100 inscription ids on ``." +#: src/runes/specification.md:414 +msgid "A" 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." +#: src/runes/specification.md:415 +msgid "B" 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." +#: src/runes/specification.md:416 src/runes/specification.md:421 +msgid "…" 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." +#: src/runes/specification.md:417 +msgid "Y" msgstr "" -"来自上述递归端点的响应是 JSON。为了向后兼容,支持其他端点,其中一些返回纯文本响应。 " -#: src/inscriptions/recursion.md:57 -msgid "`/blockheight`: latest block height." -msgstr "`/blockheight`:最新区块高度。" +#: src/runes/specification.md:417 +msgid "24" +msgstr "" -#: src/inscriptions/recursion.md:58 -msgid "`/blockhash`: latest block hash." -msgstr "`/blockhash`:最新的块哈希。" +#: src/runes/specification.md:418 +msgid "Z" +msgstr "" -#: src/inscriptions/recursion.md:59 -msgid "`/blockhash/`: block hash at given block height." -msgstr "`/blockhash/`:给定块高度的块哈希。" +#: src/runes/specification.md:419 +msgid "AA" +msgstr "" -#: src/inscriptions/recursion.md:60 -msgid "`/blocktime`: UNIX time stamp of latest block." -msgstr "`/blocktime`:最新块的 UNIX 时间戳。" +#: src/runes/specification.md:419 +msgid "26" +msgstr "" -#: src/inscriptions/recursion.md:65 -msgid "`/r/blockhash/0`:" +#: src/runes/specification.md:420 +msgid "AB" msgstr "" -#: src/inscriptions/recursion.md:67 -msgid "" -"```json\n" -"\"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\"\n" -"```" +#: src/runes/specification.md:420 +msgid "27" msgstr "" -#: src/inscriptions/recursion.md:71 -msgid "`/r/blockheight`:" +#: src/runes/specification.md:422 +msgid "AY" msgstr "" -#: src/inscriptions/recursion.md:73 -msgid "" -"```json\n" -"777000\n" -"```" +#: src/runes/specification.md:423 +msgid "AZ" msgstr "" -#: src/inscriptions/recursion.md:77 -msgid "`/r/blockinfo/0`:" +#: src/runes/specification.md:423 +msgid "51" msgstr "" -#: src/inscriptions/recursion.md:79 +#: src/runes/specification.md:424 +msgid "BA" +msgstr "" + +#: src/runes/specification.md:424 +msgid "52" +msgstr "" + +#: src/runes/specification.md:426 +msgid "And so on and so on." +msgstr "依此类推。" + +#: src/runes/specification.md:428 +msgid "Rune names `AAAAAAAAAAAAAAAAAAAAAAAAAAA` and above are reserved." +msgstr " 符文名称`AAAAAAAAAAAAAAAAAAAAAAAAAAA`及以上被保留。" + +#: src/runes/specification.md:430 +msgid "If `rune` is omitted a reserved rune name is allocated as follows:" +msgstr "如果省略`rune`,则按以下方式分配保留的符文名称: " + +#: src/runes/specification.md:432 msgid "" -"```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" +"```rust\n" +"fn reserve(block: u64, tx: u32) -> Rune {\n" +" Rune(\n" +" 6402364363415443603228541259936211926\n" +" + (u128::from(block) << 32 | u128::from(tx))\n" +" )\n" "}\n" "```" msgstr "" -#: src/inscriptions/recursion.md:111 -msgid "`/r/blocktime`:" +#: src/runes/specification.md:441 +msgid "" +"`6402364363415443603228541259936211926` corresponds to the rune name " +"`AAAAAAAAAAAAAAAAAAAAAAAAAAA`." msgstr "" +"`6402364363415443603228541259936211926` 对应于符文名称 " +"`AAAAAAAAAAAAAAAAAAAAAAAAAAA`." -#: src/inscriptions/recursion.md:113 +#: src/runes/specification.md:444 msgid "" -"```json\n" -"1700770905\n" -"```" +"If `rune` is present, it must be unlocked as of the block in which the " +"etching appears." msgstr "" +" 如果存在 `rune`,则它必须在蚀刻出现的区块中解锁。" -#: src/inscriptions/recursion.md:117 src/inscriptions/recursion.md:178 +#: src/runes/specification.md:447 msgid "" -"`/r/" -"children/60bcf821240064a9c55225c4f01711b0ebbcab39aa3fafeefe4299ab158536fai0/49`:" +"Initially, all rune names of length thirteen and longer, up until the first " +"reserved rune name, are unlocked." msgstr "" +" 最初,所有长度为十三及以上的符文名称,直到第一个保留的符文名称,都被解锁。" -#: src/inscriptions/recursion.md:119 src/inscriptions/recursion.md:180 +#: src/runes/specification.md:450 msgid "" -"```json\n" -"{\n" -" \"ids\":[\n" -" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4900\",\n" -" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4901\",\n" -" ...\n" -" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4935\",\n" -" \"7cd66b8e3a63dcd2fada917119830286bca0637267709d6df1ca78d98a1b4487i4936\"\n" -" ],\n" -" \"more\":false,\n" -" \"page\":49\n" -"}\n" -"```" +"Runes begin unlocking in block 840,000, the block in which the runes " +"protocol activates." msgstr "" +" 符文从840,000区块开始解锁,即符文协议激活的区块。" -#: src/inscriptions/recursion.md:133 +#: src/runes/specification.md:453 msgid "" -"`r/" -"inscription/3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0`" +"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." msgstr "" +"此后,每个17,500区块周期,连续解锁下一个最短长度的符文名称。" +"因此,在840,000到857,500区块之间,解锁十二字符的符文名称," +"在857,500到875,000区块之间,解锁十一字符的符文名称," +"依此类推,直到在1,032,500到1,050,000区块之间解锁一个字符的符文名称。具体的解锁时间表请参见ord代码库。" -#: src/inscriptions/recursion.md:135 +#: src/runes/specification.md:460 msgid "" -"```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" -"```" +"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." msgstr "" +" 为了防止对已广播但未挖掘的蚀刻进行前置操作,如果正在蚀刻非保留的符文名称," +"则蚀刻交易必须包含对正在蚀刻的名称的有效承诺。" -#: src/inscriptions/recursion.md:152 +#: src/runes/specification.md:464 msgid "" -"`/r/" -"metadata/35b66389b44535861c44b2b18ed602997ee11db9a30d384ae89630c9fc6f011fi3`:" +"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." msgstr "" +"承诺 `commitment` 包括在输入见证 `tapscript` 中推送的符文名称数据,编码为省略尾随零字节的小端整数,其中被花费的输出至少有六次确认。" -#: src/inscriptions/recursion.md:154 +#: src/runes/specification.md:468 +msgid "If a valid commitment is not present, the etching is ignored." +msgstr " 如果没有有效的承诺 `commitment`,蚀刻将被忽略。" + +#: src/runes/specification.md:472 msgid "" -"```json\n" -"\"a2657469746c65664d656d6f727966617574686f726e79656c6c6f775f6f72645f626f74\"\n" -"```" +"A runestone may mint a rune by including the rune's ID in the `Mint` field." msgstr "" +" 符石可以通过在铸币`Mint`字段中包含符文的ID来铸造符文。" -#: src/inscriptions/recursion.md:158 -msgid "`/r/sat/1023795949035695`:" +#: src/runes/specification.md:474 +msgid "" +"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." msgstr "" +"如果铸币是开放的,铸币金额将添加到交易输入中的未分配符文中。这些符文可以使用法令转移," +"并且否则将转移到第一个非`OP_RETURN`输出,或由指针Pointer`字段指定的输出。" -#: src/inscriptions/recursion.md:160 +#: src/runes/specification.md:479 msgid "" -"```json\n" -"{\n" -" \"ids\":[\n" -" \"17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0\"\n" -" ],\n" -" \"more\":false,\n" -" \"page\":0\n" -"}\n" -"```" +"Mints may be made in any transaction after an etching, including in the same " +"block." msgstr "" +" 铸币可以在蚀刻之后的任何交易中进行,包括在同一个区块中。" -#: src/inscriptions/recursion.md:170 -msgid "`/r/sat/1023795949035695/at/-1`:" +#: src/runes/specification.md:494 +msgid "" +"A runestone may contain any number of edicts, which are processed in " +"sequence." msgstr "" +" 符石可以包含任意数量的法令edicts,这些法令edicts按顺序处理。" -#: src/inscriptions/recursion.md:172 +#: src/runes/specification.md:496 msgid "" -"```json\n" -"{\n" -" \"id\":" -"\"17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0\"\n" -"}\n" -"```" +"Before edicts are processed, input runes, as well as minted or premined " +"runes, if any, are unallocated." msgstr "" +" 在处理法令edicts之前,输入符文以及铸造或预铸的符文(如果有)是未分配的。" -#: src/inscriptions/rendering.md:4 -msgid "Aspect Ratio" -msgstr "纵横比" - -#: src/inscriptions/rendering.md:7 +#: src/runes/specification.md:499 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." +"Each edict decrements the unallocated balance of rune `id` and increments " +"the balance allocated to transaction outputs of rune `id`." msgstr "" -"铭文应以正方形的纵横比进行渲染。非正方形纵横比的铭文不应被裁剪,而应该居中并调整大小以适应其容器。" +"每个法令将rune `id`的未分配余额减少,并将rune `id`的余额增加到交易输出。" -#: src/inscriptions/rendering.md:11 -msgid "Maximum Size" -msgstr "最大尺寸" +#: src/runes/specification.md:502 +msgid "" +"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`." +msgstr "" +"如果法令edict将分配的符文数量超过当前未分配的符文,则数量将减少到当前未分配的符文`数量`。" +"换句话说,法令edict分配了rune`id`的所有剩余未分配单位。" -#: src/inscriptions/rendering.md:14 +#: src/runes/specification.md:506 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." +"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." msgstr "" -"由[ordinals.com](https://ordinals.com/)使用的`ord` 浏览器," -"展示的铭文预览的最大尺寸为576乘以576像素,这使得它在选择最大显示尺寸时是一个合理的选择。" +" 因为在蚀刻被包含在区块之前不知道蚀刻的符文的ID,所以使用ID `0:0`表示此交易中正在蚀刻的符文(如果有)。" -#: src/inscriptions/rendering.md:18 -msgid "Image Rendering" -msgstr "图片渲染" +#: src/runes/specification.md:509 +msgid "An edict with `amount` zero allocates all remaining units of rune `id`." +msgstr "" +"金额`amount`为零的法令分配了rune `id`的所有剩余单位。" -#: src/inscriptions/rendering.md:21 +#: src/runes/specification.md:511 msgid "" -"The CSS `image-rendering` property controls how images are resampled when " -"upscaled and downscaled." +"An edict with `output` equal to the number of transaction outputs allocates " +"`amount` runes to each non-`OP_RETURN` output." msgstr "" -"CSS中的`image-rendering` 属性控制了在图片放大和缩小时如何重新采样图片。" +"输出`output` 等于交易输出数量`amount` 的法令将金额符文分配给每个非`OP_RETURN`输出。" -#: src/inscriptions/rendering.md:24 +#: src/runes/specification.md:514 msgid "" -"When downscaling image inscriptions, `image-rendering: auto`, should be " -"used. This is desirable even when downscaling pixel art." +"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." msgstr "" -"在缩小图片铭文时,应使用`image-rendering: auto`,即使在缩小像素艺术图片时,这也是可取的。" +"金额`amount` 为零且输出等于交易输出`output` 数量的法令将所有未分配的rune `id`单位平均分配给每个非`OP_RETURN`输出。" +"如果未分配的符文数量不能被非`OP_RETURN`输出的数量整除,则前`R`个非`OP_RETURN`输出将分配1个额外的符文," +"其中`R`是将未分配的rune `id`单位余额除以非`OP_RETURN`输出数量后的余数。" -#: src/inscriptions/rendering.md:27 +#: src/runes/specification.md:521 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." +"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." msgstr "" -"在放大非AVIF格式的图片铭文时,应使用`image-rendering: pixelated`。" -"这在放大像素艺术图片时是可取的,因为它保留了像素的锐利边缘。" -"虽然在放大非像素艺术图片时这可能不太理想,但为了与ord浏览器的视觉兼容性,仍应使用此设置。" +"如果符石中的任何法令具有`区块`为零且`tx`大于零的符文ID,或`输出`大于交易输出数量,则符石是纪念碑。" -#: src/inscriptions/rendering.md:32 +#: src/runes/specification.md:525 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." +"Note that edicts in cenotaphs are not processed, and all input runes are " +"burned." msgstr "" -"在放大AVIF和JPEG XL格式的图片铭文时,应使用`image-rendering: auto`。" -"这允许铭文者选择非像素化的放大方式,适用于非像素艺术的铭文。" -"直到JPEG XL格式被浏览器广泛支持之前,它并不是一个推荐的图片格式。" +"注意,纪念碑中的法令不会被处理,所有输入符文都将被销毁。" #: src/faq.md:1 msgid "Ordinal Theory FAQ" @@ -3662,7 +5050,7 @@ msgid "" "`datadir` option because the cookie file will still be in the default " "location for `bitcoin-cli` and `ord` to find." msgstr "" -"T区块链占用约600GB的磁盘空间。如果你有一个外接硬盘来存储区块,可以使用配置选" +"区块链占用约600GB的磁盘空间。如果你有一个外接硬盘来存储区块,可以使用配置选" "项`blocksdir=`. 这比使用`datadir` 选项更简单, `bitcoin-" "cli` 和 `ord` 可以在默认的位置找到cookie文件" @@ -3811,8 +5199,8 @@ msgid "" "requires [`ord server`](explorer.md) running in the background. Make sure " "these programs are running:" msgstr "" -"`ord` 使用Bitcoin Core来管理私钥,签署交易以及向比特币网络广播交易。" -"此外,`ord` 钱包需要在后台运行[`ord server`](explorer.md),请确保这些程序运行" +"`ord` 使用Bitcoin Core来管理私钥,签署交易以及向比特币网络广播交易。此外," +"`ord` 钱包需要在后台运行[`ord server`](explorer.md),请确保这些程序运行" #: src/guides/wallet.md:173 msgid "" @@ -3853,8 +5241,7 @@ 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`" -"你可以设置以下选项:" +"如果你想指定不同的名称或者在非默认的URL上运行 `ord server`你可以设置以下选项:" #: src/guides/wallet.md:195 msgid "" @@ -3884,8 +5271,8 @@ msgid "" "and import them into another descriptor-based wallet. To export the wallet " "descriptors, which include your private keys:" msgstr "" -"`ord`钱包使用描述符descriptors,你可以导出输出描述符并将它们导入另外一个基于描述符的钱包" -"导出钱包描述符,其中包含你的私钥:" +"`ord`钱包使用描述符descriptors,你可以导出输出描述符并将它们导入另外一个基于描" +"述符的钱包导出钱包描述符,其中包含你的私钥:" #: src/guides/wallet.md:212 msgid "" @@ -4008,8 +5395,7 @@ msgstr "" 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 " +msgstr "将描述符粘贴到终端中,UNIX里按CTRL-D 或 Windows里按 CTRL-Z " #: src/guides/wallet.md:270 msgid "Receiving Sats" @@ -4028,6 +5414,7 @@ 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 +#: src/guides/wallet.md:429 msgid "" "```\n" "ord wallet receive\n" @@ -4042,7 +5429,8 @@ msgstr "向上面地址发送一些资金。" msgid "You can see pending transactions with:" msgstr "你可以使用以下命令看到交易情况:" -#: src/guides/wallet.md:286 src/guides/wallet.md:378 src/guides/wallet.md:405 +#: src/guides/wallet.md:286 src/guides/wallet.md:378 src/guides/wallet.md:414 +#: src/guides/wallet.md:440 msgid "" "```\n" "ord wallet transactions\n" @@ -4148,7 +5536,7 @@ msgid "" "printed when you run:" msgstr "一旦reveal交易完成记账,你可以使用以下命令查询铭文ID:" -#: src/guides/wallet.md:338 src/guides/wallet.md:385 src/guides/wallet.md:411 +#: src/guides/wallet.md:338 src/guides/wallet.md:385 src/guides/wallet.md:446 msgid "" "```\n" "ord wallet inscriptions\n" @@ -4164,8 +5552,8 @@ msgid "" "Parent-child inscriptions enable what is colloquially known as collections, " "see [provenance](../inscriptions/provenance.md) for more information." msgstr "" -"父子铭文使得人们通常所说的收藏成为可能,有关更多信息," -"请参见[provenance](../inscriptions/provenance.md)。" +"父子铭文使得人们通常所说的收藏成为可能,有关更多信息,请参见[provenance](../" +"inscriptions/provenance.md)。" #: src/guides/wallet.md:348 msgid "" @@ -4173,8 +5561,9 @@ msgid "" "inscribed and present in the wallet. To choose a parent run `ord wallet " "inscriptions` and copy the inscription id (``)." msgstr "" -"要使一个铭文成为另一个铭文的子项,父铭文必须已经被铭刻并且存在于钱包中。" -"要选择一个父铭文,请运行`ord wallet inscriptions`并复制铭文ID(``)。" +"要使一个铭文成为另一个铭文的子项,父铭文必须已经被铭刻并且存在于钱包中。要选" +"择一个父铭文,请运行`ord wallet inscriptions`并复制铭文ID" +"(``)。" #: src/guides/wallet.md:352 msgid "Now inscribe the child inscription and specify the parent like so:" @@ -4192,14 +5581,13 @@ msgstr "" msgid "" "This relationship cannot be added retroactively, the parent has to be " "present at inception of the child." -msgstr "" -"这种父子关系不能事后添加,父铭文必须在子铭文创建之初就存在。" +msgstr "这种父子关系不能事后添加,父铭文必须在子铭文创建之初就存在。" #: src/guides/wallet.md:361 msgid "Sending Inscriptions" msgstr "发送铭文" -#: src/guides/wallet.md:364 +#: src/guides/wallet.md:364 src/guides/wallet.md:392 msgid "Ask the recipient to generate a new address by running:" msgstr "铭文接收方使用一下命令生成地址" @@ -4214,7 +5602,7 @@ msgid "" "```" msgstr "" -#: src/guides/wallet.md:376 src/guides/wallet.md:404 +#: src/guides/wallet.md:376 src/guides/wallet.md:412 src/guides/wallet.md:439 msgid "See the pending transaction with:" msgstr "检查未完成交易情况:" @@ -4225,25 +5613,68 @@ msgid "" msgstr "一旦交易确认,接收方可以使用一下命令查看接收到的铭文" #: src/guides/wallet.md:389 +msgid "Sending Runes" +msgstr "发送铭文" + +#: src/guides/wallet.md:398 +msgid "Send the runes by running:" +msgstr "使用命令格式发送铭文:" + +#: src/guides/wallet.md:400 +msgid "" +"```\n" +"ord wallet send --fee-rate
\n" +"```" +msgstr "" + +#: src/guides/wallet.md:404 +msgid "" +"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`." +msgstr "" +"在 `RUNES_AMOUNT` 是要发送的符文数量,一个 `:` 字符,和符文的名字。" +"例如,如果你想发送1000个EXAMPLE符文,你应该使用`1000:EXAMPLE`。" + +#: src/guides/wallet.md:408 +msgid "" +"```\n" +"ord wallet send --fee-rate 1 SOME_ADDRESS 1000:EXAMPLE\n" +"```" +msgstr "" + +#: src/guides/wallet.md:418 +msgid "" +"Once the send transaction confirms, the recipient can confirm receipt with:" +msgstr "一旦交易确认,接收方可以使用一下命令查看接收到的铭文" + +#: src/guides/wallet.md:420 +msgid "" +"```\n" +"ord wallet balance\n" +"```" +msgstr "" + +#: src/guides/wallet.md:424 msgid "Receiving Inscriptions" msgstr "接收铭文" -#: src/guides/wallet.md:392 +#: src/guides/wallet.md:427 msgid "Generate a new receive address using:" msgstr "使用以下命令生成一个新的接收地址" -#: src/guides/wallet.md:398 +#: src/guides/wallet.md:433 msgid "The sender can transfer the inscription to your address using:" msgstr "发送方使用命令发送铭文到你的地址" -#: src/guides/wallet.md:400 +#: src/guides/wallet.md:435 msgid "" "```\n" "ord wallet send --fee-rate ADDRESS INSCRIPTION_ID\n" "```" msgstr "" -#: src/guides/wallet.md:409 +#: src/guides/wallet.md:444 msgid "Once the send transaction confirms, you can confirm receipt by running:" msgstr "一旦交易确认,你可以使用以下命令确认收到" @@ -4268,7 +5699,7 @@ msgstr "创建批量铭文,使用批处理文件`batch.yaml`, 运行" #: src/guides/batch-inscribing.md:13 msgid "" "```bash\n" -"ord wallet inscribe --fee-rate 21 --batch batch.yaml\n" +"ord wallet batch --fee-rate 21 --batch batch.yaml\n" "```" msgstr "" @@ -4300,6 +5731,33 @@ msgid "" "# sat to inscribe on, can only be used with `same-sat`:\n" "# sat: 5000000000\n" "\n" +"# rune to etch (optional)\n" +"etching:\n" +" # rune name\n" +" rune: THE•BEST•RUNE\n" +" # allow subdividing super-unit into `10^divisibility` sub-units\n" +" divisibility: 2\n" +" # premine\n" +" premine: 1000.00\n" +" # total supply, must be equal to `premine + terms.cap * terms.amount`\n" +" supply: 10000.00\n" +" # currency symbol\n" +" symbol: $\n" +" # mint terms (optional)\n" +" terms:\n" +" # amount per mint\n" +" amount: 100.00\n" +" # maximum number of mints\n" +" cap: 90\n" +" # mint start and end absolute block height (optional)\n" +" height:\n" +" start: 840000\n" +" end: 850000\n" +" # mint start and end block height relative to etching height (optional)\n" +" offset:\n" +" start: 1000\n" +" end: 9000\n" +"\n" "# inscriptions to inscribe\n" "inscriptions:\n" " # path to inscription content\n" @@ -4322,10 +5780,10 @@ msgid "" "orci\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" +" # inscription metaprotocol (optional)\n" +" metaprotocol: DOPEPROTOCOL-42069\n" "\n" "- file: tulip.png\n" " destination: " @@ -5029,8 +6487,7 @@ msgstr "" #: src/guides/moderation.md:4 msgid "" "`ord` includes a block explorer, which you can run locally with `ord server`." -msgstr "" -"`ord` 包含了一个区块浏览器,你可以在本地运行`ord server`." +msgstr "`ord` 包含了一个区块浏览器,你可以在本地运行`ord server`." #: src/guides/moderation.md:6 msgid "" @@ -5644,23 +7101,23 @@ msgstr "" msgid "" "`ord` can be configured with the command line, environment variables, a " "configuration file, and default values." -msgstr "" -"`ord`可以通过命令行、环境变量、配置文件以及默认值进行配置。" +msgstr "`ord`可以通过命令行、环境变量、配置文件以及默认值进行配置。" #: src/guides/settings.md:7 msgid "" "The command line takes precedence over environment variables, which take " "precedence over the configuration file, which takes precedence over defaults." msgstr "" -"命令行的优先级高于环境变量,环境变量的优先级又高于配置文件,配置文件的优先级高于默认值。" +"命令行的优先级高于环境变量,环境变量的优先级又高于配置文件,配置文件的优先级" +"高于默认值。" #: src/guides/settings.md:10 msgid "" "The path to the configuration file can be given with `--config " "`. `ord` will error if `` doesn't exist." msgstr "" -"配置文件的路径可以通过 `--config `给出." -" 如果 `` 不存在则`ord` 会显示错误 ." +"配置文件的路径可以通过 `--config `给出. 如果 `` 不" +"存在则`ord` 会显示错误 ." #: src/guides/settings.md:13 msgid "" @@ -5669,16 +7126,18 @@ msgid "" "` 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`。如果它不存在,这不是一个错误。" +"可以使用`--config-dir ` 或 `--datadir ` 指定" +"包含名为ord.yaml的配置文件的目录路径。在这种情况下,配置路径为" +"`/ord.yaml`或`/ord.yaml`。如果它不存在,这不" +"是一个错误。" #: src/guides/settings.md:18 msgid "" "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的文件,它将会被加载。" +"如果没有给出`--config`、`--config-dir`或`--datadir`中的任何一个,并且在默认数" +"据目录中存在一个名为ord.yaml的文件,它将会被加载。" #: src/guides/settings.md:21 msgid "" @@ -5688,9 +7147,10 @@ msgid "" "`--datadir` on the command line, the `ORD_DATA_DIR` environment variable, or " "`data_dir` in the config file." msgstr "" -"对于命令行中名为`--setting-name`的设置,环境变量将被命名为`ORD_SETTING_NAME`," -"配置文件中的字段将被命名为`setting_name`。例如,数据目录可以通过命令行中的`--datadir`、" -"环境变量`ORD_DATA_DIR`或配置文件中的`data_dir`来配置。" +"对于命令行中名为`--setting-name`的设置,环境变量将被命名为" +"`ORD_SETTING_NAME`,配置文件中的字段将被命名为`setting_name`。例如,数据目录" +"可以通过命令行中的`--datadir`、环境变量`ORD_DATA_DIR`或配置文件中的`data_dir`" +"来配置。" #: src/guides/settings.md:27 msgid "See `ord --help` for documentation of all the settings." @@ -5700,8 +7160,7 @@ msgstr "查看`ord --help`可以获取所有设置的文档。" msgid "" "`ord`'s current configuration can be viewed as JSON with the `ord settings` " "command." -msgstr "" -"`ord`当前的配置可以通过`ord settings`命令以JSON格式查看。" +msgstr "`ord`当前的配置可以通过`ord settings`命令以JSON格式查看。" #: src/guides/settings.md:32 msgid "Example Configuration" @@ -5751,15 +7210,13 @@ msgstr "隐藏铭文内容" msgid "" "Inscription content can be selectively prevented from being served by `ord " "server`." -msgstr "" -"铭文内容可以被选择性地阻止由`ord server`提供服务。" +msgstr "铭文内容可以被选择性地阻止由`ord server`提供服务。" #: src/guides/settings.md:74 msgid "" "Unlike other settings, this can only be configured with the configuration " "file or environment variables." -msgstr "" -"与其他设置不同,这只能通过配置文件或环境变量来配置。" +msgstr "与其他设置不同,这只能通过配置文件或环境变量来配置。" #: src/guides/settings.md:77 msgid "To hide inscriptions with an environment variable:" @@ -5893,16 +7350,17 @@ msgid "" "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`。" +"`ord env `在``中创建一个测试环境,启动`bitcoind`和`ord " +"server`实例,打印与测试`bitcoind`和`ord server`实例交互的示例命令,等待`CTRL-" +"C`,然后关闭`bitcoind`和`ord server`。" #: src/guides/testing.md:12 msgid "" "`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 "" -"`ord env`尝试使用端口9000作为`bitcoind`的RPC接口,以及端口`9001`作为`ord`的RPC接口," -"但如果这些端口被占用,它将回退到随机的未使用端口。" +"`ord env`尝试使用端口9000作为`bitcoind`的RPC接口,以及端口`9001`作为`ord`的" +"RPC接口,但如果这些端口被占用,它将回退到随机的未使用端口。" #: src/guides/testing.md:15 msgid "" @@ -5910,16 +7368,16 @@ msgid "" "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`。" +"在env目录内部,`ord env`将会将`bitcoind`的配置写入`bitcoin.conf`,`ord`的配置" +"写入`ord.yaml`,以及环境配置写入`env.json`。" #: src/guides/testing.md:19 msgid "" "`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 "" -"`env.json`包含了调用`bitcoin-cli`和`ord wallet`所需的命令," -"以及`bitcoind`和`ord server`正在监听的端口信息。" +"`env.json`包含了调用`bitcoin-cli`和`ord wallet`所需的命令,以及`bitcoind`和" +"`ord server`正在监听的端口信息。" #: src/guides/testing.md:22 msgid "These can be extracted into shell commands using `jq`:" @@ -5981,8 +7439,8 @@ msgid "" "Most `ord` commands in [wallet](wallet.md) and [explorer](explorer.md) can " "be run with the following network flags:" msgstr "" -"大多数在[钱包](wallet.md) 和 [浏览器](explorer.md) 中的 `ord`命令可以使" -"用以下网络标志运行:" +"大多数在[钱包](wallet.md) 和 [浏览器](explorer.md) 中的 `ord`命令可以使用以下" +"网络标志运行:" #: src/guides/testing.md:54 msgid "Network" @@ -6033,7 +7491,7 @@ msgstr "" #: src/guides/testing.md:71 msgid "Run `ord server` in regtest with:" -msgstr "在regtest里运行bitcoind,使用:" +msgstr "在regtest里运行ord server,使用:" #: src/guides/testing.md:73 msgid "" @@ -6141,8 +7599,8 @@ msgid "" "change the inscription IDs in your inscription to the mainnet inscription " "IDs of your dependencies before making the final inscription on mainnet." msgstr "" -"然而,铭文ID在主网和测试链之间是不同的,因此在在主网上进行最终铭文之前," -"你必须将你铭文中的铭文ID更改为你依赖项的主网铭文ID。" +"然而,铭文ID在主网和测试链之间是不同的,因此在在主网上进行最终铭文之前,你必" +"须将你铭文中的铭文ID更改为你依赖项的主网铭文ID。" #: src/guides/testing.md:131 msgid "Then you can inscribe your recursive inscription with:" @@ -6183,11 +7641,12 @@ msgid "" "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,这将返回主网铭文的内容。" +"为了避免在测试时必须将依赖铭文ID更改为主网铭文ID,你可以在测试时使用内容代" +"理。`ord server`接受一个`--content-proxy`选项,它需要另一个`ord server`实例的" +"URL。当设置了内容代理并且铭文未找到时,向`/content/`发出请" +"求,`ord server`将会将请求转发给内容代理。这允许你运行一个带有主网内容代理的" +"测试`ord server`实例。然后你可以在测试铭文中使用主网铭文ID,这将返回主网铭文" +"的内容。" #: src/guides/testing.md:155 msgid "" @@ -6343,7 +7802,7 @@ msgstr "不普通的" #: src/bounty/2.md:7 msgid " sat to the submission address:" -msgstr "聪到下列地址" +msgstr "聪到下列地址:" #: src/bounty/2.md:9 msgid "✅: [347100000000000](https://ordinals.com/sat/347100000000000)" @@ -6391,9 +7850,9 @@ msgid "" "sat 0, the first sat to be mined is `nvtdijuwxlp` and the name of sat " "2,099,999,997,689,999, the last sat to be mined, is `a`." msgstr "" -"任务3有两个部分,都是基于_序数名字_序数名字是把序数数字用修改后的base-26进行" -"的编码.为了避免将短名字锁定在不可花费的创世区块奖励中,随着序数的_变长_,序数" -"名字将变得_更短_ 比如第一个开采的0号聪的名字是`nvtdijuwxlp`,而最后一个被开采" +"任务3有两个部分,都是基于 _序数名字_,序数名字是把序数数字用修改后的base-26进行" +"的编码.为了避免将短名字锁定在不可花费的创世区块奖励中,随着序数的 _变长_,序数" +"名字将变得 _更短_, 比如第一个开采的0号聪的名字是`nvtdijuwxlp`,而最后一个被开采" "的2,099,999,997,689,999号聪的名字,则是 `a`." #: src/bounty/3.md:14 @@ -6504,15 +7963,15 @@ msgstr "在平局情况下,如果两个提交的出现了相同的频率,则 #: src/bounty/3.md:66 msgid "Part 0: 200,000 sats" -msgstr "" +msgstr "第0部分: 200,000 sats" #: src/bounty/3.md:67 msgid "Part 1: 200,000 sats" -msgstr "" +msgstr "第1部分: 200,000 sats" #: src/bounty/3.md:68 msgid "Total: 400,000 sats" -msgstr "" +msgstr "总量: 400,000 sats" #: src/bounty/3.md:73 msgid "" @@ -6522,7 +7981,7 @@ msgstr "" #: src/bounty/3.md:78 msgid "Unclaimed!" -msgstr "仍然有效!" +msgstr "无人认领!" #~ msgid "" #~ "[Ordinal Art: Mint Your own NFTs on Bitcoin w/ @rodarmor](https://www." diff --git a/docs/src/guides/batch-inscribing.md b/docs/src/guides/batch-inscribing.md index 9ccb539cb0..ca527e0420 100644 --- a/docs/src/guides/batch-inscribing.md +++ b/docs/src/guides/batch-inscribing.md @@ -1,7 +1,7 @@ Batch Inscribing ================ -Multiple inscriptions can be created inscriptions at the same time using the +Multiple inscriptions can be created at the same time using the [pointer field](./../inscriptions/pointer.md). This is especially helpful for collections, or other cases when multiple inscriptions should share the same parent, since the parent can passed into a reveal transaction that creates diff --git a/docs/src/guides/collecting/sparrow-wallet.md b/docs/src/guides/collecting/sparrow-wallet.md index f72206b653..d667c2ef6d 100644 --- a/docs/src/guides/collecting/sparrow-wallet.md +++ b/docs/src/guides/collecting/sparrow-wallet.md @@ -89,9 +89,9 @@ You can then check your wallet's inscriptions using `ord wallet inscriptions` 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: -`ord --wallet ord_from_sparrow wallet restore "BIP39 SEED PHRASE"` +`ord wallet --name ord_from_sparrow wallet restore --from mnemonic` -`ord --wallet ord_from_sparrow wallet inscriptions` +`ord wallet --name ord_from_sparrow wallet inscriptions` `bitcoin-cli -rpcwallet=ord_from_sparrow rescanblockchain 767430` diff --git a/docs/src/guides/explorer.md b/docs/src/guides/explorer.md index 84fdf974d9..0e3f1ea2e2 100644 --- a/docs/src/guides/explorer.md +++ b/docs/src/guides/explorer.md @@ -87,7 +87,6 @@ what is shown in the HTML. These endpoints are: - `/inscriptions/` - `/inscriptions//` - `/output/` -- `/output/` - `/sat/` To get a list of the latest 100 inscriptions you would do: diff --git a/docs/src/guides/sat-hunting.md b/docs/src/guides/sat-hunting.md index 2a30999dad..17ab49ad85 100644 --- a/docs/src/guides/sat-hunting.md +++ b/docs/src/guides/sat-hunting.md @@ -1,11 +1,6 @@ Sat Hunting =========== -*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`.* - 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. @@ -42,10 +37,9 @@ There are a few things you'll need before you start. - Get a copy of `ord` from [the repo](https://github.com/ordinals/ord/). - - Run `RUST_LOG=info ord index`. It should connect to your bitcoin core - node and start indexing. - - - Wait for it to finish indexing. + - Run `ord --index-sats server`. It should connect to your bitcoin core node and start indexing. + + - Once it has finished indexing, leave the server running and submit new `ord` commands in a separate terminal session. 3. Third, you'll need a wallet with UTXOs that you want to search. @@ -243,7 +237,13 @@ button to display the descriptor. ### Transferring Ordinals -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. +The `ord` wallet supports transferring specific satoshis by using the +name of the satoshi. To send the satoshi `zonefruits`, do: + +``` +ord wallet send zonefruits --fee-rate 21 +``` + +You can also use the `bitcoin-cli` commands `createrawtransaction`, +`signrawtransactionwithwallet`, and `sendrawtransaction`, but this +method can be complex and is outside the scope of this guide. diff --git a/docs/src/guides/testing.md b/docs/src/guides/testing.md index 9218c7e1a1..26a13272b4 100644 --- a/docs/src/guides/testing.md +++ b/docs/src/guides/testing.md @@ -57,7 +57,8 @@ can be run with the following network flags: | Signet | `--signet` or `-s` | | Regtest | `--regtest` or `-r` | -Regtest doesn't require downloading the blockchain or indexing ord. +Regtest doesn't require downloading the blockchain since you create your own +private blockchain, so indexing `ord` is almost instantaneous. Example ------- diff --git a/docs/src/inscriptions.md b/docs/src/inscriptions.md index 73f3237924..9e78d311ec 100644 --- a/docs/src/inscriptions.md +++ b/docs/src/inscriptions.md @@ -61,7 +61,7 @@ bytes. The inscription content is contained within the input of a reveal 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 +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. Content @@ -150,6 +150,26 @@ This is accomplished by loading HTML and SVG inscriptions inside `iframes` with the `sandbox` attribute, as well as serving inscription content with `Content-Security-Policy` headers. +Self-Reference +-------------- + +The content of the inscription with ID `INSCRIPTION_ID` must served from the +URL path `/content/`. + +This allows inscriptions to retrieve their own inscription ID with: + +```js +let inscription_id = window.location.pathname.split("/").pop(); +``` + +If an inscription with ID X delegates to an inscription with ID Y, that is to +say, if inscription X contains a delegate field with value Y, the content of +inscription X must be served from the URL path `/content/X`, *not* +`/content/Y`. + +This allows delegating inscriptions to use their own inscription ID as a seed +for generative delegate content. + Reinscriptions -------------- diff --git a/docs/src/inscriptions/delegate.md b/docs/src/inscriptions/delegate.md index 534173be0d..545d7a3448 100644 --- a/docs/src/inscriptions/delegate.md +++ b/docs/src/inscriptions/delegate.md @@ -2,8 +2,9 @@ Delegate ======== 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. +an inscription with a delegate will instead return the content, content type +and content encoding of the delegate. This can be used to cheaply create copies +of an inscription. ### Specification diff --git a/docs/src/inscriptions/recursion.md b/docs/src/inscriptions/recursion.md index d5839ba8f9..69365a19b1 100644 --- a/docs/src/inscriptions/recursion.md +++ b/docs/src/inscriptions/recursion.md @@ -40,7 +40,7 @@ The recursive endpoints are: - `/r/blocktime`: UNIX time stamp of latest block. - `/r/children/`: the first 100 child inscription ids. - `/r/children//`: the set of 100 child inscription ids on ``. -- `/r/inscription/:inscription_id`: information about an inscription +- `/r/inscription/`: information about an inscription - `/r/metadata/`: JSON string containing the hex-encoded CBOR metadata. - `/r/sat/`: the first 100 inscription ids on a sat. - `/r/sat//`: the set of 100 inscription ids on ``. @@ -130,7 +130,7 @@ Examples } ``` -- `r/inscription/3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0` +- `/r/inscription/3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36i0` ```json { diff --git a/docs/src/runes.md b/docs/src/runes.md index 31b21cf4f9..904bf757a3 100644 --- a/docs/src/runes.md +++ b/docs/src/runes.md @@ -27,7 +27,7 @@ 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 +in text as `BLOCK:TX`. For example, the ID of the rune etched in the 20th transaction of the 500th block is `500:20`. Etching @@ -38,8 +38,8 @@ 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 consist of the letters A through Z and are between one and twenty-six +letters 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`. @@ -48,6 +48,9 @@ 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. +Spacers can only be placed between two letters. Finally, spacers do not +count towards the letter count. + ### Divisibility A rune's divisibility is how finely it may be divided into its atomic units. diff --git a/docs/src/runes/specification.md b/docs/src/runes/specification.md index b247790bf2..d3b9b36354 100644 --- a/docs/src/runes/specification.md +++ b/docs/src/runes/specification.md @@ -114,9 +114,12 @@ 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. +The payload buffer is assembled by concatenating data pushes, after `OP_13`, in +the matching script pubkey. + +Data pushes are opcodes 0 through 78 inclusive. If a non-data push opcode is +encountered, i.e., any opcode equal to or greater than opcode 79, the +deciphered runestone is a cenotaph with no etching, mint, or edicts. #### Decoding the Integer Sequence @@ -260,6 +263,7 @@ FLAG_VALUE`: enum Flag { Etching = 0, Terms = 1, + Turbo = 2, Cenotaph = 127, } ``` @@ -268,6 +272,10 @@ The `Etching` flag marks this transaction as containing an etching. The `Terms` flag marks this transaction's etching as having open mint terms. +The `Turbo` flag marks this transaction's etching as opting into future +protocol changes. These protocol changes may increase light client validation +costs, or just be highly degenerate. + The `Cenotaph` flag is unrecognized. If the value of the flags field after removing recognized flags is nonzero, the diff --git a/rustfmt.toml b/rustfmt.toml index 84ec2a761f..41464169c4 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -edition = "2018" +edition = "2021" max_width = 100 newline_style = "Unix" tab_spaces = 2 diff --git a/src/api.rs b/src/api.rs index 9093465334..f148066b93 100644 --- a/src/api.rs +++ b/src/api.rs @@ -10,11 +10,12 @@ pub use crate::templates::{ #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Block { - pub hash: BlockHash, - pub target: BlockHash, pub best_height: u32, + pub hash: BlockHash, pub height: u32, pub inscriptions: Vec, + pub runes: Vec, + pub target: BlockHash, } impl Block { @@ -23,6 +24,7 @@ impl Block { height: Height, best_height: Height, inscriptions: Vec, + runes: Vec, ) -> Self { Self { hash: block.header.block_hash(), @@ -30,6 +32,7 @@ impl Block { height: height.0, best_height: best_height.0, inscriptions, + runes, } } } @@ -129,7 +132,7 @@ pub struct Output { pub address: Option>, pub indexed: bool, pub inscriptions: Vec, - pub runes: Vec<(SpacedRune, Pile)>, + pub runes: BTreeMap, pub sat_ranges: Option>, pub script_pubkey: String, pub spent: bool, @@ -142,25 +145,25 @@ impl Output { chain: Chain, inscriptions: Vec, outpoint: OutPoint, - output: TxOut, + tx_out: TxOut, indexed: bool, - runes: Vec<(SpacedRune, Pile)>, + runes: BTreeMap, sat_ranges: Option>, spent: bool, ) -> Self { Self { address: chain - .address_from_script(&output.script_pubkey) + .address_from_script(&tx_out.script_pubkey) .ok() .map(|address| uncheck(&address)), indexed, inscriptions, runes, sat_ranges, - script_pubkey: output.script_pubkey.to_asm_string(), + script_pubkey: tx_out.script_pubkey.to_asm_string(), spent, transaction: outpoint.txid.to_string(), - value: output.value, + value: tx_out.value, } } } diff --git a/src/decimal.rs b/src/decimal.rs index 91cb86b6ee..226ef65390 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -2,8 +2,8 @@ use super::*; #[derive(Debug, PartialEq, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)] pub struct Decimal { - value: u128, - scale: u8, + pub value: u128, + pub scale: u8, } impl Decimal { @@ -26,7 +26,7 @@ impl Decimal { impl Display for Decimal { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let magnitude = 10u128.pow(self.scale.into()); + let magnitude = 10u128.checked_pow(self.scale.into()).ok_or(fmt::Error)?; let integer = self.value / magnitude; let mut fraction = self.value % magnitude; @@ -68,7 +68,10 @@ impl FromStr for Decimal { } else { let trailing_zeros = decimal.chars().rev().take_while(|c| *c == '0').count(); let significant_digits = decimal.chars().count() - trailing_zeros; - let decimal = decimal.parse::()? / 10u128.pow(u32::try_from(trailing_zeros).unwrap()); + let decimal = decimal.parse::()? + / 10u128 + .checked_pow(u32::try_from(trailing_zeros).unwrap()) + .context("excessive trailing zeros")?; (decimal, u8::try_from(significant_digits).unwrap()) }; diff --git a/src/index.rs b/src/index.rs index bec84d0a25..9618e9725e 100644 --- a/src/index.rs +++ b/src/index.rs @@ -48,7 +48,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 24; +const SCHEMA_VERSION: u64 = 25; define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, &SatPointValue, u32 } define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 } @@ -157,19 +157,6 @@ pub(crate) struct TransactionInfo { pub(crate) starting_timestamp: u128, } -pub(crate) struct InscriptionInfo { - pub(crate) children: Vec, - pub(crate) entry: InscriptionEntry, - pub(crate) parents: Vec, - pub(crate) output: Option, - pub(crate) satpoint: SatPoint, - pub(crate) inscription: Inscription, - pub(crate) previous: Option, - pub(crate) next: Option, - pub(crate) rune: Option, - pub(crate) charms: u16, -} - pub(crate) trait BitcoinCoreRpcResultExt { fn into_option(self) -> Result>; } @@ -399,6 +386,7 @@ impl Index { spaced_rune: SpacedRune { rune, spacers: 128 }, symbol: Some('\u{29C9}'), timestamp: 0, + turbo: true, } .store(), )?; @@ -867,6 +855,23 @@ impl Index { ) } + pub(crate) fn get_rune_by_number(&self, number: usize) -> Result> { + match self + .database + .begin_read()? + .open_table(RUNE_ID_TO_RUNE_ENTRY)? + .iter()? + .nth(number) + { + Some(result) => { + let rune_result = + result.map(|(_id, entry)| RuneEntry::load(entry.value()).spaced_rune.rune); + Ok(rune_result.ok()) + } + None => Ok(None), + } + } + pub(crate) fn rune( &self, rune: Rune, @@ -919,31 +924,56 @@ impl Index { Ok(entries) } + pub(crate) fn runes_paginated( + &self, + page_size: usize, + page_index: usize, + ) -> Result<(Vec<(RuneId, RuneEntry)>, bool)> { + let mut entries = Vec::new(); + + for result in self + .database + .begin_read()? + .open_table(RUNE_ID_TO_RUNE_ENTRY)? + .iter()? + .rev() + .skip(page_index.saturating_mul(page_size)) + .take(page_size.saturating_add(1)) + { + let (id, entry) = result?; + entries.push((RuneId::load(id.value()), RuneEntry::load(entry.value()))); + } + + let more = entries.len() > page_size; + + Ok((entries, more)) + } + 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)> { + pub(crate) fn decode_rune_balance(buffer: &[u8]) -> Result<((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()?, + block: block.try_into()?, + tx: tx.try_into()?, }; let (balance, balance_len) = varint::decode(&buffer[len..])?; len += balance_len; - Some(((id, balance), len)) + Ok(((id, balance), len)) } pub(crate) fn get_rune_balances_for_outpoint( &self, outpoint: OutPoint, - ) -> Result> { + ) -> Result> { let rtx = self.database.begin_read()?; let outpoint_to_balances = rtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; @@ -951,12 +981,12 @@ impl Index { let id_to_rune_entries = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; let Some(balances) = outpoint_to_balances.get(&outpoint.store())? else { - return Ok(Vec::new()); + return Ok(BTreeMap::new()); }; let balances_buffer = balances.value(); - let mut balances = Vec::new(); + let mut balances = BTreeMap::new(); let mut i = 0; while i < balances_buffer.len() { let ((id, amount), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap(); @@ -964,14 +994,14 @@ impl Index { let entry = RuneEntry::load(id_to_rune_entries.get(id.store())?.unwrap().value()); - balances.push(( + balances.insert( entry.spaced_rune, Pile { amount, divisibility: entry.divisibility, symbol: entry.symbol, }, - )); + ); } Ok(balances) @@ -1741,6 +1771,29 @@ impl Index { .collect::>>() } + pub(crate) fn get_runes_in_block(&self, block_height: u64) -> Result> { + let rtx = self.database.begin_read()?; + + let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; + + let min_id = RuneId { + block: block_height, + tx: 0, + }; + + let max_id = RuneId { + block: block_height, + tx: u32::MAX, + }; + + let runes = rune_id_to_rune_entry + .range(min_id.store()..=max_id.store())? + .map(|result| result.map(|(_, entry)| RuneEntry::load(entry.value()).spaced_rune)) + .collect::, StorageError>>()?; + + Ok(runes) + } + pub(crate) fn get_highest_paying_inscriptions_in_block( &self, block_height: u32, @@ -1801,15 +1854,11 @@ impl Index { ) } - pub fn inscription_info_benchmark(index: &Index, inscription_number: i32) { - Self::inscription_info(index, query::Inscription::Number(inscription_number)).unwrap(); - } - pub(crate) fn inscription_info( - index: &Index, + &self, query: query::Inscription, - ) -> Result> { - let rtx = index.database.begin_read()?; + ) -> Result, Inscription)>> { + let rtx = self.database.begin_read()?; let sequence_number = match query { query::Inscription::Id(id) => rtx @@ -1842,7 +1891,7 @@ impl Index { .value(), ); - let Some(transaction) = index.get_transaction(entry.id.txid)? else { + let Some(transaction) = self.get_transaction(entry.id.txid)? else { return Ok(None); }; @@ -1866,7 +1915,7 @@ impl Index { { None } else { - let Some(transaction) = index.get_transaction(satpoint.outpoint.txid)? else { + let Some(transaction) = self.get_transaction(satpoint.outpoint.txid)? else { return Ok(None); }; @@ -1943,18 +1992,53 @@ impl Index { Charm::Lost.set(&mut charms); } - Ok(Some(InscriptionInfo { - children, - entry, - parents, + let effective_mime_type = if let Some(delegate_id) = inscription.delegate() { + let delegate_result = self.get_inscription_by_id(delegate_id); + if let Ok(Some(delegate)) = delegate_result { + delegate.content_type().map(str::to_string) + } else { + inscription.content_type().map(str::to_string) + } + } else { + inscription.content_type().map(str::to_string) + }; + + Ok(Some(( + api::Inscription { + address: output + .as_ref() + .and_then(|o| { + self + .settings + .chain() + .address_from_script(&o.script_pubkey) + .ok() + }) + .map(|address| address.to_string()), + charms: Charm::charms(charms), + children, + content_encoding: inscription.content_encoding_str().map(|s| s.to_string()), + content_length: inscription.content_length(), + content_type: inscription.content_type().map(|s| s.to_string()), + delegate: inscription.delegate(), + effective_content_type: effective_mime_type, + fee: entry.fee, + height: entry.height, + id: entry.id, + inscription_sequence: entry.sequence_number, + next, + number: entry.inscription_number, + parents, + previous, + rune, + sat: entry.sat, + satpoint, + timestamp: timestamp(entry.timestamp.into()).timestamp(), + value: output.as_ref().map(|o| o.value), + }, output, - satpoint, inscription, - previous, - next, - rune, - charms, - })) + ))) } pub(crate) fn get_inscription_entry( @@ -2104,6 +2188,61 @@ impl Index { .collect(), ) } + + pub(crate) fn get_output_info(&self, outpoint: OutPoint) -> Result> { + let sat_ranges = self.list(outpoint)?; + + let indexed; + + let txout = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { + let mut value = 0; + + if let Some(ranges) = &sat_ranges { + for (start, end) in ranges { + value += end - start; + } + } + + indexed = true; + + TxOut { + value, + script_pubkey: ScriptBuf::new(), + } + } else { + indexed = self.contains_output(&outpoint)?; + + let Some(tx) = self.get_transaction(outpoint.txid)? else { + return Ok(None); + }; + + let Some(output) = tx.output.into_iter().nth(outpoint.vout as usize) else { + return Ok(None); + }; + + output + }; + + let inscriptions = self.get_inscriptions_on_output(outpoint)?; + + let runes = self.get_rune_balances_for_outpoint(outpoint)?; + + let spent = self.is_output_spent(outpoint)?; + + Ok(Some(( + api::Output::new( + self.settings.chain(), + inscriptions, + outpoint, + txout.clone(), + indexed, + runes, + sat_ranges, + spent, + ), + txout, + ))) + } } #[cfg(test)] @@ -6160,7 +6299,7 @@ mod tests { } #[test] - fn event_sender_channel() { + fn inscription_event_sender_channel() { let (event_sender, mut event_receiver) = tokio::sync::mpsc::channel(1024); let context = Context::builder().event_sender(event_sender).build(); @@ -6234,4 +6373,241 @@ mod tests { } ); } + + #[test] + fn rune_event_sender_channel() { + const RUNE: u128 = 99246114928149462; + + let (event_sender, mut event_receiver) = tokio::sync::mpsc::channel(1024); + let context = Context::builder() + .arg("--index-runes") + .event_sender(event_sender) + .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, + ); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mints: 0, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() + }), + ..default() + }, + )], + [], + ); + + assert_eq!( + event_receiver.blocking_recv().unwrap(), + Event::RuneEtched { + block_height: 8, + txid: txid0, + rune_id: id, + } + ); + + let txid1 = 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); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() + }), + mints: 1, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() + }, + )], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + + assert_eq!( + event_receiver.blocking_recv().unwrap(), + Event::RuneMinted { + block_height: 9, + txid: txid1, + rune_id: id, + amount: 1000, + } + ); + + let txid2 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(9, 1, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id, + amount: 1000, + output: 0, + }], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + block: 8, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + ..default() + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..Default::default() + }), + timestamp: 8, + mints: 1, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid2, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + + event_receiver.blocking_recv().unwrap(); + + pretty_assert_eq!( + event_receiver.blocking_recv().unwrap(), + Event::RuneTransferred { + block_height: 10, + txid: txid2, + rune_id: id, + amount: 1000, + outpoint: OutPoint { + txid: txid2, + vout: 0, + }, + } + ); + + let txid3 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(10, 1, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id, + amount: 111, + output: 0, + }], + ..Default::default() + } + .encipher(), + ), + op_return_index: Some(0), + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + block: 8, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + ..default() + }, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..Default::default() + }), + timestamp: 8, + mints: 1, + burned: 111, + ..Default::default() + }, + )], + [( + OutPoint { + txid: txid3, + vout: 1, + }, + vec![(id, 889)], + )], + ); + + event_receiver.blocking_recv().unwrap(); + + pretty_assert_eq!( + event_receiver.blocking_recv().unwrap(), + Event::RuneBurned { + block_height: 11, + txid: txid3, + amount: 111, + rune_id: id, + } + ); + } } diff --git a/src/index/entry.rs b/src/index/entry.rs index f3cae51f22..faa3d2343a 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -53,6 +53,7 @@ pub struct RuneEntry { pub symbol: Option, pub terms: Option, pub timestamp: u64, + pub turbo: bool, } impl RuneEntry { @@ -153,6 +154,7 @@ pub(super) type RuneEntryValue = ( Option, // symbol Option, // terms u64, // timestamp + bool, // turbo ); impl Default for RuneEntry { @@ -169,6 +171,7 @@ impl Default for RuneEntry { symbol: None, terms: None, timestamp: 0, + turbo: false, } } } @@ -189,6 +192,7 @@ impl Entry for RuneEntry { symbol, terms, timestamp, + turbo, ): RuneEntryValue, ) -> Self { Self { @@ -220,6 +224,7 @@ impl Entry for RuneEntry { offset, }), timestamp, + turbo, } } @@ -255,6 +260,7 @@ impl Entry for RuneEntry { }| (cap, height, amount, offset), ), self.timestamp, + self.turbo, ) } } @@ -572,6 +578,7 @@ mod tests { }, symbol: Some('a'), timestamp: 10, + turbo: true, }; let value = ( @@ -589,6 +596,7 @@ mod tests { Some('a'), Some((Some(1), (Some(2), Some(3)), Some(4), (Some(5), Some(6)))), 10, + true, ); assert_eq!(entry.store(), value); diff --git a/src/index/event.rs b/src/index/event.rs index 997a70ebb4..96e1b41f99 100644 --- a/src/index/event.rs +++ b/src/index/event.rs @@ -1,4 +1,4 @@ -use crate::{InscriptionId, SatPoint}; +use super::*; #[derive(Debug, Clone, PartialEq)] pub enum Event { @@ -17,4 +17,28 @@ pub enum Event { old_location: SatPoint, sequence_number: u32, }, + RuneBurned { + amount: u128, + block_height: u32, + rune_id: RuneId, + txid: Txid, + }, + RuneEtched { + block_height: u32, + rune_id: RuneId, + txid: Txid, + }, + RuneMinted { + amount: u128, + block_height: u32, + rune_id: RuneId, + txid: Txid, + }, + RuneTransferred { + amount: u128, + block_height: u32, + outpoint: OutPoint, + rune_id: RuneId, + txid: Txid, + }, } diff --git a/src/index/lot.rs b/src/index/lot.rs index 0305c6e6f0..08214a65dd 100644 --- a/src/index/lot.rs +++ b/src/index/lot.rs @@ -7,7 +7,7 @@ use { }; #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Default, Serialize, Deserialize)] -pub(super) struct Lot(pub(super) u128); +pub struct Lot(pub u128); impl Lot { #[cfg(test)] diff --git a/src/index/testing.rs b/src/index/testing.rs index 6513b9c5ab..ae4f02a823 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -161,7 +161,7 @@ impl Context { ..default() }); - self.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + self.mine_blocks(Runestone::COMMIT_CONFIRMATIONS.into()); let mut witness = Witness::new(); @@ -197,7 +197,8 @@ impl Context { ( txid, RuneId { - block: u64::try_from(block_count + usize::from(Runestone::COMMIT_INTERVAL) + 1).unwrap(), + block: u64::try_from(block_count + usize::from(Runestone::COMMIT_CONFIRMATIONS) + 1) + .unwrap(), tx: 1, }, ) diff --git a/src/index/updater.rs b/src/index/updater.rs index 694934b04d..a511f89607 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -250,7 +250,7 @@ impl<'index> Updater<'index> { // Default rpcworkqueue in bitcoind is 16, meaning more than 16 concurrent requests will be rejected. // Since we are already requesting blocks on a separate thread, and we don't want to break if anything // else runs a request, we keep this to 12. - const PARALLEL_REQUESTS: usize = 12; + let parallel_requests: usize = settings.bitcoin_rpc_limit() as usize; thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() @@ -273,8 +273,8 @@ impl<'index> Updater<'index> { outpoints.push(outpoint); } // Break outpoints into chunks for parallel requests - let chunk_size = (outpoints.len() / PARALLEL_REQUESTS) + 1; - let mut futs = Vec::with_capacity(PARALLEL_REQUESTS); + let chunk_size = (outpoints.len() / parallel_requests) + 1; + let mut futs = Vec::with_capacity(parallel_requests); for chunk in outpoints.chunks(chunk_size) { let txids = chunk.iter().map(|outpoint| outpoint.txid).collect(); let fut = fetcher.get_transactions(txids); @@ -591,6 +591,7 @@ impl<'index> Updater<'index> { .unwrap_or(0); let mut rune_updater = RuneUpdater { + event_sender: self.index.event_sender.as_ref(), block_time: block.header.time, burned: HashMap::new(), client: &self.index.client, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 41dd42c538..c8ab5ebec5 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -215,7 +215,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { hidden: inscription.payload.hidden(), parents: inscription.payload.parents(), pointer: inscription.payload.pointer(), - reinscription: inscribed_offsets.get(&offset).is_some(), + reinscription: inscribed_offsets.contains_key(&offset), unbound: current_input_value == 0 || curse == Some(Curse::UnrecognizedEvenField) || inscription.payload.unrecognized_even_field, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 744ff587b9..cce4ae552c 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -4,6 +4,7 @@ pub(super) struct RuneUpdater<'a, 'tx, 'client> { pub(super) block_time: u32, pub(super) burned: HashMap, pub(super) client: &'client Client, + pub(super) event_sender: Option<&'a Sender>, pub(super) height: u32, pub(super) id_to_entry: &'a mut Table<'tx, RuneIdValue, RuneEntryValue>, pub(super) inscription_id_to_sequence_number: &'a Table<'tx, InscriptionIdValue, u32>, @@ -28,6 +29,15 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { if let Some(id) = artifact.mint() { if let Some(amount) = self.mint(id)? { *unallocated.entry(id).or_default() += amount; + + if let Some(sender) = self.event_sender { + sender.blocking_send(Event::RuneMinted { + block_height: self.height, + txid, + rune_id: id, + amount: amount.n(), + })?; + } } } @@ -79,22 +89,24 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { }) .collect::>(); - if amount == 0 { - // if amount is zero, divide balance between eligible outputs - let amount = *balance / destinations.len() as u128; - let remainder = usize::try_from(*balance % destinations.len() as u128).unwrap(); - - for (i, output) in destinations.iter().enumerate() { - allocate( - balance, - if i < remainder { amount + 1 } else { amount }, - *output, - ); - } - } else { - // if amount is non-zero, distribute amount to eligible outputs - for output in destinations { - allocate(balance, amount.min(*balance), output); + if !destinations.is_empty() { + if amount == 0 { + // if amount is zero, divide balance between eligible outputs + let amount = *balance / destinations.len() as u128; + let remainder = usize::try_from(*balance % destinations.len() as u128).unwrap(); + + for (i, output) in destinations.iter().enumerate() { + allocate( + balance, + if i < remainder { amount + 1 } else { amount }, + *output, + ); + } + } else { + // if amount is non-zero, distribute amount to eligible outputs + for output in destinations { + allocate(balance, amount.min(*balance), output); + } } } } else { @@ -130,8 +142,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .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 + // OP_RETURN output if there is no default if let Some(vout) = pointer .map(|pointer| pointer.into_usize()) .inspect(|&pointer| assert!(pointer < allocated.len())) @@ -179,23 +190,42 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { // Sort balances by id so tests can assert balances in a fixed order balances.sort(); + let outpoint = OutPoint { + txid, + vout: vout.try_into().unwrap(), + }; + for (id, balance) in balances { Index::encode_rune_balance(id, balance.n(), &mut buffer); - } - self.outpoint_to_balances.insert( - &OutPoint { - txid, - vout: vout.try_into().unwrap(), + if let Some(sender) = self.event_sender { + sender.blocking_send(Event::RuneTransferred { + outpoint, + block_height: self.height, + txid, + rune_id: id, + amount: balance.0, + })?; } - .store(), - buffer.as_slice(), - )?; + } + + self + .outpoint_to_balances + .insert(&outpoint.store(), buffer.as_slice())?; } // increment entries with burned runes for (id, amount) in burned { *self.burned.entry(id).or_default() += amount; + + if let Some(sender) = self.event_sender { + sender.blocking_send(Event::RuneBurned { + block_height: self.height, + txid, + rune_id: id, + amount: amount.n(), + })?; + } } Ok(()) @@ -243,6 +273,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { spaced_rune: SpacedRune { rune, spacers: 0 }, symbol: None, timestamp: self.block_time.into(), + turbo: false, }, Artifact::Runestone(Runestone { etching, .. }) => { let Etching { @@ -251,6 +282,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { premine, spacers, symbol, + turbo, .. } = etching.unwrap(); @@ -269,12 +301,21 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { }, symbol, timestamp: self.block_time.into(), + turbo, } } }; self.id_to_entry.insert(id.store(), entry.store())?; + if let Some(sender) = self.event_sender { + sender.blocking_send(Event::RuneEtched { + block_height: self.height, + txid, + rune_id: id, + })?; + } + let inscription_id = InscriptionId { txid, index: 0 }; if let Some(sequence_number) = self @@ -388,7 +429,10 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .get_raw_transaction_info(&input.previous_output.txid, None) .into_option()? else { - panic!("input not in UTXO set: {}", input.previous_output); + panic!( + "can't get input transaction: {}", + input.previous_output.txid + ); }; let taproot = tx_info.vout[input.previous_output.vout.into_usize()] @@ -396,12 +440,24 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .script()? .is_v1_p2tr(); - let mature = tx_info - .confirmations - .map(|confirmations| confirmations >= Runestone::COMMIT_INTERVAL.into()) - .unwrap_or_default(); + if !taproot { + continue; + } + + let commit_tx_height = self + .client + .get_block_header_info(&tx_info.blockhash.unwrap()) + .into_option()? + .unwrap() + .height; + + let confirmations = self + .height + .checked_sub(commit_tx_height.try_into().unwrap()) + .unwrap() + + 1; - if taproot && mature { + if confirmations >= Runestone::COMMIT_CONFIRMATIONS.into() { return Ok(true); } } diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index 5bc3014a43..81b1032d30 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -25,79 +25,77 @@ pub struct Inscription { } impl Inscription { - #[cfg(test)] - pub(crate) fn new(content_type: Option>, body: Option>) -> Self { - Self { - content_type, - body, - ..default() - } - } - - pub(crate) fn from_file( + pub fn new( chain: Chain, compress: bool, delegate: Option, metadata: Option>, metaprotocol: Option, parents: Vec, - path: impl AsRef, + path: Option, pointer: Option, rune: Option, ) -> Result { let path = path.as_ref(); - let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?; + let (body, content_type, content_encoding) = if let Some(path) = path { + let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?; - let (content_type, compression_mode) = Media::content_type_for_path(path)?; + let content_type = Media::content_type_for_path(path)?.0; - let (body, content_encoding) = if compress { - let mut compressed = Vec::new(); + let (body, content_encoding) = if compress { + let compression_mode = Media::content_type_for_path(path)?.1; + let mut compressed = Vec::new(); - { - CompressorWriter::with_params( - &mut compressed, - body.len(), - &BrotliEncoderParams { - lgblock: 24, - lgwin: 24, - mode: compression_mode, - quality: 11, - size_hint: body.len(), - ..default() - }, - ) - .write_all(&body)?; + { + CompressorWriter::with_params( + &mut compressed, + body.len(), + &BrotliEncoderParams { + lgblock: 24, + lgwin: 24, + mode: compression_mode, + quality: 11, + size_hint: body.len(), + ..default() + }, + ) + .write_all(&body)?; - let mut decompressor = brotli::Decompressor::new(compressed.as_slice(), compressed.len()); + let mut decompressor = brotli::Decompressor::new(compressed.as_slice(), compressed.len()); - let mut decompressed = Vec::new(); + let mut decompressed = Vec::new(); - decompressor.read_to_end(&mut decompressed)?; + decompressor.read_to_end(&mut decompressed)?; - ensure!(decompressed == body, "decompression roundtrip failed"); - } + ensure!(decompressed == body, "decompression roundtrip failed"); + } - if compressed.len() < body.len() { - (compressed, Some("br".as_bytes().to_vec())) + if compressed.len() < body.len() { + (compressed, Some("br".as_bytes().to_vec())) + } else { + (body, None) + } } else { (body, None) + }; + + if let Some(limit) = chain.inscription_content_size_limit() { + let len = body.len(); + if len > limit { + bail!("content size of {len} bytes exceeds {limit} byte limit for {chain} inscriptions"); + } } + + (Some(body), Some(content_type), content_encoding) } else { - (body, None) + (None, None, None) }; - if let Some(limit) = chain.inscription_content_size_limit() { - let len = body.len(); - if len > limit { - bail!("content size of {len} bytes exceeds {limit} byte limit for {chain} inscriptions"); - } - } - Ok(Self { - body: Some(body), + body, content_encoding, - content_type: Some(content_type.into()), + content_type: content_type.map(|content_type| content_type.into()), delegate: delegate.map(|delegate| delegate.value()), metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), @@ -777,14 +775,14 @@ mod tests { write!(file, "foo").unwrap(); - let inscription = Inscription::from_file( + let inscription = Inscription::new( Chain::Mainnet, false, None, None, None, Vec::new(), - file.path(), + Some(file.path().to_path_buf()), None, None, ) @@ -792,14 +790,14 @@ mod tests { assert_eq!(inscription.pointer, None); - let inscription = Inscription::from_file( + let inscription = Inscription::new( Chain::Mainnet, false, None, None, None, Vec::new(), - file.path(), + Some(file.path().to_path_buf()), Some(0), None, ) @@ -807,14 +805,14 @@ mod tests { assert_eq!(inscription.pointer, Some(Vec::new())); - let inscription = Inscription::from_file( + let inscription = Inscription::new( Chain::Mainnet, false, None, None, None, Vec::new(), - file.path(), + Some(file.path().to_path_buf()), Some(1), None, ) @@ -822,14 +820,14 @@ mod tests { assert_eq!(inscription.pointer, Some(vec![1])); - let inscription = Inscription::from_file( + let inscription = Inscription::new( Chain::Mainnet, false, None, None, None, Vec::new(), - file.path(), + Some(file.path().to_path_buf()), Some(256), None, ) diff --git a/src/inscriptions/media.rs b/src/inscriptions/media.rs index de41de2571..d5ef8e8f1a 100644 --- a/src/inscriptions/media.rs +++ b/src/inscriptions/media.rs @@ -99,7 +99,7 @@ impl Media { ("text/css", TEXT, Code(Css), &["css"]), ("text/html", TEXT, Iframe, &[]), ("text/html;charset=utf-8", TEXT, Iframe, &["html"]), - ("text/javascript", TEXT, Code(JavaScript), &["js"]), + ("text/javascript", TEXT, Code(JavaScript), &["js", "mjs"]), ("text/markdown", TEXT, Markdown, &[]), ("text/markdown;charset=utf-8", TEXT, Markdown, &["md"]), ("text/plain", TEXT, Text, &[]), diff --git a/src/lib.rs b/src/lib.rs index 1725bd5d70..2fb5899463 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ use { into_usize::IntoUsize, representation::Representation, settings::Settings, - subcommand::{Subcommand, SubcommandResult}, + subcommand::{OutputFormat, Subcommand, SubcommandResult}, tally::Tally, }, anyhow::{anyhow, bail, ensure, Context, Error}, @@ -102,7 +102,7 @@ pub mod api; pub mod arguments; mod blocktime; pub mod chain; -mod decimal; +pub mod decimal; mod deserialize_from_str; mod fee_rate; pub mod index; @@ -256,7 +256,7 @@ pub fn main() { let args = Arguments::parse(); - let minify = args.options.minify; + let format = args.options.format; match args.run() { Err(err) => { @@ -278,7 +278,7 @@ pub fn main() { } Ok(output) => { if let Some(output) = output { - output.print_json(minify); + output.print(format.unwrap_or_default()); } gracefully_shutdown_indexer(); } diff --git a/src/options.rs b/src/options.rs index b77002d6d6..6c3c7f2826 100644 --- a/src/options.rs +++ b/src/options.rs @@ -21,6 +21,8 @@ pub struct Options { help = "Authenticate to Bitcoin Core RPC as ." )] pub(crate) bitcoin_rpc_username: Option, + #[arg(long, help = "Max requests in flight. [default: 12]")] + pub(crate) bitcoin_rpc_limit: Option, #[arg(long = "chain", value_enum, help = "Use . [default: mainnet]")] pub(crate) chain_argument: Option, #[arg( @@ -63,8 +65,8 @@ pub struct Options { pub(crate) index_transactions: bool, #[arg(long, help = "Run in integration test mode.")] pub(crate) integration_test: bool, - #[arg(long, help = "Minify JSON output.")] - pub(crate) minify: bool, + #[clap(long, short, long, help = "Specify output format. [default: json]")] + pub(crate) format: Option, #[arg( long, short, diff --git a/src/re.rs b/src/re.rs index ad2d50288b..b8ece332e6 100644 --- a/src/re.rs +++ b/src/re.rs @@ -10,6 +10,7 @@ lazy_static! { 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 RUNE_NUMBER: Regex = re(r"-?[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•.]+"); diff --git a/src/runes.rs b/src/runes.rs index 3919273fb3..aea423d8f8 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -147,7 +147,7 @@ mod tests { fn runes_must_be_greater_than_or_equal_to_minimum_for_height() { let minimum = Rune::minimum_at_height( Chain::Regtest.network(), - Height((Runestone::COMMIT_INTERVAL + 2).into()), + Height((Runestone::COMMIT_CONFIRMATIONS + 2).into()), ) .0; @@ -843,6 +843,7 @@ mod tests { divisibility: Some(1), symbol: Some('$'), spacers: Some(1), + turbo: true, }), pointer: Some(10), ..default() @@ -868,6 +869,7 @@ mod tests { }, symbol: None, timestamp: id.block, + turbo: false, }, )], [], @@ -3907,6 +3909,100 @@ mod tests { ); } + #[test] + fn rune_cannot_be_minted_less_than_limit_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, + ); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mints: 0, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() + }), + ..default() + }, + )], + [], + ); + + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + mint: Some(id), + edicts: vec![Edict { + id, + amount: 111, + output: 0, + }], + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + terms: Some(Terms { + amount: Some(1000), + cap: Some(100), + ..default() + }), + mints: 1, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: 0, + timestamp: id.block, + ..default() + }, + )], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + } + #[test] fn etching_with_amount_can_be_minted() { let context = Context::builder().arg("--index-runes").build(); @@ -4319,7 +4415,368 @@ mod tests { 1, ); - let mut entry = RuneEntry { + 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); + + context.assert_runes([(id, entry)], []); + + let txid1 = context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + op_return: Some( + Runestone { + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + entry.mints += 1; + + context.assert_runes( + [(id, entry)], + [( + 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() + }; + + context.assert_runes([(id, entry)], []); + + let txid1 = 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); + + entry.mints += 1; + + context.assert_runes( + [(id, entry)], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + op_return: Some( + Runestone { + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [(id, entry)], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + } + + #[test] + fn open_mints_must_be_ended_with_etched_height_plus_offset_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(100)), + offset: (None, Some(2)), + }), + ..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(100)), + offset: (None, Some(2)), + cap: Some(100), + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); + + let txid1 = 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); + entry.mints += 1; + + context.assert_runes( + [(id, entry)], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + op_return: Some( + Runestone { + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [(id, entry)], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + } + + #[test] + fn open_mints_must_be_ended_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)), + offset: (None, Some(100)), + }), + ..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)), + offset: (None, Some(100)), + cap: Some(100), + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); + + let txid1 = 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); + entry.mints += 1; + + context.assert_runes( + [(id, entry)], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + op_return: Some( + Runestone { + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [(id, entry)], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + } + + #[test] + fn open_mints_must_be_started_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(11), None), + offset: (Some(1), None), + }), + ..default() + }), + ..default() + }, + 1, + ); + + let mut entry0 = RuneEntry { block: id.block, etching: txid0, spaced_rune: SpacedRune { @@ -4328,15 +4785,15 @@ mod tests { }, terms: Some(Terms { amount: Some(1000), - height: (Some(10), None), + height: (Some(11), None), + offset: (Some(1), None), cap: Some(100), - ..default() }), timestamp: id.block, ..default() }; - context.assert_runes([(id, entry)], []); + context.mine_blocks(1); context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], @@ -4352,7 +4809,9 @@ mod tests { context.mine_blocks(1); - context.assert_runes([(id, entry)], []); + context.assert_runes([(id, entry0)], []); + + context.mine_blocks(1); let txid1 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0, Witness::new())], @@ -4368,10 +4827,10 @@ mod tests { context.mine_blocks(1); - entry.mints += 1; + entry0.mints += 1; context.assert_runes( - [(id, entry)], + [(id, entry0)], [( OutPoint { txid: txid1, @@ -4383,7 +4842,7 @@ mod tests { } #[test] - fn open_mints_can_be_limited_with_height_end() { + fn open_mints_must_be_started_with_etched_height_plus_offset_start() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -4393,8 +4852,8 @@ mod tests { terms: Some(Terms { amount: Some(1000), cap: Some(100), - height: (None, Some(10)), - ..default() + height: (Some(9), None), + offset: (Some(3), None), }), ..default() }), @@ -4412,17 +4871,17 @@ mod tests { }, terms: Some(Terms { amount: Some(1000), - height: (None, Some(10)), + height: (Some(9), None), + offset: (Some(3), None), cap: Some(100), - ..default() }), timestamp: id.block, ..default() }; - context.assert_runes([(id, entry)], []); + context.mine_blocks(1); - let txid1 = context.core.broadcast_tx(TransactionTemplate { + context.core.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { @@ -4436,20 +4895,11 @@ mod tests { context.mine_blocks(1); - entry.mints += 1; + context.assert_runes([(id, entry)], []); - context.assert_runes( - [(id, entry)], - [( - OutPoint { - txid: txid1, - vout: 0, - }, - vec![(id, 1000)], - )], - ); + context.mine_blocks(1); - context.core.broadcast_tx(TransactionTemplate { + let txid1 = context.core.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { @@ -4463,6 +4913,8 @@ mod tests { context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( [(id, entry)], [( @@ -4768,6 +5220,91 @@ mod tests { ); } + #[test] + fn open_mints_without_a_cap_are_unmintable() { + 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), + offset: (None, Some(2)), + ..default() + }), + ..default() + }), + ..default() + }, + 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), + offset: (None, Some(2)), + ..default() + }), + ..default() + }, + )], + [], + ); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(2, 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, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mints: 0, + etching: txid0, + terms: Some(Terms { + amount: Some(1000), + offset: (None, Some(2)), + ..default() + }), + ..default() + }, + )], + [], + ); + } + #[test] fn open_mint_claims_can_use_split() { let context = Context::builder().arg("--index-runes").build(); @@ -5196,7 +5733,7 @@ mod tests { ..default() }); - context.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + context.mine_blocks(Runestone::COMMIT_CONFIRMATIONS.into()); let mut witness = Witness::new(); @@ -5256,7 +5793,7 @@ mod tests { ..default() }); - context.mine_blocks((Runestone::COMMIT_INTERVAL - 1).into()); + context.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 2).into()); let mut witness = Witness::new(); @@ -5302,6 +5839,68 @@ mod tests { context.assert_runes([], []); } + #[test] + fn immature_commits_are_not_valid_even_when_bitcoind_is_ahead() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = context.index.block_count().unwrap().into_usize(); + + context.mine_blocks_with_update(1, false); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..default() + }); + + context.mine_blocks_with_update((Runestone::COMMIT_CONFIRMATIONS - 2).into(), false); + + 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_with_update(2, false); + + context.mine_blocks_with_update(1, true); + + context.assert_runes([], []); + } + #[test] fn etchings_are_not_valid_without_commitment() { let context = Context::builder().arg("--index-runes").build(); @@ -5316,7 +5915,7 @@ mod tests { ..default() }); - context.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + context.mine_blocks(Runestone::COMMIT_CONFIRMATIONS.into()); let mut witness = Witness::new(); @@ -5387,6 +5986,85 @@ mod tests { context.assert_runes([], []); } + #[test] + fn edict_with_amount_zero_and_no_destinations_is_ignored() { + let context = Context::builder().arg("--index-runes").build(); + + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), + ..default() + }), + ..default() + }, + 1, + ); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + timestamp: id.block, + ..default() + }, + )], + [( + OutPoint { + txid: txid0, + vout: 0, + }, + vec![(id, u128::MAX)], + )], + ); + + context.core.broadcast_tx(TransactionTemplate { + inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id, + amount: 0, + output: 1, + }], + ..default() + } + .encipher(), + ), + outputs: 0, + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes( + [( + id, + RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: u128::MAX, + burned: u128::MAX, + timestamp: id.block, + ..default() + }, + )], + [], + ); + } + #[test] fn genesis_rune() { assert_eq!( @@ -5424,6 +6102,7 @@ mod tests { offset: (None, None), }), timestamp: 0, + turbo: true, }, )], [], diff --git a/src/settings.rs b/src/settings.rs index a0a487002a..295827200a 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,6 +4,7 @@ use {super::*, bitcoincore_rpc::Auth}; #[serde(default, deny_unknown_fields)] pub struct Settings { bitcoin_data_dir: Option, + bitcoin_rpc_limit: Option, bitcoin_rpc_password: Option, bitcoin_rpc_url: Option, bitcoin_rpc_username: Option, @@ -108,6 +109,7 @@ impl Settings { pub(crate) fn or(self, source: Settings) -> Self { Self { bitcoin_data_dir: self.bitcoin_data_dir.or(source.bitcoin_data_dir), + bitcoin_rpc_limit: self.bitcoin_rpc_limit.or(source.bitcoin_rpc_limit), bitcoin_rpc_password: self.bitcoin_rpc_password.or(source.bitcoin_rpc_password), bitcoin_rpc_url: self.bitcoin_rpc_url.or(source.bitcoin_rpc_url), bitcoin_rpc_username: self.bitcoin_rpc_username.or(source.bitcoin_rpc_username), @@ -147,6 +149,7 @@ impl Settings { pub(crate) fn from_options(options: Options) -> Self { Self { bitcoin_data_dir: options.bitcoin_data_dir, + bitcoin_rpc_limit: options.bitcoin_rpc_limit, bitcoin_rpc_password: options.bitcoin_rpc_password, bitcoin_rpc_url: options.bitcoin_rpc_url, bitcoin_rpc_username: options.bitcoin_rpc_username, @@ -230,6 +233,7 @@ impl Settings { Ok(Self { bitcoin_data_dir: get_path("BITCOIN_DATA_DIR"), + bitcoin_rpc_limit: get_u32("BITCOIN_RPC_LIMIT")?, bitcoin_rpc_password: get_string("BITCOIN_RPC_PASSWORD"), bitcoin_rpc_url: get_string("BITCOIN_RPC_URL"), bitcoin_rpc_username: get_string("BITCOIN_RPC_USERNAME"), @@ -262,6 +266,7 @@ impl Settings { bitcoin_rpc_password: None, bitcoin_rpc_url: Some(rpc_url.into()), bitcoin_rpc_username: None, + bitcoin_rpc_limit: None, chain: Some(Chain::Regtest), commit_interval: None, config: None, @@ -320,6 +325,7 @@ impl Settings { Ok(Self { bitcoin_data_dir: Some(bitcoin_data_dir), + bitcoin_rpc_limit: Some(self.bitcoin_rpc_limit.unwrap_or(12)), bitcoin_rpc_password: self.bitcoin_rpc_password, bitcoin_rpc_url: Some( self @@ -550,6 +556,10 @@ impl Settings { } } + pub(crate) fn bitcoin_rpc_limit(&self) -> u32 { + self.bitcoin_rpc_limit.unwrap() + } + pub(crate) fn server_url(&self) -> Option<&str> { self.server_url.as_deref() } @@ -978,6 +988,7 @@ mod tests { fn from_env() { let env = vec![ ("BITCOIN_DATA_DIR", "/bitcoin/data/dir"), + ("BITCOIN_RPC_LIMIT", "12"), ("BITCOIN_RPC_PASSWORD", "bitcoin password"), ("BITCOIN_RPC_URL", "url"), ("BITCOIN_RPC_USERNAME", "bitcoin username"), @@ -1010,6 +1021,7 @@ mod tests { Settings::from_env(env).unwrap(), Settings { bitcoin_data_dir: Some("/bitcoin/data/dir".into()), + bitcoin_rpc_limit: Some(12), bitcoin_rpc_password: Some("bitcoin password".into()), bitcoin_rpc_url: Some("url".into()), bitcoin_rpc_username: Some("bitcoin username".into()), @@ -1055,6 +1067,7 @@ mod tests { Options::try_parse_from([ "ord", "--bitcoin-data-dir=/bitcoin/data/dir", + "--bitcoin-rpc-limit=12", "--bitcoin-rpc-password=bitcoin password", "--bitcoin-rpc-url=url", "--bitcoin-rpc-username=bitcoin username", @@ -1081,6 +1094,7 @@ mod tests { ), Settings { bitcoin_data_dir: Some("/bitcoin/data/dir".into()), + bitcoin_rpc_limit: Some(12), bitcoin_rpc_password: Some("bitcoin password".into()), bitcoin_rpc_url: Some("url".into()), bitcoin_rpc_username: Some("bitcoin username".into()), diff --git a/src/subcommand.rs b/src/subcommand.rs index 2da87735db..fd90a9fd0d 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -81,20 +81,28 @@ impl Subcommand { } } +#[derive(clap::ValueEnum, Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub enum OutputFormat { + #[default] + Json, + Yaml, + Minify, +} + pub trait Output: Send { - fn print_json(&self, minify: bool); + fn print(&self, format: OutputFormat); } impl Output for T where T: Serialize + Send, { - fn print_json(&self, minify: bool) { - if minify { - serde_json::to_writer(io::stdout(), self).ok(); - } else { - serde_json::to_writer_pretty(io::stdout(), self).ok(); - } + fn print(&self, format: OutputFormat) { + match format { + OutputFormat::Json => serde_json::to_writer_pretty(io::stdout(), self).ok(), + OutputFormat::Yaml => serde_yaml::to_writer(io::stdout(), self).ok(), + OutputFormat::Minify => serde_json::to_writer(io::stdout(), self).ok(), + }; println!(); } } diff --git a/src/subcommand/env.rs b/src/subcommand/env.rs index 9e2a4058f3..8f3478a0c5 100644 --- a/src/subcommand/env.rs +++ b/src/subcommand/env.rs @@ -1,4 +1,4 @@ -use {super::*, colored::Colorize, std::net::TcpListener}; +use {super::*, crate::wallet::batch, colored::Colorize, std::net::TcpListener}; struct KillOnDrop(process::Child); @@ -17,6 +17,16 @@ impl Drop for KillOnDrop { pub(crate) struct Env { #[arg(default_value = "env", help = "Create env in .")] directory: PathBuf, + #[arg( + long, + help = "Decompress encoded content. Currently only supports brotli. Be careful using this on production instances. A decompressed inscription may be arbitrarily large, making decompression a DoS vector." + )] + pub(crate) decompress: bool, + #[arg( + long, + help = "Proxy `/content/INSCRIPTION_ID` requests to `/content/INSCRIPTION_ID` if the inscription is not present on current chain." + )] + pub(crate) content_proxy: Option, } #[derive(Serialize)] @@ -50,24 +60,60 @@ impl Env { fs::create_dir_all(&absolute)?; - fs::write( - absolute.join("bitcoin.conf"), - format!( - "regtest=1 + let bitcoin_conf = absolute.join("bitcoin.conf"); + + if !bitcoin_conf.try_exists()? { + fs::write( + bitcoin_conf, + format!( + "datacarriersize=1000000 +regtest=1 datadir={absolute_str} listen=0 txindex=1 [regtest] rpcport={bitcoind_port} ", - ), - )?; + ), + )?; + } + + fs::write(absolute.join("inscription.txt"), "FOO")?; + + let yaml = serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: "FOO".parse::().unwrap(), + supply: "2000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + amount: "1000".parse().unwrap(), + cap: 1, + ..default() + }), + turbo: false, + }), + inscriptions: vec![batch::Entry { + file: Some("env/inscription.txt".into()), + ..default() + }], + ..default() + }) + .unwrap(); + + let batch_yaml = absolute.join("batch.yaml"); + + if !batch_yaml.try_exists()? { + fs::write(absolute.join("batch.yaml"), yaml)?; + } let _bitcoind = KillOnDrop( Command::new("bitcoind") .arg(format!("-conf={}", absolute.join("bitcoin.conf").display())) .stdout(Stdio::null()) - .spawn()?, + .spawn() + .expect("failed to start bitcoind"), ); loop { @@ -82,23 +128,36 @@ rpcport={bitcoind_port} let config = absolute.join("ord.yaml"); - fs::write( - config, - serde_yaml::to_string(&Settings::for_env(&absolute, &rpc_url, &server_url))?, - )?; + if !config.try_exists()? { + fs::write( + config, + serde_yaml::to_string(&Settings::for_env(&absolute, &rpc_url, &server_url))?, + )?; + } let ord = std::env::current_exe()?; - let _ord = KillOnDrop( - Command::new(&ord) - .arg("--datadir") - .arg(&absolute) - .arg("server") - .arg("--polling-interval=100ms") - .arg("--http-port") - .arg(ord_port.to_string()) - .spawn()?, - ); + let decompress = self.decompress; + let content_proxy = self.content_proxy.map(|url| url.to_string()); + + let mut command = Command::new(&ord); + let ord_server = command + .arg("--datadir") + .arg(&absolute) + .arg("server") + .arg("--polling-interval=100ms") + .arg("--http-port") + .arg(ord_port.to_string()); + + if decompress { + ord_server.arg("--decompress"); + } + + if let Some(content_proxy) = content_proxy { + ord_server.arg("--content-proxy").arg(content_proxy); + } + + let _ord = KillOnDrop(ord_server.spawn()?); thread::sleep(Duration::from_millis(250)); diff --git a/src/subcommand/runes.rs b/src/subcommand/runes.rs index 645cce331c..0ec292f75d 100644 --- a/src/subcommand/runes.rs +++ b/src/subcommand/runes.rs @@ -20,6 +20,7 @@ pub struct RuneInfo { pub symbol: Option, pub terms: Option, pub timestamp: DateTime, + pub turbo: bool, pub tx: u32, } @@ -52,6 +53,7 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { symbol, terms, timestamp, + turbo, }, )| { ( @@ -70,6 +72,7 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { symbol, terms, timestamp: crate::timestamp(timestamp), + turbo, tx: id.tx, }, ) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 0b1155427d..772feba523 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -13,12 +13,11 @@ use { InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml, ParentsHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, - PreviewVideoHtml, RangeHtml, RareTxt, RuneBalancesHtml, RuneHtml, RunesHtml, SatHtml, - TransactionHtml, + PreviewVideoHtml, RangeHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, TransactionHtml, }, axum::{ body, - extract::{Extension, Json, Path, Query}, + extract::{DefaultBodyLimit, Extension, Json, Path, Query}, http::{header, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Redirect, Response}, routing::{get, post}, @@ -48,7 +47,7 @@ pub(crate) use server_config::ServerConfig; mod accept_encoding; mod accept_json; mod error; -pub(crate) mod query; +pub mod query; mod server_config; enum SpawnConfig { @@ -207,6 +206,7 @@ impl Server { .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_query", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) + .route("/inscriptions", post(Self::inscriptions_json)) .route("/inscriptions/:page", get(Self::inscriptions_paginated)) .route( "/inscriptions/block/:height", @@ -219,6 +219,7 @@ impl Server { .route("/install.sh", get(Self::install_script)) .route("/ordinal/:sat", get(Self::ordinal)) .route("/output/:output", get(Self::output)) + .route("/outputs", post(Self::outputs)) .route("/parents/:inscription_id", get(Self::parents)) .route( "/parents/:inscription_id/:page", @@ -256,6 +257,7 @@ impl Server { .route("/rare.txt", get(Self::rare_txt)) .route("/rune/:rune", get(Self::rune)) .route("/runes", get(Self::runes)) + .route("/runes/:page", get(Self::runes_paginated)) .route("/runes/balances", get(Self::runes_balances)) .route("/sat/:sat", get(Self::sat)) .route("/search", get(Self::search_by_query)) @@ -265,9 +267,8 @@ impl Server { .route("/tx/:txid", get(Self::transaction)) .route("/update", get(Self::update)) // ---- Ordzaar routes ---- - // Deprecated: Ordzaar custom routes should use"ordzar" prefix/path + // Deprecated: Ordzaar custom routes should use"ordzaar" 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)) @@ -294,7 +295,13 @@ impl Server { .allow_origin(Any), ) .layer(CompressionLayer::new()) - .with_state(server_config); + .with_state(server_config.clone()); + + let router = if server_config.json_api_enabled { + router.layer(DefaultBodyLimit::disable()) + } else { + router + }; let router = if let Some((username, password)) = settings.credentials() { router.layer(ValidateRequestHeaderLayer::basic(username, password)) @@ -412,10 +419,13 @@ impl Server { } let rune = match rune_query { - query::Rune::SpacedRune(spaced_rune) => spaced_rune.rune, - query::Rune::RuneId(rune_id) => index + query::Rune::Spaced(spaced_rune) => spaced_rune.rune, + query::Rune::Id(rune_id) => index .get_rune_by_id(rune_id)? .ok_or_not_found(|| format!("rune {rune_id}"))?, + query::Rune::Number(number) => index + .get_rune_by_number(usize::try_from(number).unwrap())? + .ok_or_not_found(|| format!("rune number {number}"))?, }; let (id, entry, _) = index @@ -723,64 +733,21 @@ impl Server { AcceptJson(accept_json): AcceptJson, ) -> ServerResult { task::block_in_place(|| { - let sat_ranges = index.list(outpoint)?; - - let indexed; - - let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { - let mut value = 0; - - if let Some(ranges) = &sat_ranges { - for (start, end) in ranges { - value += end - start; - } - } - - indexed = true; - - TxOut { - value, - script_pubkey: ScriptBuf::new(), - } - } else { - indexed = index.contains_output(&outpoint)?; - - index - .get_transaction(outpoint.txid)? - .ok_or_not_found(|| format!("output {outpoint}"))? - .output - .into_iter() - .nth(outpoint.vout as usize) - .ok_or_not_found(|| format!("output {outpoint}"))? - }; - - let inscriptions = index.get_inscriptions_on_output(outpoint)?; - - let runes = index.get_rune_balances_for_outpoint(outpoint)?; - - let spent = index.is_output_spent(outpoint)?; + let (output_info, txout) = index + .get_output_info(outpoint)? + .ok_or_not_found(|| format!("output {outpoint}"))?; Ok(if accept_json { - Json(api::Output::new( - server_config.chain, - inscriptions, - outpoint, - output, - indexed, - runes, - sat_ranges, - spent, - )) - .into_response() + Json(output_info).into_response() } else { OutputHtml { chain: server_config.chain, - inscriptions, + inscriptions: output_info.inscriptions, outpoint, - output, - runes, - sat_ranges, - spent, + output: txout, + runes: output_info.runes, + sat_ranges: output_info.sat_ranges, + spent: output_info.spent, } .page(server_config) .into_response() @@ -788,6 +755,28 @@ impl Server { }) } + async fn outputs( + Extension(index): Extension>, + AcceptJson(accept_json): AcceptJson, + Json(outputs): Json>, + ) -> ServerResult { + task::block_in_place(|| { + Ok(if accept_json { + let mut response = Vec::new(); + for outpoint in outputs { + let (output_info, _) = index + .get_output_info(outpoint)? + .ok_or_not_found(|| format!("output {outpoint}"))?; + + response.push(output_info); + } + Json(response).into_response() + } else { + StatusCode::NOT_FOUND.into_response() + }) + }) + } + async fn range( Extension(server_config): Extension>, Path((DeserializeFromStr(start), DeserializeFromStr(end))): Path<( @@ -822,10 +811,13 @@ impl Server { } let rune = match rune_query { - query::Rune::SpacedRune(spaced_rune) => spaced_rune.rune, - query::Rune::RuneId(rune_id) => index + query::Rune::Spaced(spaced_rune) => spaced_rune.rune, + query::Rune::Id(rune_id) => index .get_rune_by_id(rune_id)? .ok_or_not_found(|| format!("rune {rune_id}"))?, + query::Rune::Number(number) => index + .get_rune_by_number(usize::try_from(number).unwrap())? + .ok_or_not_found(|| format!("rune number {number}"))?, }; let (id, entry, parent) = index @@ -860,17 +852,44 @@ impl Server { async fn runes( Extension(server_config): Extension>, Extension(index): Extension>, + accept_json: AcceptJson, + ) -> ServerResult { + Self::runes_paginated( + Extension(server_config), + Extension(index), + Path(0), + accept_json, + ) + .await + } + + async fn runes_paginated( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(page_index): Path, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { task::block_in_place(|| { + let (entries, more) = index.runes_paginated(50, page_index)?; + + let prev = page_index.checked_sub(1); + + let next = more.then_some(page_index + 1); + Ok(if accept_json { - Json(api::Runes { - entries: index.runes()?, + Json(RunesHtml { + entries, + more, + prev, + next, }) .into_response() } else { RunesHtml { - entries: index.runes()?, + entries, + more, + prev, + next, } .page(server_config) .into_response() @@ -879,15 +898,14 @@ impl Server { } async fn runes_balances( - Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { task::block_in_place(|| { - let balances = index.get_rune_balance_map()?; Ok(if accept_json { Json( - balances + index + .get_rune_balance_map()? .into_iter() .map(|(rune, balances)| { ( @@ -902,9 +920,7 @@ impl Server { ) .into_response() } else { - RuneBalancesHtml { balances } - .page(server_config) - .into_response() + StatusCode::NOT_FOUND.into_response() }) }) } @@ -980,6 +996,7 @@ impl Server { } }; + let runes = index.get_runes_in_block(u64::from(height))?; Ok(if accept_json { let inscriptions = index.get_inscriptions_in_block(height)?; Json(api::Block::new( @@ -987,6 +1004,7 @@ impl Server { Height(height), Self::index_height(&index)?, inscriptions, + runes, )) .into_response() } else { @@ -998,6 +1016,7 @@ impl Server { Self::index_height(&index)?, total_num, featured_inscriptions, + runes, ) .page(server_config) .into_response() @@ -1660,78 +1679,33 @@ impl Server { } } - let info = Index::inscription_info(&index, query)? + let (info, txout, inscription) = index + .inscription_info(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 - .output - .as_ref() - .and_then(|o| { - server_config - .chain - .address_from_script(&o.script_pubkey) - .ok() - }) - .map(|address| address.to_string()), - 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, - parents: info.parents, - previous: info.previous, - 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()), - // ---- Ordzaar ---- - }) - .into_response() + Json(info).into_response() } else { InscriptionHtml { chain: server_config.chain, - charms: Charm::Vindicated.unset(info.charms), + charms: Charm::Vindicated.unset(info.charms.iter().fold(0, |mut acc, charm| { + charm.set(&mut acc); + acc + })), children: info.children, - fee: info.entry.fee, - height: info.entry.height, - inscription: info.inscription, - id: info.entry.id, - number: info.entry.inscription_number, + fee: info.fee, + height: info.height, + inscription, + id: info.id, + number: info.number, next: info.next, - output: info.output, + output: txout, parents: info.parents, previous: info.previous, rune: info.rune, - sat: info.entry.sat, + sat: info.sat, satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp.into()), + timestamp: Utc.timestamp_opt(info.timestamp, 0).unwrap(), } .page(server_config) .into_response() @@ -1739,6 +1713,30 @@ impl Server { }) } + async fn inscriptions_json( + Extension(index): Extension>, + AcceptJson(accept_json): AcceptJson, + Json(inscriptions): Json>, + ) -> ServerResult { + task::block_in_place(|| { + Ok(if accept_json { + let mut response = Vec::new(); + for inscription in inscriptions { + let query = query::Inscription::Id(inscription); + let (info, _, _) = index + .inscription_info(query)? + .ok_or_not_found(|| format!("inscription {query}"))?; + + response.push(info); + } + + Json(response).into_response() + } else { + StatusCode::NOT_FOUND.into_response() + }) + }) + } + async fn collections( Extension(server_config): Extension>, Extension(index): Extension>, @@ -2270,7 +2268,7 @@ mod tests { ..default() }); - self.mine_blocks(Runestone::COMMIT_INTERVAL.into()); + self.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 1).into()); let witness = witness.unwrap_or_else(|| { let tapscript = script::Builder::new() @@ -2727,8 +2725,8 @@ mod tests { server.mine_blocks(1); - server.assert_redirect("/search/9:1", "/rune/AAAAAAAAAAAAA"); - server.assert_redirect("/search?query=9:1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search/8:1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search?query=8:1", "/rune/AAAAAAAAAAAAA"); server.assert_response_regex( "/search/100000000000000000000:200000000000000000", @@ -2737,6 +2735,14 @@ mod tests { ); } + #[test] + fn html_runes_balances_not_found() { + TestServer::builder() + .chain(Chain::Regtest) + .build() + .assert_response("/runes/balances", StatusCode::NOT_FOUND, ""); + } + #[test] fn fallback() { let server = TestServer::new(); @@ -2808,12 +2814,66 @@ mod tests { server.mine_blocks(1); server.assert_response_regex( - "/rune/9:1", + "/rune/8:1", StatusCode::OK, ".*Rune AAAAAAAAAAAAA.*", ); } + #[test] + fn runes_can_be_queried_by_rune_number() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_runes() + .build(); + + server.mine_blocks(1); + + server.assert_response_regex("/rune/0", StatusCode::NOT_FOUND, ".*"); + + for i in 0..10 { + let rune = Rune(RUNE + i); + 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/0", + StatusCode::OK, + ".*Rune AAAAAAAAAAAAA.*", + ); + + for i in 1..6 { + server.assert_response_regex( + format!("/rune/{}", i), + StatusCode::OK, + ".*Rune AAAAAAAAAAAA.*.*", + ); + } + + server.assert_response_regex( + "/rune/9", + StatusCode::OK, + ".*Rune AAAAAAAAAAAAJ.*", + ); + } + #[test] fn runes_are_displayed_on_runes_page() { let server = TestServer::builder() @@ -2826,7 +2886,7 @@ mod tests { server.assert_response_regex( "/runes", StatusCode::OK, - ".*Runes.*

Runes

\n
    \n
.*", + ".*Runes.*

Runes

\n
    \n
\n
\n prev\n next\n
.*", ); let (txid, id) = server.etch( @@ -2907,6 +2967,7 @@ mod tests { rune: Some(rune), symbol: Some('%'), premine: Some(u128::MAX), + turbo: true, ..default() }), ..default() @@ -2934,6 +2995,7 @@ mod tests { premine: u128::MAX, symbol: Some('%'), timestamp: id.block, + turbo: true, ..default() } )] @@ -2955,11 +3017,11 @@ mod tests {
number
0
timestamp
-
+
id
-
9:1
+
8:1
etching block
-
9
+
8
etching transaction
1
mint
@@ -2968,12 +3030,16 @@ mod tests {
340282366920938463463374607431768211455\u{A0}%
premine
340282366920938463463374607431768211455\u{A0}%
+
premine percentage
+
100%
burned
0\u{A0}%
divisibility
0
symbol
%
+
turbo
+
true
etching
{txid}
parent
@@ -2988,14 +3054,59 @@ mod tests { StatusCode::OK, ".*
- .*
rune
AAAAAAAAAAAAA
+ .*
.*", ); } + #[test] + fn etched_runes_are_displayed_on_block_page() { + let server = TestServer::builder() + .chain(Chain::Regtest) + .index_runes() + .build(); + + server.mine_blocks(1); + + let rune0 = Rune(RUNE); + + let (_txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune0), + ..default() + }), + ..default() + }, + 1, + None, + ); + + assert_eq!( + server.index.get_runes_in_block(id.block - 1).unwrap().len(), + 0 + ); + assert_eq!(server.index.get_runes_in_block(id.block).unwrap().len(), 1); + assert_eq!( + server.index.get_runes_in_block(id.block + 1).unwrap().len(), + 0 + ); + + server.assert_response_regex( + format!("/block/{}", id.block), + StatusCode::OK, + format!(".*

1 Rune

.*
  • {rune0}
  • .*"), + ); + } + #[test] fn runes_are_spaced() { let server = TestServer::builder() @@ -3260,7 +3371,9 @@ mod tests { divisibility: 1, symbol: None, } - )], + )] + .into_iter() + .collect(), spent: false, } ); @@ -3318,7 +3431,12 @@ mod tests { 3, 0, 0, - Inscription::new(None, Some("hello".as_bytes().into())).to_witness(), + Inscription { + content_type: None, + body: Some("hello".as_bytes().into()), + ..default() + } + .to_witness(), )], ..default() }); @@ -4166,7 +4284,11 @@ mod tests { fn content_response_no_content() { assert_eq!( Server::content_response( - Inscription::new(Some("text/plain".as_bytes().to_vec()), None), + Inscription { + content_type: Some("text/plain".as_bytes().to_vec()), + body: None, + ..default() + }, AcceptEncoding::default(), &ServerConfig::default(), ) @@ -4178,7 +4300,11 @@ mod tests { #[test] fn content_response_with_content() { let (headers, body) = Server::content_response( - Inscription::new(Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3])), + Inscription { + content_type: Some("text/plain".as_bytes().to_vec()), + body: Some(vec![1, 2, 3]), + ..default() + }, AcceptEncoding::default(), &ServerConfig::default(), ) @@ -4192,7 +4318,11 @@ mod tests { #[test] fn content_security_policy_no_origin() { let (headers, _) = Server::content_response( - Inscription::new(Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3])), + Inscription { + content_type: Some("text/plain".as_bytes().to_vec()), + body: Some(vec![1, 2, 3]), + ..default() + }, AcceptEncoding::default(), &ServerConfig::default(), ) @@ -4208,7 +4338,11 @@ mod tests { #[test] fn content_security_policy_with_origin() { let (headers, _) = Server::content_response( - Inscription::new(Some("text/plain".as_bytes().to_vec()), Some(vec![1, 2, 3])), + Inscription { + content_type: Some("text/plain".as_bytes().to_vec()), + body: Some(vec![1, 2, 3]), + ..default() + }, AcceptEncoding::default(), &ServerConfig { csp_origin: Some("https://ordinals.com".into()), @@ -4299,7 +4433,11 @@ mod tests { #[test] fn content_response_no_content_type() { let (headers, body) = Server::content_response( - Inscription::new(None, Some(Vec::new())), + Inscription { + content_type: None, + body: Some(Vec::new()), + ..default() + }, AcceptEncoding::default(), &ServerConfig::default(), ) @@ -4313,7 +4451,11 @@ mod tests { #[test] fn content_response_bad_content_type() { let (headers, body) = Server::content_response( - Inscription::new(Some("\n".as_bytes().to_vec()), Some(Vec::new())), + Inscription { + content_type: Some("\n".as_bytes().to_vec()), + body: Some(Vec::new()), + ..Default::default() + }, AcceptEncoding::default(), &ServerConfig::default(), ) @@ -4663,7 +4805,12 @@ mod tests { 1, 0, 0, - Inscription::new(Some("foo/bar".as_bytes().to_vec()), None).to_witness(), + Inscription { + content_type: Some("foo/bar".as_bytes().to_vec()), + body: None, + ..default() + } + .to_witness(), )], ..default() }); @@ -4692,7 +4839,12 @@ mod tests { 1, 0, 0, - Inscription::new(Some("image/png".as_bytes().to_vec()), None).to_witness(), + Inscription { + content_type: Some("image/png".as_bytes().to_vec()), + body: None, + ..default() + } + .to_witness(), )], ..default() }); diff --git a/src/subcommand/server/query.rs b/src/subcommand/server/query.rs index 3acdc5e940..f0d1efe82c 100644 --- a/src/subcommand/server/query.rs +++ b/src/subcommand/server/query.rs @@ -50,9 +50,11 @@ impl Display for Inscription { } } +#[derive(Debug)] pub(super) enum Rune { - SpacedRune(SpacedRune), - RuneId(RuneId), + Spaced(SpacedRune), + Id(RuneId), + Number(u64), } impl FromStr for Rune { @@ -60,9 +62,11 @@ impl FromStr for Rune { fn from_str(s: &str) -> Result { if s.contains(':') { - Ok(Self::RuneId(s.parse()?)) + Ok(Self::Id(s.parse()?)) + } else if re::RUNE_NUMBER.is_match(s) { + Ok(Self::Number(s.parse()?)) } else { - Ok(Self::SpacedRune(s.parse()?)) + Ok(Self::Spaced(s.parse()?)) } } } diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 5a2b55b18f..b555ee0f9c 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -1,6 +1,6 @@ use { super::*, - crate::wallet::{batch, Wallet}, + crate::wallet::{batch, wallet_constructor::WalletConstructor, Wallet}, bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult, shared_args::SharedArgs, }; @@ -12,6 +12,7 @@ pub mod create; pub mod dump; pub mod inscribe; pub mod inscriptions; +mod label; pub mod mint; pub mod outputs; pub mod receive; @@ -44,6 +45,8 @@ pub(crate) enum Subcommand { Balance, #[command(about = "Create inscriptions and runes")] Batch(batch_command::Batch), + #[command(about = "List unspent cardinal outputs in wallet")] + Cardinals, #[command(about = "Create new wallet")] Create(create::Create), #[command(about = "Dump wallet descriptors")] @@ -52,24 +55,24 @@ pub(crate) enum Subcommand { Inscribe(inscribe::Inscribe), #[command(about = "List wallet inscriptions")] Inscriptions, + #[command(about = "Export output labels")] + Label, #[command(about = "Mint a rune")] Mint(mint::Mint), + #[command(about = "List all unspent outputs in wallet")] + Outputs, #[command(about = "Generate receive address")] Receive(receive::Receive), #[command(about = "Restore wallet")] Restore(restore::Restore), #[command(about = "Resume pending etchings")] - Resume, + Resume(resume::Resume), #[command(about = "List wallet satoshis")] Sats(sats::Sats), #[command(about = "Send sat or inscription")] Send(send::Send), #[command(about = "See wallet transactions")] Transactions(transactions::Transactions), - #[command(about = "List all unspent outputs in wallet")] - Outputs, - #[command(about = "List unspent cardinal outputs in wallet")] - Cardinals, } impl WalletCommand { @@ -80,7 +83,7 @@ impl WalletCommand { _ => {} }; - let wallet = Wallet::build( + let wallet = WalletConstructor::construct( self.name.clone(), self.no_sync, settings.clone(), @@ -97,18 +100,19 @@ impl WalletCommand { match self.subcommand { Subcommand::Balance => balance::run(wallet), Subcommand::Batch(batch) => batch.run(wallet), + Subcommand::Cardinals => cardinals::run(wallet), + Subcommand::Create(_) | Subcommand::Restore(_) => unreachable!(), Subcommand::Dump => dump::run(wallet), Subcommand::Inscribe(inscribe) => inscribe.run(wallet), Subcommand::Inscriptions => inscriptions::run(wallet), + Subcommand::Label => label::run(wallet), Subcommand::Mint(mint) => mint.run(wallet), + Subcommand::Outputs => outputs::run(wallet), Subcommand::Receive(receive) => receive.run(wallet), - Subcommand::Resume => resume::run(wallet), + Subcommand::Resume(resume) => resume.run(wallet), Subcommand::Sats(sats) => sats.run(wallet), Subcommand::Send(send) => send.run(wallet), Subcommand::Transactions(transactions) => transactions.run(wallet), - Subcommand::Outputs => outputs::run(wallet), - Subcommand::Cardinals => cardinals::run(wallet), - Subcommand::Create(_) | Subcommand::Restore(_) => unreachable!(), } } } diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 05d5b9fd53..192904a069 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -5,7 +5,7 @@ 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, @@ -26,7 +26,7 @@ pub(crate) fn run(wallet: Wallet) -> SubcommandResult { let mut runic = 0; for (output, txout) in unspent_outputs { - let rune_balances = wallet.get_runes_balances_for_output(output)?; + let rune_balances = wallet.get_runes_balances_in_output(output)?; let is_ordinal = inscription_outputs.contains(output); let is_runic = !rune_balances.is_empty(); @@ -37,7 +37,16 @@ pub(crate) fn run(wallet: Wallet) -> SubcommandResult { if is_runic { for (spaced_rune, pile) in rune_balances { - *runes.entry(spaced_rune).or_default() += pile.amount; + runes + .entry(spaced_rune) + .and_modify(|decimal: &mut Decimal| { + assert_eq!(decimal.scale, pile.divisibility); + decimal.value += pile.amount; + }) + .or_insert(Decimal { + value: pile.amount, + scale: pile.divisibility, + }); } runic += txout.value; } diff --git a/src/subcommand/wallet/batch_command.rs b/src/subcommand/wallet/batch_command.rs index 486789b298..b5fd837499 100644 --- a/src/subcommand/wallet/batch_command.rs +++ b/src/subcommand/wallet/batch_command.rs @@ -101,7 +101,7 @@ impl Batch { terms .cap .checked_mul(terms.amount.to_integer(etching.divisibility)?) - .ok_or_else(|| anyhow!("`terms.count` * `terms.amount` over maximum")) + .ok_or_else(|| anyhow!("`terms.cap` * `terms.amount` over maximum")) }) .transpose()? .unwrap_or_default(); @@ -110,8 +110,8 @@ impl Batch { 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`" + .ok_or_else(|| anyhow!("`premine` + `terms.cap` * `terms.amount` over maximum"))?, + "`supply` not equal to `premine` + `terms.cap` * `terms.amount`" ); ensure!(supply > 0, "`supply` must be greater than zero"); @@ -120,7 +120,14 @@ impl Batch { let current_height = u32::try_from(bitcoin_client.get_block_count()?).unwrap(); - let reveal_height = current_height + 1 + u32::from(Runestone::COMMIT_INTERVAL); + let reveal_height = current_height + u32::from(Runestone::COMMIT_CONFIRMATIONS); + + let first_rune_height = Rune::first_rune_height(wallet.chain().into()); + + ensure!( + reveal_height >= first_rune_height, + "rune reveal height below rune activation height: {reveal_height} < {first_rune_height}", + ); if let Some(terms) = etching.terms { if let Some((start, end)) = terms.offset.and_then(|range| range.start.zip(range.end)) { @@ -224,12 +231,12 @@ inscriptions: batch::File { inscriptions: vec![ batch::Entry { - file: inscription_path, + file: Some(inscription_path), metadata: Some(Value::Mapping(metadata)), ..default() }, batch::Entry { - file: brc20_path, + file: Some(brc20_path), metaprotocol: Some("brc-20".to_string()), ..default() } diff --git a/src/subcommand/wallet/cardinals.rs b/src/subcommand/wallet/cardinals.rs index 1da96c4b67..6b7413c78a 100644 --- a/src/subcommand/wallet/cardinals.rs +++ b/src/subcommand/wallet/cardinals.rs @@ -15,10 +15,12 @@ pub(crate) fn run(wallet: Wallet) -> SubcommandResult { .map(|satpoint| satpoint.outpoint) .collect::>(); + let runic_utxos = wallet.get_runic_outputs()?; + let cardinal_utxos = unspent_outputs .iter() .filter_map(|(output, txout)| { - if inscribed_utxos.contains(output) { + if inscribed_utxos.contains(output) || runic_utxos.contains(output) { None } else { Some(CardinalUtxo { diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 25ae0e51b5..24cf80a7de 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,6 +1,12 @@ use super::*; #[derive(Debug, Parser)] +#[clap(group( + ArgGroup::new("input") + .required(true) + .multiple(true) + .args(&["delegate", "file"])) +)] pub(crate) struct Inscribe { #[command(flatten)] shared: SharedArgs, @@ -14,8 +20,11 @@ pub(crate) struct Inscribe { pub(crate) delegate: Option, #[arg(long, help = "Send inscription to .")] pub(crate) destination: Option>, - #[arg(long, help = "Inscribe sat with contents of .")] - pub(crate) file: PathBuf, + #[arg( + long, + help = "Inscribe sat with contents of . May be omitted if `--delegate` is supplied." + )] + pub(crate) file: Option, #[arg( long, help = "Include JSON in file at converted to CBOR as inscription metadata", @@ -28,7 +37,7 @@ pub(crate) struct Inscribe { pub(crate) parent: Option, #[arg( long, - help = "Amount of postage to include in the inscription. Default `10000sat`." + help = "Include postage with inscription. [default: 10000sat]" )] pub(crate) postage: Option, #[clap(long, help = "Allow reinscription.")] @@ -58,7 +67,7 @@ impl Inscribe { }], dry_run: self.shared.dry_run, etching: None, - inscriptions: vec![Inscription::from_file( + inscriptions: vec![Inscription::new( chain, self.shared.compress, self.delegate, @@ -155,4 +164,49 @@ mod tests { ".*--sat.*cannot be used with.*--satpoint.*" ); } + + #[test] + fn delegate_or_file_must_be_set() { + assert_regex_match!( + Arguments::try_parse_from(["ord", "wallet", "inscribe", "--fee-rate", "1"]) + .unwrap_err() + .to_string(), + r".*required arguments.*--delegate \|--file .*" + ); + + assert!(Arguments::try_parse_from([ + "ord", + "wallet", + "inscribe", + "--file", + "hello.txt", + "--fee-rate", + "1" + ]) + .is_ok()); + + assert!(Arguments::try_parse_from([ + "ord", + "wallet", + "inscribe", + "--delegate", + "038112028c55f3f77cc0b8b413df51f70675f66be443212da0642b7636f68a00i0", + "--fee-rate", + "1" + ]) + .is_ok()); + + assert!(Arguments::try_parse_from([ + "ord", + "wallet", + "inscribe", + "--file", + "hello.txt", + "--delegate", + "038112028c55f3f77cc0b8b413df51f70675f66be443212da0642b7636f68a00i0", + "--fee-rate", + "1" + ]) + .is_ok()); + } } diff --git a/src/subcommand/wallet/label.rs b/src/subcommand/wallet/label.rs new file mode 100644 index 0000000000..646db83ec2 --- /dev/null +++ b/src/subcommand/wallet/label.rs @@ -0,0 +1,71 @@ +use super::*; + +#[derive(Serialize)] +struct Label { + first_sat: SatLabel, + inscriptions: BTreeMap>, +} + +#[derive(Serialize)] +struct SatLabel { + name: String, + number: u64, + rarity: Rarity, +} + +#[derive(Serialize)] +struct Line { + label: String, + r#ref: String, + r#type: String, +} + +pub(crate) fn run(wallet: Wallet) -> SubcommandResult { + let mut lines: Vec = Vec::new(); + + let sat_ranges = wallet.get_output_sat_ranges()?; + + let mut inscriptions_by_output: BTreeMap>> = + BTreeMap::new(); + + for (satpoint, inscriptions) in wallet.inscriptions() { + inscriptions_by_output + .entry(satpoint.outpoint) + .or_default() + .insert(satpoint.offset, inscriptions.clone()); + } + + for (output, ranges) in sat_ranges { + let sat = Sat(ranges[0].0); + let mut inscriptions = BTreeMap::>::new(); + + if let Some(output_inscriptions) = inscriptions_by_output.get(&output) { + for (&offset, offset_inscriptions) in output_inscriptions { + inscriptions + .entry(offset) + .or_default() + .extend(offset_inscriptions); + } + } + + lines.push(Line { + label: serde_json::to_string(&Label { + first_sat: SatLabel { + name: sat.name(), + number: sat.n(), + rarity: sat.rarity(), + }, + inscriptions, + })?, + r#ref: output.to_string(), + r#type: "output".into(), + }); + } + + for line in lines { + serde_json::to_writer(io::stdout(), &line)?; + println!(); + } + + Ok(None) +} diff --git a/src/subcommand/wallet/mint.rs b/src/subcommand/wallet/mint.rs index 069c108bf2..281bdf5fe6 100644 --- a/src/subcommand/wallet/mint.rs +++ b/src/subcommand/wallet/mint.rs @@ -6,6 +6,13 @@ pub(crate) struct Mint { fee_rate: FeeRate, #[clap(long, help = "Mint . May contain `.` or `•`as spacers.")] rune: SpacedRune, + #[clap( + long, + help = "Include postage with mint output. [default: 10000sat]" + )] + postage: Option, + #[clap(long, help = "Send minted runes to .")] + destination: Option>, } #[derive(Serialize, Deserialize, Debug)] @@ -19,7 +26,7 @@ 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", + "`ord wallet mint` requires index created with `--index-runes` flag", ); let rune = self.rune.rune; @@ -32,11 +39,24 @@ impl Mint { bail!("rune {rune} has not been etched"); }; + let postage = self.postage.unwrap_or(TARGET_POSTAGE); + let amount = rune_entry - .mintable(block_height) + .mintable(block_height + 1) .map_err(|err| anyhow!("rune {rune} {err}"))?; - let destination = wallet.get_change_address()?; + let chain = wallet.chain(); + + let destination = match self.destination { + Some(destination) => destination.require_network(chain.network())?, + None => wallet.get_change_address()?, + }; + + ensure!( + destination.script_pubkey().dust_value() < postage, + "postage below dust limit of {}sat", + destination.script_pubkey().dust_value().to_sat() + ); let runestone = Runestone { mint: Some(id), @@ -62,7 +82,7 @@ impl Mint { }, TxOut { script_pubkey: destination.script_pubkey(), - value: TARGET_POSTAGE.to_sat(), + value: postage.to_sat(), }, ], }; diff --git a/src/subcommand/wallet/resume.rs b/src/subcommand/wallet/resume.rs index d844591b50..64c3fcff0b 100644 --- a/src/subcommand/wallet/resume.rs +++ b/src/subcommand/wallet/resume.rs @@ -1,18 +1,57 @@ -use super::*; +use {super::*, crate::wallet::Maturity}; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct ResumeOutput { pub etchings: Vec, } +#[derive(Debug, Parser)] +pub(crate) struct Resume { + #[arg(long, help = "Don't broadcast transactions.")] + pub(crate) dry_run: bool, +} + +impl Resume { + pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { + let mut etchings = Vec::new(); + loop { + if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) { + break; + } + + for (rune, entry) in wallet.pending_etchings()? { + if self.dry_run { + etchings.push(batch::Output { + reveal_broadcast: false, + ..entry.output.clone() + }); + continue; + }; + + match wallet.check_maturity(rune, &entry.commit)? { + Maturity::Mature => etchings.push(wallet.send_etching(rune, &entry)?), + Maturity::CommitSpent(txid) => { + eprintln!("Commitment for rune etching {rune} spent in {txid}"); + wallet.clear_etching(rune)?; + } + Maturity::CommitNotFound => {} + Maturity::BelowMinimumHeight(_) => {} + Maturity::ConfirmationsPending(_) => {} + } + } + + if wallet.pending_etchings()?.is_empty() { + break; + } + + if self.dry_run { + break; + } -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(); + if !wallet.integration_test() { + thread::sleep(Duration::from_secs(5)); + } + } - outputs.map(|etchings| Some(Box::new(ResumeOutput { etchings }) as Box)) + Ok(Some(Box::new(ResumeOutput { etchings }) as Box)) + } } diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index d7ef27e4a9..7cdbe72c74 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -11,8 +11,8 @@ pub(crate) struct Sats { #[derive(Serialize, Deserialize)] pub struct OutputTsv { - pub sat: String, - pub output: OutPoint, + pub found: BTreeMap, + pub lost: BTreeSet, } #[derive(Serialize, Deserialize)] @@ -30,24 +30,26 @@ impl Sats { "sats requires index created with `--index-sats` flag" ); - let utxos = wallet.get_output_sat_ranges()?; + let haystacks = wallet.get_output_sat_ranges()?; if let Some(path) = &self.tsv { - let mut output = Vec::new(); - for (outpoint, sat) in sats_from_tsv( - utxos, - &fs::read_to_string(path) - .with_context(|| format!("I/O error reading `{}`", path.display()))?, - )? { - output.push(OutputTsv { - sat: sat.into(), - output: outpoint, - }); - } - Ok(Some(Box::new(output))) + let tsv = fs::read_to_string(path) + .with_context(|| format!("I/O error reading `{}`", path.display()))?; + + let needles = Self::needles(&tsv)?; + + let found = Self::find(&needles, &haystacks); + + let lost = needles + .into_iter() + .filter(|(_sat, value)| !found.contains_key(*value)) + .map(|(_sat, value)| value.into()) + .collect(); + + Ok(Some(Box::new(OutputTsv { found, lost }))) } else { let mut output = Vec::new(); - for (outpoint, sat, offset, rarity) in rare_sats(utxos) { + for (outpoint, sat, offset, rarity) in Self::rare_sats(haystacks) { output.push(OutputRare { sat, output: outpoint, @@ -58,90 +60,101 @@ impl Sats { Ok(Some(Box::new(output))) } } -} -fn rare_sats(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Sat, u64, Rarity)> { - utxos - .into_iter() - .flat_map(|(outpoint, sat_ranges)| { + fn find( + needles: &[(Sat, &str)], + ranges: &[(OutPoint, Vec<(u64, u64)>)], + ) -> BTreeMap { + let mut haystacks = Vec::new(); + + for (outpoint, ranges) in ranges { let mut offset = 0; - sat_ranges.into_iter().filter_map(move |(start, end)| { - let sat = Sat(start); - let rarity = sat.rarity(); - let start_offset = offset; + for (start, end) in ranges { + haystacks.push((start, end, offset, outpoint)); offset += end - start; - if rarity > Rarity::Common { - Some((outpoint, sat, start_offset, rarity)) - } else { - None - } - }) - }) - .collect() -} - -fn sats_from_tsv( - utxos: Vec<(OutPoint, Vec<(u64, u64)>)>, - tsv: &str, -) -> Result> { - let mut needles = Vec::new(); - for (i, line) in tsv.lines().enumerate() { - if line.is_empty() || line.starts_with('#') { - continue; + } } - if let Some(value) = line.split('\t').next() { - let sat = Sat::from_str(value).map_err(|err| { - anyhow!( - "failed to parse sat from string \"{value}\" on line {}: {err}", - i + 1, - ) - })?; + haystacks.sort_by_key(|(start, _, _, _)| *start); + + let mut i = 0; + let mut j = 0; + let mut results = BTreeMap::new(); + while i < needles.len() && j < haystacks.len() { + let (needle, value) = needles[i]; + let (&start, &end, offset, outpoint) = haystacks[j]; + + if needle >= start && needle < end { + results.insert( + value.into(), + SatPoint { + outpoint: *outpoint, + offset: offset + needle.0 - start, + }, + ); + } - needles.push((sat, value)); + if needle >= end { + j += 1; + } else { + i += 1; + } } + + results } - needles.sort(); - let mut haystacks = utxos - .into_iter() - .flat_map(|(outpoint, ranges)| { - ranges - .into_iter() - .map(move |(start, end)| (start, end, outpoint)) - }) - .collect::>(); - haystacks.sort(); - - let mut i = 0; - let mut j = 0; - let mut results = Vec::new(); - while i < needles.len() && j < haystacks.len() { - let (needle, value) = needles[i]; - let (start, end, outpoint) = haystacks[j]; - - if needle >= start && needle < end { - results.push((outpoint, value)); - } + fn needles(tsv: &str) -> Result> { + let mut needles = tsv + .lines() + .enumerate() + .filter(|(_i, line)| !line.starts_with('#') && !line.is_empty()) + .filter_map(|(i, line)| { + line.split('\t').next().map(|value| { + Sat::from_str(value).map(|sat| (sat, value)).map_err(|err| { + anyhow!( + "failed to parse sat from string \"{value}\" on line {}: {err}", + i + 1, + ) + }) + }) + }) + .collect::>>()?; - if needle >= end { - j += 1; - } else { - i += 1; - } + needles.sort(); + + Ok(needles) } - Ok(results) + fn rare_sats(haystacks: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Sat, u64, Rarity)> { + haystacks + .into_iter() + .flat_map(|(outpoint, sat_ranges)| { + let mut offset = 0; + sat_ranges.into_iter().filter_map(move |(start, end)| { + let sat = Sat(start); + let rarity = sat.rarity(); + let start_offset = offset; + offset += end - start; + if rarity > Rarity::Common { + Some((outpoint, sat, start_offset, rarity)) + } else { + None + } + }) + }) + .collect() + } } #[cfg(test)] mod tests { - use {super::*, std::fmt::Write}; + use super::*; #[test] fn identify_no_rare_sats() { assert_eq!( - rare_sats(vec![( + Sats::rare_sats(vec![( outpoint(1), vec![(51 * COIN_VALUE, 100 * COIN_VALUE), (1234, 5678)], )]), @@ -152,7 +165,7 @@ mod tests { #[test] fn identify_one_rare_sat() { assert_eq!( - rare_sats(vec![( + Sats::rare_sats(vec![( outpoint(1), vec![(10, 80), (50 * COIN_VALUE, 100 * COIN_VALUE)], )]), @@ -163,7 +176,7 @@ mod tests { #[test] fn identify_two_rare_sats() { assert_eq!( - rare_sats(vec![( + Sats::rare_sats(vec![( outpoint(1), vec![(0, 100), (1050000000000000, 1150000000000000)], )]), @@ -177,7 +190,7 @@ mod tests { #[test] fn identify_rare_sats_in_different_outpoints() { assert_eq!( - rare_sats(vec![ + Sats::rare_sats(vec![ (outpoint(1), vec![(50 * COIN_VALUE, 55 * COIN_VALUE)]), (outpoint(2), vec![(100 * COIN_VALUE, 111 * COIN_VALUE)],), ]), @@ -188,134 +201,110 @@ mod tests { ) } - #[test] - fn identify_from_tsv_none() { + #[track_caller] + fn case(tsv: &str, haystacks: &[(OutPoint, Vec<(u64, u64)>)], expected: &[(&str, SatPoint)]) { assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "1\n").unwrap(), - Vec::new() - ) + Sats::find(&Sats::needles(tsv).unwrap(), haystacks), + expected + .iter() + .map(|(sat, satpoint)| (sat.to_string(), *satpoint)) + .collect() + ); + } + + #[test] + fn tsv() { + case("1\n", &[(outpoint(1), vec![(0, 1)])], &[]); } #[test] fn identify_from_tsv_single() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n").unwrap(), - vec![(outpoint(1), "0"),] - ) + case( + "0\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); } #[test] fn identify_from_tsv_two_in_one_range() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "0\n1\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) + case( + "0\n1\n", + &[(outpoint(1), vec![(0, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))], + ); } #[test] fn identify_from_tsv_out_of_order_tsv() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 2)])], "1\n0\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) + case( + "1\n0\n", + &[(outpoint(1), vec![(0, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))], + ); } #[test] fn identify_from_tsv_out_of_order_ranges() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(1, 2), (0, 1)])], "1\n0\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] - ) + case( + "1\n0\n", + &[(outpoint(1), vec![(1, 2), (0, 1)])], + &[("0", satpoint(1, 1)), ("1", satpoint(1, 0))], + ); } #[test] fn identify_from_tsv_two_in_two_ranges() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1), (1, 2)])], "0\n1\n").unwrap(), - vec![(outpoint(1), "0"), (outpoint(1), "1"),] + case( + "0\n1\n", + &[(outpoint(1), vec![(0, 1), (1, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))], ) } #[test] fn identify_from_tsv_two_in_two_outputs() { - assert_eq!( - sats_from_tsv( - vec![(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])], - "0\n1\n" - ) - .unwrap(), - vec![(outpoint(1), "0"), (outpoint(2), "1"),] - ) + case( + "0\n1\n", + &[(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])], + &[("0", satpoint(1, 0)), ("1", satpoint(2, 0))], + ); } #[test] fn identify_from_tsv_ignores_extra_columns() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\t===\n").unwrap(), - vec![(outpoint(1), "0"),] - ) + case( + "0\t===\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); } #[test] fn identify_from_tsv_ignores_empty_lines() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n\n\n").unwrap(), - vec![(outpoint(1), "0"),] - ) + case( + "0\n\n\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); } #[test] fn identify_from_tsv_ignores_comments() { - assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n#===\n").unwrap(), - vec![(outpoint(1), "0"),] - ) + case( + "0\n#===\n", + &[(outpoint(1), vec![(0, 1)])], + &[("0", satpoint(1, 0))], + ); } #[test] fn parse_error_reports_line_and_value() { assert_eq!( - sats_from_tsv(vec![(outpoint(1), vec![(0, 1)])], "0\n===\n") + Sats::needles("0\n===\n") .unwrap_err() .to_string(), "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string", - ) - } - - #[test] - fn identify_from_tsv_is_fast() { - let mut start = 0; - let mut utxos = Vec::new(); - let mut results = Vec::new(); - for i in 0..16 { - let mut ranges = Vec::new(); - let outpoint = outpoint(i); - for _ in 0..100 { - let end = start + 50 * COIN_VALUE; - ranges.push((start, end)); - for j in 0..50 { - results.push((outpoint, start + j * COIN_VALUE)); - } - start = end; - } - utxos.push((outpoint, ranges)); - } - - let mut tsv = String::new(); - for i in 0..start / COIN_VALUE { - writeln!(tsv, "{}", i * COIN_VALUE).expect("writing to string should succeed"); - } - - let start = Instant::now(); - assert_eq!( - sats_from_tsv(utxos, &tsv) - .unwrap() - .into_iter() - .map(|(outpoint, s)| (outpoint, s.parse().unwrap())) - .collect::>(), - results ); - - assert!(Instant::now() - start < Duration::from_secs(10)); } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 3a7fd5ae66..ddfcc4df50 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -8,7 +8,7 @@ pub(crate) struct Send { fee_rate: FeeRate, #[arg( long, - help = "Target amount of postage to include with sent inscriptions [default: 10000 sat]" + help = "Target postage with sent inscriptions. [default: 10000 sat]" )] pub(crate) postage: Option, address: Address, @@ -39,6 +39,7 @@ impl Send { address, rune, decimal, + self.postage.unwrap_or(TARGET_POSTAGE), self.fee_rate, )?, Outgoing::InscriptionId(id) => Self::create_unsigned_send_satpoint_transaction( @@ -209,6 +210,7 @@ impl Send { destination: Address, spaced_rune: SpacedRune, decimal: Decimal, + postage: Amount, fee_rate: FeeRate, ) -> Result { ensure!( @@ -216,10 +218,6 @@ impl Send { "sending runes with `ord send` requires index created with `--index-runes` flag", ); - let inscriptions = wallet.inscriptions(); - let runic_outputs = wallet.get_runic_outputs()?; - let bitcoin_client = wallet.bitcoin_client(); - wallet.lock_non_cardinal_outputs()?; let (id, entry, _parent) = wallet @@ -228,37 +226,64 @@ impl Send { let amount = decimal.to_integer(entry.divisibility)?; - let inscribed_outputs = inscriptions + let inscribed_outputs = wallet + .inscriptions() .keys() .map(|satpoint| satpoint.outpoint) .collect::>(); - let mut input_runes = 0; - let mut input = Vec::new(); + let balances = wallet + .get_runic_outputs()? + .into_iter() + .filter(|output| !inscribed_outputs.contains(output)) + .map(|output| { + wallet.get_runes_balances_in_output(&output).map(|balance| { + ( + output, + balance + .into_iter() + .map(|(spaced_rune, pile)| (spaced_rune.rune, pile)) + .collect(), + ) + }) + }) + .collect::>>>()?; - for output in runic_outputs { - if inscribed_outputs.contains(&output) { - continue; - } + let mut inputs = Vec::new(); + let mut input_rune_balances: BTreeMap = BTreeMap::new(); - let balance = wallet.get_rune_balance_in_output(&output, entry.spaced_rune.rune)?; + for (output, runes) in balances { + if let Some(balance) = runes.get(&spaced_rune.rune) { + if balance.amount > 0 { + *input_rune_balances.entry(spaced_rune.rune).or_default() += balance.amount; - if balance > 0 { - input_runes += balance; - input.push(output); + inputs.push(output); + } } - if input_runes >= amount { + if input_rune_balances + .get(&spaced_rune.rune) + .cloned() + .unwrap_or_default() + >= amount + { break; } } + let input_rune_balance = input_rune_balances + .get(&spaced_rune.rune) + .cloned() + .unwrap_or_default(); + + let needs_runes_change_output = input_rune_balance > amount || input_rune_balances.len() > 1; + ensure! { - input_runes >= amount, + input_rune_balance >= amount, "insufficient `{}` balance, only {} in wallet", spaced_rune, Pile { - amount: input_runes, + amount: input_rune_balance, divisibility: entry.divisibility, symbol: entry.symbol }, @@ -276,7 +301,7 @@ impl Send { let unfunded_transaction = Transaction { version: 2, lock_time: LockTime::ZERO, - input: input + input: inputs .into_iter() .map(|previous_output| TxIn { previous_output, @@ -285,31 +310,40 @@ impl Send { witness: Witness::new(), }) .collect(), - output: vec![ - TxOut { - script_pubkey: runestone.encipher(), - value: 0, - }, - TxOut { - script_pubkey: wallet.get_change_address()?.script_pubkey(), - value: TARGET_POSTAGE.to_sat(), - }, - TxOut { + output: if needs_runes_change_output { + vec![ + TxOut { + script_pubkey: runestone.encipher(), + value: 0, + }, + TxOut { + script_pubkey: wallet.get_change_address()?.script_pubkey(), + value: postage.to_sat(), + }, + TxOut { + script_pubkey: destination.script_pubkey(), + value: postage.to_sat(), + }, + ] + } else { + vec![TxOut { script_pubkey: destination.script_pubkey(), - value: TARGET_POSTAGE.to_sat(), - }, - ], + value: postage.to_sat(), + }] + }, }; let unsigned_transaction = - fund_raw_transaction(bitcoin_client, fee_rate, &unfunded_transaction)?; + fund_raw_transaction(wallet.bitcoin_client(), fee_rate, &unfunded_transaction)?; let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?; - assert_eq!( - Runestone::decipher(&unsigned_transaction), - Some(Artifact::Runestone(runestone)), - ); + if needs_runes_change_output { + assert_eq!( + Runestone::decipher(&unsigned_transaction), + Some(Artifact::Runestone(runestone)), + ); + } Ok(unsigned_transaction) } diff --git a/src/templates.rs b/src/templates.rs index f01d722636..57f0db5030 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -21,7 +21,6 @@ pub(crate) use { }, range::RangeHtml, rare::RareTxt, - rune_balances::RuneBalancesHtml, sat::SatHtml, }; @@ -48,7 +47,6 @@ mod preview; mod range; mod rare; pub mod rune; -pub mod rune_balances; pub mod runes; pub mod sat; pub mod status; diff --git a/src/templates/block.rs b/src/templates/block.rs index f5d1294e74..1f2e8088f3 100644 --- a/src/templates/block.rs +++ b/src/templates/block.rs @@ -2,13 +2,14 @@ use super::*; #[derive(Boilerplate)] pub(crate) struct BlockHtml { - hash: BlockHash, - target: BlockHash, best_height: Height, block: Block, + featured_inscriptions: Vec, + hash: BlockHash, height: Height, inscription_count: usize, - featured_inscriptions: Vec, + runes: Vec, + target: BlockHash, } impl BlockHtml { @@ -18,6 +19,7 @@ impl BlockHtml { best_height: Height, inscription_count: usize, featured_inscriptions: Vec, + runes: Vec, ) -> Self { Self { hash: block.header.block_hash(), @@ -27,6 +29,7 @@ impl BlockHtml { best_height, inscription_count, featured_inscriptions, + runes, } } } @@ -49,6 +52,7 @@ mod tests { Height(0), Height(0), 0, + Vec::new(), Vec::new() ), " @@ -64,6 +68,7 @@ mod tests { prev next .* +

    0 Runes

    0 Inscriptions

    @@ -84,6 +89,7 @@ mod tests { Height(0), Height(1), 0, + Vec::new(), Vec::new() ), r"

    Block 0

    .*prev\s*.*" @@ -98,6 +104,7 @@ mod tests { Height(1), Height(1), 0, + Vec::new(), Vec::new() ), r"

    Block 1

    .*\s*next.*", diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 0536ddff47..a81431d48c 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -404,9 +404,9 @@ mod tests {

    Inscription 1

    .*
    - .*
    rune
    A•A
    + .*
    " .unindent() diff --git a/src/templates/output.rs b/src/templates/output.rs index ef13b6e2b8..b68e702733 100644 --- a/src/templates/output.rs +++ b/src/templates/output.rs @@ -6,7 +6,7 @@ pub(crate) struct OutputHtml { pub(crate) inscriptions: Vec, pub(crate) outpoint: OutPoint, pub(crate) output: TxOut, - pub(crate) runes: Vec<(SpacedRune, Pile)>, + pub(crate) runes: BTreeMap, pub(crate) sat_ranges: Option>, pub(crate) spent: bool, } @@ -32,7 +32,7 @@ mod tests { inscriptions: Vec::new(), outpoint: outpoint(1), output: TxOut { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: Some(vec![(0, 1), (1, 3)]), spent: false, }, @@ -66,7 +66,7 @@ mod tests { value: 1, script_pubkey: script::Builder::new().push_int(0).into_script(), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: None, spent: true, }, @@ -91,7 +91,7 @@ mod tests { inscriptions: Vec::new(), outpoint: outpoint(1), output: TxOut { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: Some(vec![(0, 1), (1, 3)]), spent: true, }, @@ -122,7 +122,7 @@ mod tests { inscriptions: Vec::new(), outpoint: outpoint(1), output: TxOut { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: None, spent: false, } @@ -152,7 +152,7 @@ mod tests { value: 3, script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), }, - runes: Vec::new(), + runes: BTreeMap::new(), sat_ranges: None, spent: false, }, @@ -191,7 +191,9 @@ mod tests { divisibility: 1, symbol: None, } - )], + )] + .into_iter() + .collect(), sat_ranges: None, spent: false, }, diff --git a/src/templates/rune.rs b/src/templates/rune.rs index e772ff83f1..c4333315fa 100644 --- a/src/templates/rune.rs +++ b/src/templates/rune.rs @@ -42,6 +42,7 @@ mod tests { }, symbol: Some('%'), timestamp: 0, + turbo: true, }, id: RuneId { block: 10, tx: 9 }, mintable: true, @@ -86,12 +87,16 @@ mod tests {
    100.123456889\u{A0}%
    premine
    0.123456789\u{A0}%
    +
    premine percentage
    +
    0.12%
    burned
    123456789.123456789\u{A0}%
    divisibility
    9
    symbol
    %
    +
    turbo
    +
    true
    etching
    0{64}
    parent
    @@ -120,6 +125,7 @@ mod tests { }, symbol: Some('%'), timestamp: 0, + turbo: false, }, id: RuneId { block: 10, tx: 9 }, mintable: false, @@ -134,6 +140,40 @@ mod tests { ); } + #[test] + fn display_no_turbo() { + assert_regex_match!( + RuneHtml { + entry: RuneEntry { + block: 0, + burned: 123456789123456789, + terms: None, + divisibility: 9, + etching: Txid::all_zeros(), + mints: 0, + number: 25, + premine: 0, + spaced_rune: SpacedRune { + rune: Rune(u128::MAX), + spacers: 1 + }, + symbol: Some('%'), + timestamp: 0, + turbo: false, + }, + id: RuneId { block: 10, tx: 9 }, + mintable: false, + parent: None, + }, + "

    B•CGDENLQRQWDSLRUGSNLBTMFIJAV

    +
    .* +
    turbo
    +
    false
    +.*
    +" + ); + } + #[test] fn display_empty_mint() { assert_regex_match!( @@ -158,6 +198,7 @@ mod tests { }, symbol: Some('%'), timestamp: 0, + turbo: false, }, id: RuneId { block: 10, tx: 9 }, mintable: false, diff --git a/src/templates/rune_balances.rs b/src/templates/rune_balances.rs deleted file mode 100644 index 0bd645bec9..0000000000 --- a/src/templates/rune_balances.rs +++ /dev/null @@ -1,102 +0,0 @@ -use super::*; - -#[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)] -pub struct RuneBalancesHtml { - pub balances: BTreeMap>, -} - -impl PageContent for RuneBalancesHtml { - fn title(&self) -> String { - "Rune Balances".to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const RUNE: u128 = 99246114928149462; - - #[test] - fn display_rune_balances() { - let balances: BTreeMap> = vec![ - ( - SpacedRune::new(Rune(RUNE), 0), - vec![( - OutPoint { - txid: txid(1), - vout: 1, - }, - Pile { - amount: 1000, - divisibility: 0, - symbol: Some('$'), - }, - )] - .into_iter() - .collect(), - ), - ( - SpacedRune::new(Rune(RUNE + 1), 0), - vec![( - OutPoint { - txid: txid(2), - vout: 2, - }, - Pile { - amount: 12345678, - divisibility: 1, - symbol: Some('¢'), - }, - )] - .into_iter() - .collect(), - ), - ] - .into_iter() - .collect(); - - assert_regex_match!( - RuneBalancesHtml { balances }.to_string(), - "

    Rune Balances

    - - - - - - - - - - - - - -
    runebalances
    .* - - - - - -
    - 1{64}:1 - - 1000\u{A0}\\$ -
    -
    .* - - - - - -
    - 2{64}:2 - - 1234567\\.8\u{A0}¢ -
    -
    -" - .unindent() - ); - } -} diff --git a/src/templates/runes.rs b/src/templates/runes.rs index 1e616605fc..bfc7e51760 100644 --- a/src/templates/runes.rs +++ b/src/templates/runes.rs @@ -3,6 +3,9 @@ use super::*; #[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)] pub struct RunesHtml { pub entries: Vec<(RuneId, RuneEntry)>, + pub more: bool, + pub prev: Option, + pub next: Option, } impl PageContent for RunesHtml { @@ -29,13 +32,62 @@ mod tests { ..default() } )], + more: false, + prev: None, + next: None, } .to_string(), "

    Runes

    -" +
    + prev + next +
    " + ); + } + + #[test] + fn with_prev_and_next() { + assert_eq!( + RunesHtml { + entries: vec![ + ( + RuneId { block: 0, tx: 0 }, + RuneEntry { + spaced_rune: SpacedRune { + rune: Rune(0), + spacers: 0 + }, + ..Default::default() + } + ), + ( + RuneId { block: 0, tx: 1 }, + RuneEntry { + spaced_rune: SpacedRune { + rune: Rune(2), + spacers: 0 + }, + ..Default::default() + } + ) + ], + prev: Some(1), + next: Some(2), + more: true, + } + .to_string(), + "

    Runes

    +
      +
    • A
    • +
    • C
    • +
    +
    + + +
    " ); } } diff --git a/src/test.rs b/src/test.rs index c4bea74599..dcf9127cfc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -93,7 +93,11 @@ impl From for Inscription { } pub(crate) fn inscription(content_type: &str, body: impl AsRef<[u8]>) -> Inscription { - Inscription::new(Some(content_type.into()), Some(body.as_ref().into())) + Inscription { + content_type: Some(content_type.into()), + body: Some(body.as_ref().into()), + ..default() + } } pub(crate) fn inscription_id(n: u32) -> InscriptionId { diff --git a/src/wallet.rs b/src/wallet.rs index b1ff75f0b3..cbca88c7fc 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -10,10 +10,6 @@ use { bitcoincore_rpc::bitcoincore_rpc_json::{Descriptor, ImportDescriptors, Timestamp}, entry::{EtchingEntry, EtchingEntryValue}, fee_rate::FeeRate, - futures::{ - future::{self, FutureExt}, - try_join, TryFutureExt, - }, index::entry::Entry, indicatif::{ProgressBar, ProgressStyle}, log::log_enabled, @@ -27,6 +23,7 @@ use { pub mod batch; pub mod entry; pub mod transaction_builder; +pub mod wallet_constructor; const SCHEMA_VERSION: u64 = 1; @@ -50,21 +47,13 @@ impl From for u64 { } } -#[derive(Clone)] -struct OrdClient { - url: Url, - client: reqwest::Client, -} - -impl OrdClient { - pub async fn get(&self, path: &str) -> Result { - self - .client - .get(self.url.join(path)?) - .send() - .map_err(|err| anyhow!(err)) - .await - } +#[derive(Debug, PartialEq)] +pub(crate) enum Maturity { + BelowMinimumHeight(u64), + CommitNotFound, + CommitSpent(Txid), + ConfirmationsPending(u32), + Mature, } pub(crate) struct Wallet { @@ -83,231 +72,6 @@ pub(crate) struct Wallet { } impl Wallet { - pub(crate) fn build( - name: String, - no_sync: bool, - settings: Settings, - rpc_url: Url, - ) -> Result { - let mut headers = HeaderMap::new(); - - headers.insert( - header::ACCEPT, - header::HeaderValue::from_static("application/json"), - ); - - if let Some((username, password)) = settings.credentials() { - let credentials = - base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}")); - headers.insert( - header::AUTHORIZATION, - header::HeaderValue::from_str(&format!("Basic {credentials}")).unwrap(), - ); - } - - let database = Self::open_database(&name, &settings)?; - - let ord_client = reqwest::blocking::ClientBuilder::new() - .default_headers(headers.clone()) - .build()?; - - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build()? - .block_on(async move { - let bitcoin_client = { - let client = Self::check_version(settings.bitcoin_rpc_client(Some(name.clone()))?)?; - - if !client.list_wallets()?.contains(&name) { - client.load_wallet(&name)?; - } - - Self::check_descriptors(&name, client.list_descriptors(None)?.descriptors)?; - - client - }; - - let async_ord_client = OrdClient { - url: rpc_url.clone(), - client: reqwest::ClientBuilder::new() - .default_headers(headers.clone()) - .build()?, - }; - - let chain_block_count = bitcoin_client.get_block_count().unwrap() + 1; - - if !no_sync { - for i in 0.. { - let response = async_ord_client.get("/blockcount").await?; - 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` after {i} attempts"); - } - tokio::time::sleep(Duration::from_millis(50)).await; - } - } - - let mut utxos = Self::get_utxos(&bitcoin_client)?; - let locked_utxos = Self::get_locked_utxos(&bitcoin_client)?; - utxos.extend(locked_utxos.clone()); - - let requests = utxos - .clone() - .into_keys() - .map(|output| (output, Self::get_output(&async_ord_client, output))) - .collect::>(); - - let futures = requests.into_iter().map(|(output, req)| async move { - let result = req.await; - (output, result) - }); - - let results = future::join_all(futures).await; - - let mut output_info = BTreeMap::new(); - for (output, result) in results { - let info = result?; - output_info.insert(output, info); - } - - let requests = output_info - .iter() - .flat_map(|(_output, info)| info.inscriptions.clone()) - .collect::>() - .into_iter() - .map(|id| (id, Self::get_inscription_info(&async_ord_client, id))) - .collect::>(); - - let futures = requests.into_iter().map(|(output, req)| async move { - let result = req.await; - (output, result) - }); - - let (results, status) = try_join!( - future::join_all(futures).map(Ok), - Self::get_server_status(&async_ord_client) - )?; - - let mut inscriptions = BTreeMap::new(); - let mut inscription_info = BTreeMap::new(); - for (id, result) in results { - let info = result?; - inscriptions - .entry(info.satpoint) - .or_insert_with(Vec::new) - .push(id); - - inscription_info.insert(id, info); - } - - Ok(Wallet { - bitcoin_client, - database, - has_rune_index: status.rune_index, - has_sat_index: status.sat_index, - inscription_info, - inscriptions, - locked_utxos, - ord_client, - output_info, - rpc_url, - settings, - utxos, - }) - }) - } - - async fn get_output(ord_client: &OrdClient, output: OutPoint) -> Result { - let response = ord_client.get(&format!("/output/{output}")).await?; - - if !response.status().is_success() { - bail!("wallet failed get output: {}", response.text().await?); - } - - let output_json: api::Output = serde_json::from_str(&response.text().await?)?; - - if !output_json.indexed { - bail!("output in wallet but not in ord server: {output}"); - } - - Ok(output_json) - } - - fn get_utxos(bitcoin_client: &Client) -> Result> { - Ok( - bitcoin_client - .list_unspent(None, None, None, None, None)? - .into_iter() - .map(|utxo| { - let outpoint = OutPoint::new(utxo.txid, utxo.vout); - let txout = TxOut { - script_pubkey: utxo.script_pub_key, - value: utxo.amount.to_sat(), - }; - - (outpoint, txout) - }) - .collect(), - ) - } - - fn get_locked_utxos(bitcoin_client: &Client) -> Result> { - #[derive(Deserialize)] - pub(crate) struct JsonOutPoint { - txid: Txid, - vout: u32, - } - - let outpoints = bitcoin_client.call::>("listlockunspent", &[])?; - - let mut utxos = BTreeMap::new(); - - for outpoint in outpoints { - let txout = bitcoin_client - .get_raw_transaction(&outpoint.txid, None)? - .output - .get(TryInto::::try_into(outpoint.vout).unwrap()) - .cloned() - .ok_or_else(|| anyhow!("Invalid output index"))?; - - utxos.insert(OutPoint::new(outpoint.txid, outpoint.vout), txout); - } - - Ok(utxos) - } - - async fn get_inscription_info( - ord_client: &OrdClient, - inscription_id: InscriptionId, - ) -> Result { - let response = ord_client - .get(&format!("/inscription/{inscription_id}")) - .await?; - - if !response.status().is_success() { - bail!("inscription {inscription_id} not found"); - } - - Ok(serde_json::from_str(&response.text().await?)?) - } - - async fn get_server_status(ord_client: &OrdClient) -> Result { - let response = ord_client.get("/status").await?; - - if !response.status().is_success() { - bail!("could not get status: {}", response.text().await?) - } - - Ok(serde_json::from_str(&response.text().await?)?) - } - pub(crate) fn get_output_sat_ranges(&self) -> Result)>> { ensure!( self.has_sat_index, @@ -462,10 +226,10 @@ impl Wallet { Ok(runic_outputs) } - pub(crate) fn get_runes_balances_for_output( + pub(crate) fn get_runes_balances_in_output( &self, output: &OutPoint, - ) -> Result> { + ) -> Result> { Ok( self .output_info @@ -476,22 +240,6 @@ impl Wallet { ) } - pub(crate) fn get_rune_balance_in_output(&self, output: &OutPoint, rune: Rune) -> Result { - Ok( - self - .get_runes_balances_for_output(output)? - .iter() - .map(|(spaced_rune, pile)| { - if spaced_rune.rune == rune { - pile.amount - } else { - 0 - } - }) - .sum(), - ) - } - pub(crate) fn get_rune( &self, rune: Rune, @@ -541,44 +289,87 @@ impl Wallet { 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); - } + fn is_above_minimum_at_height(&self, rune: Rune) -> Result { + Ok( + rune + >= Rune::minimum_at_height( + self.chain().network(), + Height(u32::try_from(self.bitcoin_client().get_block_count()? + 1).unwrap()), + ), + ) + } - let transaction = self + pub(crate) fn check_maturity(&self, rune: Rune, commit: &Transaction) -> Result { + Ok( + if let Some(commit_tx) = 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() + .into_option()? + { + let current_confirmations = u32::try_from(commit_tx.info.confirmations)?; + if self + .bitcoin_client() + .get_tx_out(&commit.txid(), 0, Some(true))? + .is_none() { - continue; + Maturity::CommitSpent(commit_tx.info.txid) + } else if !self.is_above_minimum_at_height(rune)? { + Maturity::BelowMinimumHeight(self.bitcoin_client().get_block_count()? + 1) + } else if current_confirmations + 1 < Runestone::COMMIT_CONFIRMATIONS.into() { + Maturity::ConfirmationsPending( + u32::from(Runestone::COMMIT_CONFIRMATIONS) - current_confirmations - 1, + ) + } else { + Maturity::Mature } - } + } else { + Maturity::CommitNotFound + }, + ) + } - let tx_out = self - .bitcoin_client() - .get_tx_out(&commit.txid(), 0, Some(true))?; + pub(crate) fn wait_for_maturation(&self, rune: Rune) -> Result { + let Some(entry) = self.load_etching(rune)? else { + bail!("no etching found"); + }; + + eprintln!( + "Waiting for rune {} commitment {} to mature…", + rune, + entry.commit.txid() + ); + + let mut pending_confirmations: u32 = Runestone::COMMIT_CONFIRMATIONS.into(); + + let progress = ProgressBar::new(pending_confirmations.into()).with_style( + ProgressStyle::default_bar() + .template("Maturing in...[{eta}] {spinner:.green} [{bar:40.cyan/blue}] {pos}/{len}") + .unwrap() + .progress_chars("█▓▒░ "), + ); + + loop { + if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) { + eprintln!("Suspending batch. Run `ord wallet resume` to continue."); + return Ok(entry.output); + } - if let Some(tx_out) = tx_out { - if tx_out.confirmations >= Runestone::COMMIT_INTERVAL.into() { + match self.check_maturity(rune, &entry.commit)? { + Maturity::Mature => { + progress.finish_with_message("Rune matured, submitting..."); break; } + Maturity::ConfirmationsPending(remaining) => { + if remaining < pending_confirmations { + pending_confirmations = remaining; + progress.inc(1); + } + } + Maturity::CommitSpent(txid) => { + self.clear_etching(rune)?; + bail!("rune commitment {} spent, can't send reveal tx", txid); + } + _ => {} } if !self.integration_test() { @@ -586,12 +377,16 @@ impl Wallet { } } - match self.bitcoin_client().send_raw_transaction(&reveal) { + self.send_etching(rune, &entry) + } + + pub(crate) fn send_etching(&self, rune: Rune, entry: &EtchingEntry) -> Result { + match self.bitcoin_client().send_raw_transaction(&entry.reveal) { Ok(txid) => txid, Err(err) => { return Err(anyhow!( "Failed to send reveal transaction: {err}\nCommit tx {} will be recovered once mined", - commit.txid() + entry.commit.txid() )) } }; @@ -600,7 +395,7 @@ impl Wallet { Ok(batch::Output { reveal_broadcast: true, - ..output + ..entry.output.clone() }) } @@ -762,7 +557,10 @@ impl Wallet { } pub(crate) fn open_database(wallet_name: &String, settings: &Settings) -> Result { - let path = settings.data_dir().join(format!("{wallet_name}.redb")); + let path = settings + .data_dir() + .join("wallets") + .join(format!("{wallet_name}.redb")); if let Err(err) = fs::create_dir_all(path.parent().unwrap()) { bail!( @@ -888,7 +686,7 @@ impl Wallet { ) } - pub(crate) fn clear_etching(&self, rune: &Rune) -> Result { + pub(crate) fn clear_etching(&self, rune: Rune) -> Result { let wtx = self.database.begin_write()?; wtx.open_table(RUNE_TO_ETCHING)?.remove(rune.0)?; diff --git a/src/wallet/batch/entry.rs b/src/wallet/batch/entry.rs index cba1bb537b..24839f8fb6 100644 --- a/src/wallet/batch/entry.rs +++ b/src/wallet/batch/entry.rs @@ -5,7 +5,7 @@ use super::*; pub struct Entry { pub delegate: Option, pub destination: Option>, - pub file: PathBuf, + pub file: Option, pub metadata: Option, pub metaprotocol: Option, pub satpoint: Option, diff --git a/src/wallet/batch/etching.rs b/src/wallet/batch/etching.rs index 700e90eaf9..8b8e8db339 100644 --- a/src/wallet/batch/etching.rs +++ b/src/wallet/batch/etching.rs @@ -8,5 +8,6 @@ pub struct Etching { pub rune: SpacedRune, pub supply: Decimal, pub symbol: char, - pub terms: Option, + pub terms: Option, + pub turbo: bool, } diff --git a/src/wallet/batch/file.rs b/src/wallet/batch/file.rs index 68f8bae4f3..d5d1eada17 100644 --- a/src/wallet/batch/file.rs +++ b/src/wallet/batch/file.rs @@ -9,7 +9,7 @@ pub struct File { pub postage: Option, #[serde(default)] pub reinscribe: bool, - pub etching: Option, + pub etching: Option, pub sat: Option, pub satpoint: Option, } @@ -129,14 +129,14 @@ impl File { } } - inscriptions.push(Inscription::from_file( + inscriptions.push(Inscription::new( wallet.chain(), compress, entry.delegate, entry.metadata()?, entry.metaprotocol.clone(), self.parent.into_iter().collect(), - &entry.file, + entry.file.clone(), Some(pointer), self .etching @@ -146,7 +146,7 @@ impl File { let postage = if self.mode == Mode::SatPoints { let satpoint = entry .satpoint - .ok_or_else(|| anyhow!("no satpoint specified for {}", entry.file.display()))?; + .ok_or_else(|| anyhow!("no satpoint specified for entry {i}"))?; let txout = utxos .get(&satpoint.outpoint) @@ -393,10 +393,11 @@ inscriptions: end: Some(9000), }), }), + turbo: true, }), inscriptions: vec![ batch::Entry { - file: "mango.avif".into(), + file: Some("mango.avif".into()), delegate: Some( "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0" .parse() @@ -424,12 +425,12 @@ inscriptions: ..default() }, batch::Entry { - file: "token.json".into(), + file: Some("token.json".into()), metaprotocol: Some("DOPEPROTOCOL-42069".into()), ..default() }, batch::Entry { - file: "tulip.png".into(), + file: Some("tulip.png".into()), destination: Some( "bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6" .parse() @@ -446,4 +447,21 @@ inscriptions: } ); } + + #[test] + fn batchfile_no_delegate_no_file_allowed() { + let tempdir = TempDir::new().unwrap(); + let batch_file = tempdir.path().join("batch.yaml"); + fs::write( + batch_file.clone(), + r#" +mode: shared-output +inscriptions: + - +"#, + ) + .unwrap(); + + assert!(batch::File::load(batch_file.as_path()).is_ok()); + } } diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index 1f51127464..704edc1ccd 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -48,6 +48,7 @@ impl Plan { ) -> SubcommandResult { let Transactions { commit_tx, + commit_vout, reveal_tx, recovery_key_pair, total_fees, @@ -128,13 +129,18 @@ impl Plan { .send_raw_transaction(&signed_commit_tx)?; if let Some(ref rune_info) = rune { + wallet.bitcoin_client().lock_unspent(&[OutPoint { + txid: commit_txid, + vout: commit_vout.try_into().unwrap(), + }])?; + let commit = consensus::encode::deserialize::(&signed_commit_tx)?; let reveal = consensus::encode::deserialize::(&signed_reveal_tx)?; - Ok(Some(Box::new(wallet.wait_for_maturation( + wallet.save_etching( &rune_info.rune.rune, - commit.clone(), - reveal.clone(), + &commit, + &reveal, self.output( commit.txid(), None, @@ -145,7 +151,11 @@ impl Plan { self.inscriptions.clone(), rune.clone(), ), - )?))) + )?; + + Ok(Some(Box::new( + wallet.wait_for_maturation(rune_info.rune.rune)?, + ))) } else { let reveal = match wallet .bitcoin_client() @@ -446,6 +456,10 @@ impl Plan { edicts: Vec::new(), etching: Some(ordinals::Etching { divisibility: (etching.divisibility > 0).then_some(etching.divisibility), + 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), terms: etching .terms .map(|terms| -> Result { @@ -463,10 +477,7 @@ impl Plan { }) }) .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), + turbo: etching.turbo, }), mint: None, pointer: (premine > 0).then_some((reveal_outputs.len() - 1).try_into().unwrap()), @@ -656,6 +667,7 @@ impl Plan { Ok(Transactions { commit_tx: unsigned_commit_tx, + commit_vout: vout, recovery_key_pair, reveal_tx, rune, @@ -711,7 +723,7 @@ impl Plan { script_sig: script::Builder::new().into_script(), witness: Witness::new(), sequence: if etching { - Sequence::from_height(Runestone::COMMIT_INTERVAL) + Sequence::from_height(Runestone::COMMIT_CONFIRMATIONS - 1) } else { Sequence::ENABLE_RBF_NO_LOCKTIME }, diff --git a/src/wallet/batch/transactions.rs b/src/wallet/batch/transactions.rs index 72f1416b67..28e3a1628f 100644 --- a/src/wallet/batch/transactions.rs +++ b/src/wallet/batch/transactions.rs @@ -4,6 +4,7 @@ use super::*; pub(crate) struct Transactions { pub(crate) rune: Option, pub(crate) commit_tx: Transaction, + pub(crate) commit_vout: usize, pub(crate) recovery_key_pair: TweakedKeyPair, pub(crate) reveal_tx: Transaction, pub(crate) total_fees: u64, diff --git a/src/wallet/wallet_constructor.rs b/src/wallet/wallet_constructor.rs new file mode 100644 index 0000000000..b42c979d19 --- /dev/null +++ b/src/wallet/wallet_constructor.rs @@ -0,0 +1,238 @@ +use super::*; + +#[derive(Clone)] +pub(crate) struct WalletConstructor { + ord_client: reqwest::blocking::Client, + name: String, + no_sync: bool, + rpc_url: Url, + settings: Settings, +} + +impl WalletConstructor { + pub(crate) fn construct( + name: String, + no_sync: bool, + settings: Settings, + rpc_url: Url, + ) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + header::ACCEPT, + header::HeaderValue::from_static("application/json"), + ); + + if let Some((username, password)) = settings.credentials() { + let credentials = + base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}")); + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(&format!("Basic {credentials}")).unwrap(), + ); + } + + Self { + ord_client: reqwest::blocking::ClientBuilder::new() + .timeout(None) + .default_headers(headers.clone()) + .build()?, + name, + no_sync, + rpc_url, + settings, + } + .build() + } + + pub(crate) fn build(self) -> Result { + let database = Wallet::open_database(&self.name, &self.settings)?; + + let bitcoin_client = { + let client = + Wallet::check_version(self.settings.bitcoin_rpc_client(Some(self.name.clone()))?)?; + + if !client.list_wallets()?.contains(&self.name) { + client.load_wallet(&self.name)?; + } + + if client.get_wallet_info()?.private_keys_enabled { + Wallet::check_descriptors(&self.name, client.list_descriptors(None)?.descriptors)?; + } + + client + }; + + let chain_block_count = bitcoin_client.get_block_count().unwrap() + 1; + + if !self.no_sync { + for i in 0.. { + let response = self.get("/blockcount")?; + + if response + .text()? + .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` after {i} attempts"); + } + std::thread::sleep(Duration::from_millis(50)); + } + } + + let mut utxos = Self::get_utxos(&bitcoin_client)?; + let locked_utxos = Self::get_locked_utxos(&bitcoin_client)?; + utxos.extend(locked_utxos.clone()); + + let output_info = self.get_output_info(utxos.clone().into_keys().collect())?; + + let inscriptions = output_info + .iter() + .flat_map(|(_output, info)| info.inscriptions.clone()) + .collect::>(); + + let (inscriptions, inscription_info) = self.get_inscriptions(&inscriptions)?; + + let status = self.get_server_status()?; + + Ok(Wallet { + bitcoin_client, + database, + has_rune_index: status.rune_index, + has_sat_index: status.sat_index, + inscription_info, + inscriptions, + locked_utxos, + ord_client: self.ord_client, + output_info, + rpc_url: self.rpc_url, + settings: self.settings, + utxos, + }) + } + + fn get_output_info(&self, outputs: Vec) -> Result> { + let response = self.post("/outputs", &outputs)?; + + if !response.status().is_success() { + bail!("wallet failed get outputs: {}", response.text()?); + } + + let output_info: BTreeMap = outputs + .into_iter() + .zip(serde_json::from_str::>(&response.text()?)?) + .collect(); + + for (output, info) in &output_info { + if !info.indexed { + bail!("output in wallet but not in ord server: {output}"); + } + } + + Ok(output_info) + } + + fn get_inscriptions( + &self, + inscriptions: &Vec, + ) -> Result<( + BTreeMap>, + BTreeMap, + )> { + let response = self.post("/inscriptions", inscriptions)?; + + if !response.status().is_success() { + bail!("wallet failed get inscriptions: {}", response.text()?); + } + + let mut inscriptions = BTreeMap::new(); + let mut inscription_infos = BTreeMap::new(); + for info in serde_json::from_str::>(&response.text()?)? { + inscriptions + .entry(info.satpoint) + .or_insert_with(Vec::new) + .push(info.id); + + inscription_infos.insert(info.id, info); + } + + Ok((inscriptions, inscription_infos)) + } + + fn get_utxos(bitcoin_client: &Client) -> Result> { + Ok( + bitcoin_client + .list_unspent(None, None, None, None, None)? + .into_iter() + .map(|utxo| { + let outpoint = OutPoint::new(utxo.txid, utxo.vout); + let txout = TxOut { + script_pubkey: utxo.script_pub_key, + value: utxo.amount.to_sat(), + }; + + (outpoint, txout) + }) + .collect(), + ) + } + + fn get_locked_utxos(bitcoin_client: &Client) -> Result> { + #[derive(Deserialize)] + pub(crate) struct JsonOutPoint { + txid: Txid, + vout: u32, + } + + let outpoints = bitcoin_client.call::>("listlockunspent", &[])?; + + let mut utxos = BTreeMap::new(); + + for outpoint in outpoints { + let Some(tx_out) = bitcoin_client.get_tx_out(&outpoint.txid, outpoint.vout, Some(false))? + else { + continue; + }; + + utxos.insert( + OutPoint::new(outpoint.txid, outpoint.vout), + TxOut { + value: tx_out.value.to_sat(), + script_pubkey: ScriptBuf::from_bytes(tx_out.script_pub_key.hex), + }, + ); + } + + Ok(utxos) + } + + fn get_server_status(&self) -> Result { + let response = self.get("/status")?; + + if !response.status().is_success() { + bail!("could not get status: {}", response.text()?) + } + + Ok(serde_json::from_str(&response.text()?)?) + } + + pub fn get(&self, path: &str) -> Result { + self + .ord_client + .get(self.rpc_url.join(path)?) + .send() + .map_err(|err| anyhow!(err)) + } + + pub fn post(&self, path: &str, body: &impl Serialize) -> Result { + self + .ord_client + .post(self.rpc_url.join(path)?) + .json(body) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .map_err(|err| anyhow!(err)) + } +} diff --git a/static/rune.svg b/static/rune.svg new file mode 100644 index 0000000000..b0423c6605 --- /dev/null +++ b/static/rune.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/block.html b/templates/block.html index dec589f8fb..bf33f2561e 100644 --- a/templates/block.html +++ b/templates/block.html @@ -21,6 +21,14 @@

    Block {{ self.height }}

    next %% } +

    {{"Rune".tally(self.runes.len())}}

    +%% if self.runes.len() > 0 { + +%% }

    {{"Inscription".tally(self.inscription_count)}}

    %% for id in &self.featured_inscriptions { diff --git a/templates/inscription.html b/templates/inscription.html index 09000ac48a..fd044035e2 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -38,6 +38,10 @@

    Inscription {{ self.number }}

    all
    +%% } +%% if let Some(rune) = self.rune { +
    rune
    +
    {{ rune }}
    %% }
    id
    {{ self.id }}
    @@ -111,8 +115,4 @@

    Inscription {{ self.number }}

    {{ self.satpoint.offset }}
    ethereum teleburn address
    {{ teleburn::Ethereum::from(self.id) }}
    -%% if let Some(rune) = self.rune { -
    rune
    -
    {{ rune }}
    -%% } diff --git a/templates/page.html b/templates/page.html index 5139453080..0456d99498 100644 --- a/templates/page.html +++ b/templates/page.html @@ -20,6 +20,7 @@