diff --git a/CHANGELOG.md b/CHANGELOG.md index cef3bb6103..a266c81a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,34 @@ Changelog ========= +[0.15.0](https://github.com/ordinals/ord/releases/tag/0.15.0) - 2023-01-08 +-------------------------------------------------------------------------- + +### Added +- Add no sync option to server command (#2966) +- Vindicate cursed inscriptions (#2950) +- Add JSON endpoints for Runes (#2941) +- Add JSON endpoint for status (#2955) +- Add chain to status page (#2953) + +### Changed +- Enter beta (#2973) + +### Performance +- Avoid skip when getting paginated inscriptions (#2975) +- Dispatch requests to tokio thread pool (#2974) + +### Misc +- Fix Project Board link (#2991) +- Update server names in justfile (#2954) +- Update delegate.md (#2976) +- Fix a typo (#2980) +- Use enums for runestone tags and flags (#2956) +- Make `FundRawTransactionOptions ` public (#2938) +- Deduplicate deploy script case statements (#2962) +- Remove quotes around key to allow shell expansion (#2951) +- Restart sshd in deploy script (#2952) + [0.14.1](https://github.com/ordinals/ord/releases/tag/0.14.1) - 2023-01-03 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 7fbaa9f339..5e1bdc7b37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" dependencies = [ "concurrent-queue", - "event-listener 4.0.2", + "event-listener 4.0.3", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -239,7 +239,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" dependencies = [ - "event-listener 4.0.2", + "event-listener 4.0.3", "event-listener-strategy", "pin-project-lite", ] @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.6.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" @@ -269,7 +269,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -288,7 +288,7 @@ dependencies = [ "js-sys", "lazy_static", "log", - "rustls 0.22.1", + "rustls 0.22.2", "rustls-pki-types", "thiserror", "wasm-bindgen", @@ -443,9 +443,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" [[package]] name = "bech32" @@ -560,7 +560,7 @@ dependencies = [ "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.2.0", "piper", "tracing", ] @@ -576,7 +576,7 @@ dependencies = [ "new_mime_guess", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -699,9 +699,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.12" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +checksum = "33e92c5c1a78c62968ec57dbc2440366a2d6e5a23faf829970ff1585dc6b18e2" dependencies = [ "clap_builder", "clap_derive", @@ -709,9 +709,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "f4323769dc8a61e2c39ad7dc26f6f2800524691a44d74fe3d1071a5c24db6370" dependencies = [ "anstream", "anstyle", @@ -728,7 +728,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -799,9 +799,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -853,34 +853,28 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if 1.0.0", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.17" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if 1.0.0", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" -dependencies = [ - "cfg-if 1.0.0", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -947,7 +941,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -969,7 +963,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1099,7 +1093,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1160,9 +1154,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "218a870470cce1469024e9fb66b901aa983929d81304a1cdb299f28118e550d5" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" dependencies = [ "concurrent-queue", "parking", @@ -1175,7 +1169,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ - "event-listener 4.0.2", + "event-listener 4.0.3", "pin-project-lite", ] @@ -1305,9 +1299,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" dependencies = [ "futures-core", "pin-project-lite", @@ -1321,7 +1315,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1331,7 +1325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3afda89bce8f65072d24f8b99a2127e229462d8008182ca93f1d5d2e5df8f22f" dependencies = [ "futures-io", - "rustls 0.22.1", + "rustls 0.22.2", "rustls-pki-types", ] @@ -1485,7 +1479,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", "bytes", "headers-core", "http 0.2.11", @@ -1839,9 +1833,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libredox" @@ -2147,7 +2141,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2176,13 +2170,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord-litecoin" -version = "0.14.1" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", "axum", "axum-server", - "base64 0.21.5", + "base64 0.21.6", "bech32", "bip39", "bitcoin", @@ -2218,7 +2212,7 @@ dependencies = [ "reqwest", "rss", "rust-embed", - "rustls 0.22.1", + "rustls 0.22.2", "rustls-acme", "serde", "serde_json", @@ -2297,7 +2291,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2412,9 +2406,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.74" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] @@ -2586,7 +2580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "async-compression", - "base64 0.21.5", + "base64 0.21.6", "bytes", "encoding_rs", "futures-core", @@ -2680,7 +2674,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.46", + "syn 2.0.48", "walkdir", ] @@ -2759,14 +2753,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6b63262c9fcac8659abfaa96cac103d28166d3ff3eaf8f412e19f3ae9e5a48" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", "ring 0.17.7", "rustls-pki-types", - "rustls-webpki 0.102.0", + "rustls-webpki 0.102.1", "subtle", "zeroize", ] @@ -2806,7 +2800,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.6", ] [[package]] @@ -2827,9 +2821,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.0" +version = "0.102.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2635c8bc2b88d367767c5de8ea1d8db9af3f6219eba28442242d9ab81d1b89" +checksum = "ef4ca26037c909dedb327b48c3327d0ba91d3dd3c4e05dad328f210ffb68e95b" dependencies = [ "ring 0.17.7", "rustls-pki-types", @@ -2934,29 +2928,29 @@ checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.110" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "indexmap", "itoa", @@ -3125,9 +3119,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.46" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -3154,9 +3148,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.3" +version = "0.30.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2dbd2894d23b2d78dae768d85e323b557ac3ac71a5d917a31536d8f77ebada" +checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys", @@ -3243,7 +3237,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3325,7 +3319,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3631,7 +3625,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3665,7 +3659,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 76d4c42f28..2743804389 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord-litecoin" description = "◉ Ordinal wallet and block explorer for litecoin" -version = "0.14.1" +version = "0.15.0" license = "CC0-1.0" edition = "2021" autotests = false diff --git a/README.md b/README.md index df21aa763c..79e091c2a6 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,10 @@ See [the docs](https://docs.ordinals.com) for documentation and guides. See [the BIP](bip.mediawiki) for a technical description of the assignment and transfer algorithm. -See [the project board](https://github.com/users/casey/projects/3/) for +See [the project board](https://github.com/orgs/ordinals/projects/1) for currently prioritized issues. -See [milestones](https://github.com/ordinals/ord/milestones) to get a sense of -where the project is and where it's going. - -Join [the Discord server](https://ordlite.com) to chat with fellow +Join [the Discord server](https://discord.gg/87cjuz4FYg) to chat with fellow ordinal degenerates. Donate diff --git a/deploy/setup b/deploy/setup index 4725b924d7..c558dcb4a6 100755 --- a/deploy/setup +++ b/deploy/setup @@ -10,37 +10,8 @@ BRANCH=$3 COMMIT=$4 REVISION="ord-$BRANCH-$COMMIT" -case $CHAIN in - main) - CSP_ORIGIN=ordinals.com - ;; - regtest) - CSP_ORIGIN=regtest.ordinals.com - ;; - signet) - CSP_ORIGIN=signet.ordinals.com - ;; - test) - CSP_ORIGIN=testnet.ordinals.com - ;; - *) - echo "Unknown chain: $CHAIN" - exit 1 - ;; -esac - touch ~/.hushlogin -sed -i -E 's/#?PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config - -mkdir -p \ - /etc/systemd/system/litecoind.service.d \ - /etc/systemd/system/ord.service.d - -printf "[Service]\nEnvironment=CHAIN=%s\nEnvironment=CSP_ORIGIN=%s\n" $CHAIN $CSP_ORIGIN \ - | tee /etc/systemd/system/litecoind.service.d/override.conf \ - > /etc/systemd/system/ord.service.d/override.conf - hostnamectl set-hostname $DOMAIN apt-get install --yes \ @@ -64,15 +35,23 @@ ufw allow ssh case $CHAIN in main) + COOKIE_FILE_DIR=/var/lib/litecoind + CSP_ORIGIN=ordinals.com ufw allow 9333 ;; regtest) + COOKIE_FILE_DIR=/var/lib/litecoind/regtest + CSP_ORIGIN=regtest.ordinals.com ufw allow 18444 ;; signet) + COOKIE_FILE_DIR=/var/lib/litecoind/signet + CSP_ORIGIN=signet.ordinals.com ufw allow 39333 ;; test) + COOKIE_FILE_DIR=/var/lib/litecoind/testnet3 + CSP_ORIGIN=testnet.ordinals.com ufw allow 19333 ;; *) @@ -81,6 +60,18 @@ case $CHAIN in ;; esac +mkdir -p \ + /etc/systemd/system/litecoind.service.d \ + /etc/systemd/system/ord.service.d + +printf "[Service]\nEnvironment=CHAIN=%s\nEnvironment=CSP_ORIGIN=%s\n" $CHAIN $CSP_ORIGIN \ + | tee /etc/systemd/system/litecoind.service.d/override.conf \ + > /etc/systemd/system/ord.service.d/override.conf + +sed -i -E 's/#?PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config +sshd -t +systemctl restart sshd + ufw --force enable if ! which litecoind; then @@ -117,25 +108,6 @@ systemctl daemon-reload systemctl enable litecoind systemctl restart litecoind -case $CHAIN in - main) - COOKIE_FILE_DIR=/var/lib/litecoind - ;; - regtest) - COOKIE_FILE_DIR=/var/lib/litecoind/regtest - ;; - signet) - COOKIE_FILE_DIR=/var/lib/litecoind/signet - ;; - test) - COOKIE_FILE_DIR=/var/lib/litecoind/testnet3 - ;; - *) - echo "Unknown chain: $CHAIN" - exit 1 - ;; -esac - while [[ ! -f $COOKIE_FILE_DIR/.cookie ]]; do echo "Waiting for litecoind…" sleep 1 diff --git a/docs/po/zh.po b/docs/po/zh.po index 052df5cc25..e0871b9c07 100644 --- a/docs/po/zh.po +++ b/docs/po/zh.po @@ -4058,7 +4058,7 @@ msgstr "" msgid "" "Teleburn addresses are derived from inscription IDs. They have no " "corresponding private key, so assets sent to a teleburn address are burned. " -"Currently, only Ethereum teleburn addresses are suppported. Pull requests " +"Currently, only Ethereum teleburn addresses are supported. Pull requests " "adding teleburn addresses for other chains are welcome." msgstr "" "Teleburn 地址源自铭文的ID,他们没有私钥,因此发往燃烧传送地址的资产将被烧毁 " diff --git a/docs/src/inscriptions/delegate.md b/docs/src/inscriptions/delegate.md index 55fc800035..51b17ab30a 100644 --- a/docs/src/inscriptions/delegate.md +++ b/docs/src/inscriptions/delegate.md @@ -33,7 +33,7 @@ OP_IF OP_ENDIF ``` -Note that the value of tag `11` is binary, not hex. +Note that the value of tag `11` is decimal, not hex. The delegate field value uses the same encoding as the parent field. See [provenance](provenance.md) for more examples of inscrpition ID encodings; diff --git a/justfile b/justfile index 71da16a57d..296f35809d 100644 --- a/justfile +++ b/justfile @@ -37,14 +37,16 @@ deploy-signet branch='master' remote='ordinals/ord': (deploy branch remote 'sign deploy-testnet branch='master' remote='ordinals/ord': (deploy branch remote 'test' 'testnet.ordinals.net') +servers := 'alpha bravo charlie regtest signet testnet' + initialize-server-keys: #!/usr/bin/env bash set -euxo pipefail rm -rf tmp/ssh mkdir -p tmp/ssh ssh-keygen -C ordinals -f tmp/ssh/id_ed25519 -t ed25519 -N '' - for server in alpha balance regtest signet stability testnet; do - ssh-copy-id -i tmp/ssh/id_ed25519.pub root@$SERVER.ordinals.net + for server in {{ servers }}; do + ssh-copy-id -i tmp/ssh/id_ed25519.pub root@$server.ordinals.net scp tmp/ssh/* root@$server.ordinals.net:.ssh done rm -rf tmp/ssh @@ -52,8 +54,15 @@ initialize-server-keys: install-personal-key key='~/.ssh/id_ed25519.pub': #!/usr/bin/env bash set -euxo pipefail - for server in alpha balance regtest signet stability testnet; do - ssh-copy-id -i '{{ key }}' root@$server.ordinals.net + for server in {{ servers }}; do + ssh-copy-id -i {{ key }} root@$server.ordinals.net + done + +server-keys: + #!/usr/bin/env bash + set -euxo pipefail + for server in {{ servers }}; do + ssh root@$server.ordinals.net cat .ssh/authorized_keys done log unit='ord' domain='alpha.ordinals.net': diff --git a/src/index.rs b/src/index.rs index dea86b1ae6..9cd0734414 100644 --- a/src/index.rs +++ b/src/index.rs @@ -11,7 +11,7 @@ use { super::*, crate::{ subcommand::{find::FindRangeOutput, server::InscriptionQuery}, - templates::{RuneHtml, StatusHtml}, + templates::StatusHtml, }, bitcoin::block::Header, bitcoincore_rpc::{json::GetBlockHeaderResult, Client}, @@ -30,7 +30,7 @@ use { }, }; -pub(crate) use self::entry::RuneEntry; +pub use self::entry::RuneEntry; pub(crate) mod entry; mod fetcher; @@ -87,18 +87,18 @@ pub enum List { #[derive(Copy, Clone)] pub(crate) enum Statistic { Schema = 0, - BlessedInscriptions, - Commits, - CursedInscriptions, - IndexRunes, - IndexSats, - LostSats, - OutputsTraversed, - ReservedRunes, - Runes, - SatRanges, - UnboundInscriptions, - IndexTransactions, + BlessedInscriptions = 1, + Commits = 2, + CursedInscriptions = 3, + IndexRunes = 4, + IndexSats = 5, + LostSats = 6, + OutputsTraversed = 7, + ReservedRunes = 8, + Runes = 9, + SatRanges = 10, + UnboundInscriptions = 11, + IndexTransactions = 12, } impl Statistic { @@ -431,6 +431,7 @@ impl Index { Ok(StatusHtml { blessed_inscriptions, + chain: self.options.chain(), cursed_inscriptions, height, inscriptions: blessed_inscriptions + cursed_inscriptions, @@ -854,29 +855,10 @@ impl Index { ) } - pub(crate) fn rune(&self, rune: Rune) -> Result> { - let rtx = self.database.begin_read()?; - - let Some(id) = rtx - .open_table(RUNE_TO_RUNE_ID)? - .get(rune.0)? - .map(|guard| guard.value()) - else { - return Ok(None); - }; - - let entry = RuneEntry::load( - rtx - .open_table(RUNE_ID_TO_RUNE_ENTRY)? - .get(id)? - .unwrap() - .value(), - ); - - Ok(Some((RuneId::load(id), entry))) - } - - pub(crate) fn rune_html(&self, rune: Rune) -> Result> { + pub(crate) fn rune( + &self, + rune: Rune, + ) -> Result)>> { let rtx = self.database.begin_read()?; let Some(id) = rtx @@ -906,11 +888,7 @@ impl Index { .is_some() .then_some(parent); - Ok(Some(RuneHtml { - entry, - id: RuneId::load(id), - parent, - })) + Ok(Some((RuneId::load(id), entry, parent))) } pub(crate) fn runes(&self) -> Result> { @@ -1678,23 +1656,32 @@ impl Index { pub(crate) fn get_inscriptions_paginated( &self, - page_size: usize, - page_index: usize, + page_size: u32, + page_index: u32, ) -> Result<(Vec, bool)> { let rtx = self.database.begin_read()?; let sequence_number_to_inscription_entry = rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?; - let mut inscriptions = sequence_number_to_inscription_entry + let last = sequence_number_to_inscription_entry .iter()? + .next_back() + .map(|result| result.map(|(number, _entry)| number.value())) + .transpose()? + .unwrap_or_default(); + + let start = last.saturating_sub(page_size.saturating_mul(page_index)); + + let end = start.saturating_sub(page_size); + + let mut inscriptions = sequence_number_to_inscription_entry + .range(end..=start)? .rev() - .skip(page_size.saturating_mul(page_index)) - .take(page_size.saturating_add(1)) - .flat_map(|result| result.map(|(_number, entry)| InscriptionEntry::load(entry.value()).id)) - .collect::>(); + .map(|result| result.map(|(_number, entry)| InscriptionEntry::load(entry.value()).id)) + .collect::, StorageError>>()?; - let more = inscriptions.len() > page_size; + let more = u32::try_from(inscriptions.len()).unwrap_or(u32::MAX) > page_size; if more { inscriptions.pop(); @@ -3366,7 +3353,7 @@ mod tests { } #[test] - fn get_latest_inscriptions_with_no_prev_and_next() { + fn get_latest_inscriptions_with_no_more() { for context in Context::configurations() { context.mine_blocks(1); @@ -3384,6 +3371,33 @@ mod tests { } } + #[test] + fn get_latest_inscriptions_with_more() { + for context in Context::configurations() { + context.mine_blocks(1); + + let mut ids = Vec::new(); + + for i in 0..101 { + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())], + ..Default::default() + }); + context.mine_blocks(1); + ids.push(InscriptionId { txid, index: 0 }); + } + + ids.reverse(); + ids.pop(); + + assert_eq!(ids.len(), 100); + + let (inscriptions, more) = context.index.get_inscriptions_paginated(100, 0).unwrap(); + assert_eq!(inscriptions, ids); + assert!(more); + } + } + // #[test] // fn unsynced_index_fails() { // for context in Context::configurations() { @@ -3404,7 +3418,7 @@ mod tests { // ) // .unwrap_err() // .to_string(), - // r"output in Litecoin Core wallet but not in ord index: [[:xdigit:]]{64}:\d+" + // r"output in Bitcoin Core wallet but not in ord index: [[:xdigit:]]{64}:\d+" // ); // } // } @@ -5586,4 +5600,227 @@ mod tests { ); } } + + #[test] + fn pre_jubilee_first_reinscription_after_cursed_inscription_is_blessed() { + for context in Context::configurations() { + context.mine_blocks(1); + + // Before the jubilee, an inscription on a sat using a pushnum opcode is + // cursed and not vindicated. + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([]) + .push_opcode(opcodes::all::OP_PUSHNUM_1) + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + let entry = context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + assert!(Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Cursed)); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Vindicated)); + + let sat = entry.sat; + + assert_eq!(entry.inscription_number, -1); + + // Before the jubilee, reinscription on the same sat is not cursed and + // not vindicated. + + let inscription = Inscription::default(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + let entry = context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + assert_eq!(entry.inscription_number, 0); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Cursed)); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Vindicated)); + + assert_eq!(sat, entry.sat); + + // Before the jubilee, a third reinscription on the same sat is cursed + // and not vindicated. + + let inscription = Inscription::default(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 1, 0, inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + let entry = context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + assert!(Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Cursed)); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Vindicated)); + + assert_eq!(entry.inscription_number, -2); + + assert_eq!(sat, entry.sat); + } + } + + #[test] + fn post_jubilee_first_reinscription_after_vindicated_inscription_not_vindicated() { + for context in Context::configurations() { + context.mine_blocks(110); + // After the jubilee, an inscription on a sat using a pushnum opcode is + // vindicated and not cursed. + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([]) + .push_opcode(opcodes::all::OP_PUSHNUM_1) + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + ..Default::default() + }); + + let inscription_id = InscriptionId { txid, index: 0 }; + + context.mine_blocks(1); + + let entry = context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Cursed)); + + assert!(Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Vindicated)); + + let sat = entry.sat; + + assert_eq!(entry.inscription_number, 0); + + // After the jubilee, a reinscription on the same is not cursed and not + // vindicated. + + let inscription = Inscription::default(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(111, 1, 0, inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + let entry = context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Cursed)); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Vindicated)); + + assert_eq!(entry.inscription_number, 1); + + assert_eq!(sat, entry.sat); + + // After the jubilee, a third reinscription on the same is vindicated and + // not cursed. + + let inscription = Inscription::default(); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(112, 1, 0, inscription.to_witness())], + ..Default::default() + }); + + context.mine_blocks(1); + + let inscription_id = InscriptionId { txid, index: 0 }; + + let entry = context + .index + .get_inscription_entry(inscription_id) + .unwrap() + .unwrap(); + + assert!(!Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Cursed)); + + assert!(Charm::charms(entry.charms) + .iter() + .any(|charm| *charm == Charm::Vindicated)); + + assert_eq!(entry.inscription_number, 2); + + assert_eq!(sat, entry.sat); + } + } } diff --git a/src/index/entry.rs b/src/index/entry.rs index e5b1a9976c..116ee448ea 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -28,21 +28,21 @@ impl Entry for Header { } } -#[derive(Debug, PartialEq, Copy, Clone)] -pub(crate) struct RuneEntry { - pub(crate) burned: u128, - pub(crate) deadline: Option, - pub(crate) divisibility: u8, - pub(crate) end: Option, - pub(crate) etching: Txid, - pub(crate) limit: Option, - pub(crate) mints: u64, - pub(crate) number: u64, - pub(crate) rune: Rune, - pub(crate) spacers: u32, - pub(crate) supply: u128, - pub(crate) symbol: Option, - pub(crate) timestamp: u32, +#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub struct RuneEntry { + pub burned: u128, + pub deadline: Option, + pub divisibility: u8, + pub end: Option, + pub etching: Txid, + pub limit: Option, + pub mints: u64, + pub number: u64, + pub rune: Rune, + pub spacers: u32, + pub supply: u128, + pub symbol: Option, + pub timestamp: u32, } pub(super) type RuneEntryValue = ( diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index a491950073..19f99033cc 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -30,6 +30,7 @@ enum Origin { pointer: Option, reinscription: bool, unbound: bool, + vindicated: bool, }, Old { old_satpoint: SatPoint, @@ -76,6 +77,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let mut floating_inscriptions = Vec::new(); let mut id_counter = 0; let mut inscribed_offsets = BTreeMap::new(); + let jubilant = self.height >= self.chain.jubilee_height(); let mut total_input_value = 0; let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); @@ -142,9 +144,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { index: id_counter, }; - let curse = if self.height >= self.chain.jubilee_height() { - None - } else if inscription.payload.unrecognized_even_field { + let curse = if inscription.payload.unrecognized_even_field { Some(Curse::UnrecognizedEvenField) } else if inscription.payload.duplicate_field { Some(Curse::DuplicateField) @@ -167,17 +167,18 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let initial_inscription_sequence_number = self.id_to_sequence_number.get(id.store())?.unwrap().value(); - let initial_inscription_is_cursed = InscriptionEntry::load( + let entry = InscriptionEntry::load( self .sequence_number_to_entry .get(initial_inscription_sequence_number)? .unwrap() .value(), - ) - .inscription_number - < 0; + ); + + let initial_inscription_was_cursed_or_vindicated = + entry.inscription_number < 0 || Charm::Vindicated.is_set(entry.charms); - if initial_inscription_is_cursed { + if initial_inscription_was_cursed_or_vindicated { None } else { Some(Curse::Reinscription) @@ -201,13 +202,14 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { inscription_id, offset, origin: Origin::New { - reinscription: inscribed_offsets.get(&offset).is_some(), - cursed: curse.is_some(), + cursed: curse.is_some() && !jubilant, fee: 0, hidden: inscription.payload.hidden(), parent: inscription.payload.parent(), pointer: inscription.payload.pointer(), + reinscription: inscribed_offsets.get(&offset).is_some(), unbound, + vindicated: curse.is_some() && jubilant, }, }); @@ -404,6 +406,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { pointer: _, reinscription, unbound, + vindicated, } => { let inscription_number = if cursed { let number: i32 = self.cursed_inscription_count.try_into().unwrap(); @@ -467,6 +470,10 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Charm::Unbound.set(&mut charms); } + if vindicated { + Charm::Vindicated.set(&mut charms); + } + if let Some(Sat(n)) = sat { self.sat_to_sequence_number.insert(&n, &sequence_number)?; } diff --git a/src/inscriptions/charm.rs b/src/inscriptions/charm.rs index b80c5c6616..d0770886d7 100644 --- a/src/inscriptions/charm.rs +++ b/src/inscriptions/charm.rs @@ -1,19 +1,20 @@ -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq)] pub(crate) enum Charm { - Coin, - Cursed, - Epic, - Legendary, - Lost, - Nineball, - Rare, - Reinscription, - Unbound, - Uncommon, + Coin = 0, + Cursed = 1, + Epic = 2, + Legendary = 3, + Lost = 4, + Nineball = 5, + Rare = 6, + Reinscription = 7, + Unbound = 8, + Uncommon = 9, + Vindicated = 10, } impl Charm { - pub(crate) const ALL: [Charm; 10] = [ + pub(crate) const ALL: [Charm; 11] = [ Self::Coin, Self::Uncommon, Self::Rare, @@ -24,6 +25,7 @@ impl Charm { Self::Cursed, Self::Unbound, Self::Lost, + Self::Vindicated, ]; fn flag(self) -> u16 { @@ -50,6 +52,7 @@ impl Charm { Self::Reinscription => "♻️", Self::Unbound => "🔓", Self::Uncommon => "🌱", + Self::Vindicated => "❤️‍🔥", } } @@ -65,6 +68,16 @@ impl Charm { Self::Reinscription => "reinscription", Self::Unbound => "unbound", Self::Uncommon => "uncommon", + Self::Vindicated => "vindicated", } } + + #[cfg(test)] + pub(crate) fn charms(charms: u16) -> Vec { + Self::ALL + .iter() + .filter(|charm| charm.is_set(charms)) + .cloned() + .collect() + } } diff --git a/src/lib.rs b/src/lib.rs index 2f6c653f06..37320fc027 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use { deserialize_from_str::DeserializeFromStr, epoch::Epoch, height::Height, - index::{List, RuneEntry}, + index::List, inscriptions::{media, teleburn, Charm, Media, ParsedEnvelope}, outgoing::Outgoing, representation::Representation, @@ -48,7 +48,6 @@ use { Witness, }, bitcoincore_rpc::{Client, RpcApi}, - chain::Chain, chrono::{DateTime, TimeZone, Utc}, ciborium::Value, clap::{ArgGroup, Parser}, @@ -84,8 +83,9 @@ use { }; pub use self::{ + chain::Chain, fee_rate::FeeRate, - index::Index, + index::{Index, RuneEntry}, inscriptions::{Envelope, Inscription, InscriptionId}, object::Object, options::Options, @@ -115,7 +115,7 @@ macro_rules! tprintln { mod arguments; mod blocktime; -mod chain; +pub mod chain; mod config; mod decimal; mod decimal_sat; diff --git a/src/runes.rs b/src/runes.rs index ccc88be8bb..0f58905b61 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1,4 +1,7 @@ -use super::*; +use { + self::{flag::Flag, tag::Tag}, + super::*, +}; pub use {edict::Edict, rune::Rune, rune_id::RuneId, runestone::Runestone}; @@ -11,11 +14,13 @@ const RESERVED: u128 = 6402364363415443603228541259936211926; mod edict; mod etching; +mod flag; mod pile; mod rune; mod rune_id; mod runestone; mod spaced_rune; +mod tag; pub mod varint; type Result = std::result::Result; diff --git a/src/runes/etching.rs b/src/runes/etching.rs index 24fb593a29..75e1344f55 100644 --- a/src/runes/etching.rs +++ b/src/runes/etching.rs @@ -6,7 +6,7 @@ pub struct Etching { pub divisibility: u8, pub limit: Option, pub rune: Option, + pub spacers: u32, pub symbol: Option, pub term: Option, - pub spacers: u32, } diff --git a/src/runes/flag.rs b/src/runes/flag.rs new file mode 100644 index 0000000000..fcc39e93b9 --- /dev/null +++ b/src/runes/flag.rs @@ -0,0 +1,51 @@ +pub(super) enum Flag { + Etch = 0, + #[allow(unused)] + Burn = 127, +} + +impl Flag { + pub(super) fn mask(self) -> u128 { + 1 << self as u128 + } + + pub(super) fn take(self, flags: &mut u128) -> bool { + let mask = self.mask(); + let set = *flags & mask != 0; + *flags &= !mask; + set + } + + pub(super) fn set(self, flags: &mut u128) { + *flags |= self.mask() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mask() { + assert_eq!(Flag::Etch.mask(), 0b1); + assert_eq!(Flag::Burn.mask(), 1 << 127); + } + + #[test] + fn take() { + let mut flags = 1; + assert!(Flag::Etch.take(&mut flags)); + assert_eq!(flags, 0); + + let mut flags = 0; + assert!(!Flag::Etch.take(&mut flags)); + assert_eq!(flags, 0); + } + + #[test] + fn set() { + let mut flags = 0; + Flag::Etch.set(&mut flags); + assert_eq!(flags, 1); + } +} diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index e1cf9aa63e..c56a41d2dc 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -1,25 +1,5 @@ use super::*; -const TAG_BODY: u128 = 0; -const TAG_FLAGS: u128 = 2; -const TAG_RUNE: u128 = 4; -const TAG_LIMIT: u128 = 6; -const TAG_TERM: u128 = 8; -const TAG_DEADLINE: u128 = 10; -const TAG_DEFAULT_OUTPUT: u128 = 12; - -const TAG_DIVISIBILITY: u128 = 1; -const TAG_SPACERS: u128 = 3; -const TAG_SYMBOL: u128 = 5; - -const FLAG_ETCH: u128 = 0b000_0001; - -#[allow(unused)] -const TAG_BURN: u128 = 254; - -#[allow(unused)] -const TAG_NOP: u128 = 255; - const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; #[derive(Default, Serialize, Debug, PartialEq)] @@ -32,22 +12,22 @@ pub struct Runestone { struct Message { fields: HashMap, - body: Vec, + edicts: Vec, } impl Message { fn from_integers(payload: &[u128]) -> Self { - let mut body = Vec::new(); + let mut edicts = Vec::new(); let mut fields = HashMap::new(); for i in (0..payload.len()).step_by(2) { let tag = payload[i]; - if tag == TAG_BODY { + if Tag::Body == tag { let mut id = 0u128; for chunk in payload[i + 1..].chunks_exact(3) { id = id.saturating_add(chunk[0]); - body.push(Edict { + edicts.push(Edict { id, amount: chunk[1], output: chunk[2], @@ -63,7 +43,7 @@ impl Message { fields.entry(tag).or_insert(value); } - Self { fields, body } + Self { fields, edicts } } } @@ -79,47 +59,65 @@ impl Runestone { let integers = Runestone::integers(&payload); - let Message { mut fields, body } = Message::from_integers(&integers); + let Message { mut fields, edicts } = Message::from_integers(&integers); + + let deadline = Tag::Deadline + .take(&mut fields) + .and_then(|deadline| u32::try_from(deadline).ok()); + + let default_output = Tag::DefaultOutput + .take(&mut fields) + .and_then(|default| u32::try_from(default).ok()); + + let divisibility = Tag::Divisibility + .take(&mut fields) + .and_then(|divisibility| u8::try_from(divisibility).ok()) + .and_then(|divisibility| (divisibility <= MAX_DIVISIBILITY).then_some(divisibility)) + .unwrap_or_default(); + + let limit = Tag::Limit + .take(&mut fields) + .and_then(|limit| (limit <= MAX_LIMIT).then_some(limit)); - let deadline = fields.remove(&TAG_DEADLINE); - let divisibility = fields.remove(&TAG_DIVISIBILITY); - let flags = fields.remove(&TAG_FLAGS).unwrap_or_default(); - let limit = fields.remove(&TAG_LIMIT); - let rune = fields.remove(&TAG_RUNE); - let spacers = fields.remove(&TAG_SPACERS); - let symbol = fields.remove(&TAG_SYMBOL); - let term = fields.remove(&TAG_TERM); - let default_output = fields.remove(&TAG_DEFAULT_OUTPUT); + let rune = Tag::Rune.take(&mut fields).map(Rune); - let etch = flags & FLAG_ETCH != 0; - let unrecognized_flags = flags & !FLAG_ETCH != 0; + let spacers = Tag::Spacers + .take(&mut fields) + .and_then(|spacers| u32::try_from(spacers).ok()) + .and_then(|spacers| (spacers <= MAX_SPACERS).then_some(spacers)) + .unwrap_or_default(); + + let symbol = Tag::Symbol + .take(&mut fields) + .and_then(|symbol| u32::try_from(symbol).ok()) + .and_then(char::from_u32); + + let term = Tag::Term + .take(&mut fields) + .and_then(|term| u32::try_from(term).ok()); + + let mut flags = Tag::Flags.take(&mut fields).unwrap_or_default(); + + let etch = Flag::Etch.take(&mut flags); let etching = if etch { Some(Etching { - deadline: deadline.and_then(|deadline| u32::try_from(deadline).ok()), - divisibility: divisibility - .and_then(|divisibility| u8::try_from(divisibility).ok()) - .and_then(|divisibility| (divisibility <= MAX_DIVISIBILITY).then_some(divisibility)) - .unwrap_or_default(), - limit: limit.and_then(|limit| (limit <= MAX_LIMIT).then_some(limit)), - rune: rune.map(Rune), - spacers: spacers - .and_then(|spacers| u32::try_from(spacers).ok()) - .and_then(|spacers| (spacers <= MAX_SPACERS).then_some(spacers)) - .unwrap_or_default(), - symbol: symbol - .and_then(|symbol| u32::try_from(symbol).ok()) - .and_then(char::from_u32), - term: term.and_then(|term| u32::try_from(term).ok()), + deadline, + divisibility, + limit, + rune, + spacers, + symbol, + term, }) } else { None }; Ok(Some(Self { - burn: unrecognized_flags || fields.keys().any(|tag| tag % 2 == 0), - default_output: default_output.and_then(|default| u32::try_from(default).ok()), - edicts: body, + burn: flags != 0 || fields.keys().any(|tag| tag % 2 == 0), + default_output, + edicts, etching, })) } @@ -128,57 +126,50 @@ impl Runestone { let mut payload = Vec::new(); if let Some(etching) = self.etching { - varint::encode_to_vec(TAG_FLAGS, &mut payload); - varint::encode_to_vec(FLAG_ETCH, &mut payload); + let mut flags = 0; + Flag::Etch.set(&mut flags); + + Tag::Flags.encode(flags, &mut payload); if let Some(rune) = etching.rune { - varint::encode_to_vec(TAG_RUNE, &mut payload); - varint::encode_to_vec(rune.0, &mut payload); + Tag::Rune.encode(rune.0, &mut payload); } if let Some(deadline) = etching.deadline { - varint::encode_to_vec(TAG_DEADLINE, &mut payload); - varint::encode_to_vec(deadline.into(), &mut payload); + Tag::Deadline.encode(deadline.into(), &mut payload); } if etching.divisibility != 0 { - varint::encode_to_vec(TAG_DIVISIBILITY, &mut payload); - varint::encode_to_vec(etching.divisibility.into(), &mut payload); + Tag::Divisibility.encode(etching.divisibility.into(), &mut payload); } if etching.spacers != 0 { - varint::encode_to_vec(TAG_SPACERS, &mut payload); - varint::encode_to_vec(etching.spacers.into(), &mut payload); + Tag::Spacers.encode(etching.spacers.into(), &mut payload); } if let Some(symbol) = etching.symbol { - varint::encode_to_vec(TAG_SYMBOL, &mut payload); - varint::encode_to_vec(symbol.into(), &mut payload); + Tag::Symbol.encode(symbol.into(), &mut payload); } if let Some(limit) = etching.limit { - varint::encode_to_vec(TAG_LIMIT, &mut payload); - varint::encode_to_vec(limit, &mut payload); + Tag::Limit.encode(limit, &mut payload); } if let Some(term) = etching.term { - varint::encode_to_vec(TAG_TERM, &mut payload); - varint::encode_to_vec(term.into(), &mut payload); + Tag::Term.encode(term.into(), &mut payload); } } if let Some(default_output) = self.default_output { - varint::encode_to_vec(TAG_DEFAULT_OUTPUT, &mut payload); - varint::encode_to_vec(default_output.into(), &mut payload); + Tag::DefaultOutput.encode(default_output.into(), &mut payload); } if self.burn { - varint::encode_to_vec(TAG_BURN, &mut payload); - varint::encode_to_vec(0, &mut payload); + Tag::Burn.encode(0, &mut payload); } if !self.edicts.is_empty() { - varint::encode_to_vec(TAG_BODY, &mut payload); + varint::encode_to_vec(Tag::Body.into(), &mut payload); let mut edicts = self.edicts.clone(); edicts.sort_by_key(|edict| edict.id); @@ -503,7 +494,7 @@ mod tests { #[test] fn deciphering_non_empty_runestone_is_successful() { assert_eq!( - decipher(&[TAG_BODY, 1, 2, 3]), + decipher(&[Tag::Body.into(), 1, 2, 3]), Runestone { edicts: vec![Edict { id: 1, @@ -518,7 +509,14 @@ mod tests { #[test] fn decipher_etching() { assert_eq!( - decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_BODY, 1, 2, 3]), + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Body.into(), + 1, + 2, + 3 + ]), Runestone { edicts: vec![Edict { id: 1, @@ -534,7 +532,16 @@ mod tests { #[test] fn decipher_etching_with_rune() { assert_eq!( - decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 4, TAG_BODY, 1, 2, 3]), + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), + 4, + Tag::Body.into(), + 1, + 2, + 3 + ]), Runestone { edicts: vec![Edict { id: 1, @@ -553,7 +560,16 @@ mod tests { #[test] fn decipher_etching_with_term() { assert_eq!( - decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_TERM, 4, TAG_BODY, 1, 2, 3]), + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Term.into(), + 4, + Tag::Body.into(), + 1, + 2, + 3 + ]), Runestone { edicts: vec![Edict { id: 1, @@ -572,7 +588,16 @@ mod tests { #[test] fn decipher_etching_with_limit() { assert_eq!( - decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_LIMIT, 4, TAG_BODY, 1, 2, 3]), + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Limit.into(), + 4, + Tag::Body.into(), + 1, + 2, + 3 + ]), Runestone { edicts: vec![Edict { id: 1, @@ -591,7 +616,18 @@ mod tests { #[test] fn duplicate_tags_are_ignored() { assert_eq!( - decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 4, TAG_RUNE, 5, TAG_BODY, 1, 2, 3,]), + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), + 4, + Tag::Rune.into(), + 5, + Tag::Body.into(), + 1, + 2, + 3, + ]), Runestone { edicts: vec![Edict { id: 1, @@ -610,7 +646,7 @@ mod tests { #[test] fn unrecognized_odd_tag_is_ignored() { assert_eq!( - decipher(&[TAG_NOP, 100, TAG_BODY, 1, 2, 3]), + decipher(&[Tag::Nop.into(), 100, Tag::Body.into(), 1, 2, 3]), Runestone { edicts: vec![Edict { id: 1, @@ -625,7 +661,7 @@ mod tests { #[test] fn unrecognized_even_tag_is_burn() { assert_eq!( - decipher(&[TAG_BURN, 0, TAG_BODY, 1, 2, 3]), + decipher(&[Tag::Burn.into(), 0, Tag::Body.into(), 1, 2, 3]), Runestone { edicts: vec![Edict { id: 1, @@ -641,7 +677,14 @@ mod tests { #[test] fn unrecognized_flag_is_burn() { assert_eq!( - decipher(&[TAG_FLAGS, 1 << 1, TAG_BODY, 1, 2, 3]), + decipher(&[ + Tag::Flags.into(), + Flag::Burn.mask(), + Tag::Body.into(), + 1, + 2, + 3 + ]), Runestone { edicts: vec![Edict { id: 1, @@ -657,7 +700,7 @@ mod tests { #[test] fn tag_with_no_value_is_ignored() { assert_eq!( - decipher(&[TAG_FLAGS, 1, TAG_FLAGS]), + decipher(&[Tag::Flags.into(), 1, Tag::Flags.into()]), Runestone { etching: Some(Etching::default()), ..Default::default() @@ -668,7 +711,18 @@ mod tests { #[test] fn additional_integers_in_body_are_ignored() { assert_eq!( - decipher(&[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 4, TAG_BODY, 1, 2, 3, 4, 5]), + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), + 4, + Tag::Body.into(), + 1, + 2, + 3, + 4, + 5 + ]), Runestone { edicts: vec![Edict { id: 1, @@ -688,13 +742,13 @@ mod tests { fn decipher_etching_with_divisibility() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_RUNE, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), 4, - TAG_DIVISIBILITY, + Tag::Divisibility.into(), 5, - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -719,13 +773,13 @@ mod tests { fn divisibility_above_max_is_ignored() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_RUNE, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), 4, - TAG_DIVISIBILITY, + Tag::Divisibility.into(), (MAX_DIVISIBILITY + 1).into(), - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -749,11 +803,11 @@ mod tests { fn symbol_above_max_is_ignored() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_SYMBOL, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Symbol.into(), u128::from(u32::from(char::MAX) + 1), - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -774,13 +828,13 @@ mod tests { fn decipher_etching_with_symbol() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_RUNE, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), 4, - TAG_SYMBOL, + Tag::Symbol.into(), 'a'.into(), - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -805,23 +859,23 @@ mod tests { fn decipher_etching_with_all_etching_tags() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_RUNE, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), 4, - TAG_DEADLINE, + Tag::Deadline.into(), 7, - TAG_DIVISIBILITY, + Tag::Divisibility.into(), 1, - TAG_SPACERS, + Tag::Spacers.into(), 5, - TAG_SYMBOL, + Tag::Symbol.into(), 'a'.into(), - TAG_TERM, + Tag::Term.into(), 2, - TAG_LIMIT, + Tag::Limit.into(), 3, - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -850,17 +904,17 @@ mod tests { fn recognized_even_etching_fields_in_non_etching_are_ignored() { assert_eq!( decipher(&[ - TAG_RUNE, + Tag::Rune.into(), 4, - TAG_DIVISIBILITY, + Tag::Divisibility.into(), 1, - TAG_SYMBOL, + Tag::Symbol.into(), 'a'.into(), - TAG_TERM, + Tag::Term.into(), 2, - TAG_LIMIT, + Tag::Limit.into(), 3, - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -882,15 +936,15 @@ mod tests { fn decipher_etching_with_divisibility_and_symbol() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_RUNE, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), 4, - TAG_DIVISIBILITY, + Tag::Divisibility.into(), 1, - TAG_SYMBOL, + Tag::Symbol.into(), 'a'.into(), - TAG_BODY, + Tag::Body.into(), 1, 2, 3, @@ -916,11 +970,11 @@ mod tests { fn tag_values_are_not_parsed_as_tags() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_DIVISIBILITY, - TAG_BODY, - TAG_BODY, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Divisibility.into(), + Tag::Body.into(), + Tag::Body.into(), 1, 2, 3, @@ -940,7 +994,7 @@ mod tests { #[test] fn runestone_may_contain_multiple_edicts() { assert_eq!( - decipher(&[TAG_BODY, 1, 2, 3, 3, 5, 6]), + decipher(&[Tag::Body.into(), 1, 2, 3, 3, 5, 6]), Runestone { edicts: vec![ Edict { @@ -962,7 +1016,7 @@ mod tests { #[test] fn id_deltas_saturate_to_max() { assert_eq!( - decipher(&[TAG_BODY, 1, 2, 3, u128::max_value(), 5, 6]), + decipher(&[Tag::Body.into(), 1, 2, 3, u128::max_value(), 5, 6]), Runestone { edicts: vec![ Edict { @@ -990,16 +1044,31 @@ mod tests { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .push_slice(b"RUNE_TEST") - .push_slice::<&PushBytes>(varint::encode(TAG_FLAGS).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(FLAG_ETCH).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>( - varint::encode(TAG_DIVISIBILITY) + varint::encode(Tag::Flags.into()) + .as_slice() + .try_into() + .unwrap() + ) + .push_slice::<&PushBytes>( + varint::encode(Flag::Etch.mask()) + .as_slice() + .try_into() + .unwrap() + ) + .push_slice::<&PushBytes>( + varint::encode(Tag::Divisibility.into()) .as_slice() .try_into() .unwrap() ) .push_slice::<&PushBytes>(varint::encode(5).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(TAG_BODY).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>( + varint::encode(Tag::Body.into()) + .as_slice() + .try_into() + .unwrap() + ) .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(3).as_slice().try_into().unwrap()) @@ -1369,9 +1438,9 @@ mod tests { fn etching_with_term_greater_than_maximum_is_ignored() { assert_eq!( decipher(&[ - TAG_FLAGS, - FLAG_ETCH, - TAG_TERM, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Term.into(), u128::from(u64::max_value()) + 1, ]), Runestone { @@ -1445,25 +1514,25 @@ mod tests { burn: false, }, &[ - TAG_FLAGS, - FLAG_ETCH, - TAG_RUNE, + Tag::Flags.into(), + Flag::Etch.mask(), + Tag::Rune.into(), 4, - TAG_DEADLINE, + Tag::Deadline.into(), 2, - TAG_DIVISIBILITY, + Tag::Divisibility.into(), 1, - TAG_SPACERS, + Tag::Spacers.into(), 6, - TAG_SYMBOL, + Tag::Symbol.into(), '@'.into(), - TAG_LIMIT, + Tag::Limit.into(), 3, - TAG_TERM, + Tag::Term.into(), 5, - TAG_DEFAULT_OUTPUT, + Tag::DefaultOutput.into(), 11, - TAG_BODY, + Tag::Body.into(), 6, 5, 7, @@ -1487,7 +1556,7 @@ mod tests { burn: false, ..Default::default() }, - &[TAG_FLAGS, FLAG_ETCH, TAG_RUNE, 3], + &[Tag::Flags.into(), Flag::Etch.mask(), Tag::Rune.into(), 3], ); case( @@ -1504,7 +1573,7 @@ mod tests { burn: false, ..Default::default() }, - &[TAG_FLAGS, FLAG_ETCH], + &[Tag::Flags.into(), Flag::Etch.mask()], ); case( @@ -1512,7 +1581,7 @@ mod tests { burn: true, ..Default::default() }, - &[TAG_BURN, 0], + &[Tag::Burn.into(), 0], ); } diff --git a/src/runes/tag.rs b/src/runes/tag.rs new file mode 100644 index 0000000000..f33ab00caa --- /dev/null +++ b/src/runes/tag.rs @@ -0,0 +1,84 @@ +use super::*; + +#[derive(Copy, Clone, Debug)] +pub(super) enum Tag { + Body = 0, + Flags = 2, + Rune = 4, + Limit = 6, + Term = 8, + Deadline = 10, + DefaultOutput = 12, + #[allow(unused)] + Burn = 254, + + Divisibility = 1, + Spacers = 3, + Symbol = 5, + #[allow(unused)] + Nop = 255, +} + +impl Tag { + pub(super) fn take(self, fields: &mut HashMap) -> Option { + fields.remove(&self.into()) + } + + pub(super) fn encode(self, value: u128, payload: &mut Vec) { + varint::encode_to_vec(self.into(), payload); + varint::encode_to_vec(value, payload); + } +} + +impl From for u128 { + fn from(tag: Tag) -> Self { + tag as u128 + } +} + +impl PartialEq for Tag { + fn eq(&self, other: &u128) -> bool { + u128::from(*self) == *other + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_u128() { + assert_eq!(0u128, Tag::Body.into()); + assert_eq!(2u128, Tag::Flags.into()); + } + + #[test] + fn partial_eq() { + assert_eq!(Tag::Body, 0); + assert_eq!(Tag::Flags, 2); + } + + #[test] + fn take() { + let mut fields = vec![(2, 3)].into_iter().collect::>(); + + assert_eq!(Tag::Flags.take(&mut fields), Some(3)); + + assert!(fields.is_empty()); + + assert_eq!(Tag::Flags.take(&mut fields), None); + } + + #[test] + fn encode() { + let mut payload = Vec::new(); + + Tag::Flags.encode(3, &mut payload); + + assert_eq!(payload, [2, 3]); + + Tag::Rune.encode(5, &mut payload); + + assert_eq!(payload, [2, 3, 4, 5]); + } +} diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 052335ab2f..3705df14da 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -14,8 +14,8 @@ use { InscriptionsHtml, InscriptionsJson, OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, - RangeHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, SatInscriptionJson, SatInscriptionsJson, - SatJson, StatusHtml, TransactionHtml, + RangeHtml, RareTxt, RuneHtml, RuneJson, RunesHtml, RunesJson, SatHtml, SatInscriptionJson, + SatInscriptionsJson, SatJson, TransactionHtml, }, }, axum::{ @@ -170,6 +170,8 @@ pub(crate) struct Server { 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, alias = "nosync", help = "Do not update the index.")] + no_sync: bool, } impl Server { @@ -181,8 +183,10 @@ impl Server { if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) { break; } - if let Err(error) = index_clone.update() { - log::warn!("Updating index: {error}"); + if !self.no_sync { + if let Err(error) = index_clone.update() { + log::warn!("Updating index: {error}"); + } } thread::sleep(Duration::from_millis(5000)); }); @@ -469,16 +473,18 @@ impl Server { } async fn clock(Extension(index): Extension>) -> ServerResult { - Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - HeaderValue::from_static("default-src 'unsafe-inline'"), - )], - ClockSvg::new(Self::index_height(&index)?), + task::block_in_place(|| { + Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + HeaderValue::from_static("default-src 'unsafe-inline'"), + )], + ClockSvg::new(Self::index_height(&index)?), + ) + .into_response(), ) - .into_response(), - ) + }) } async fn sat( @@ -487,43 +493,45 @@ impl Server { Path(DeserializeFromStr(sat)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let inscriptions = index.get_inscription_ids_by_sat(sat)?; - let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| { - inscriptions.first().and_then(|&first_inscription_id| { - index - .get_inscription_satpoint_by_id(first_inscription_id) - .ok() - .flatten() - }) - }); - let blocktime = index.block_time(sat.height())?; - Ok(if accept_json { - Json(SatJson { - number: sat.0, - decimal: sat.decimal().to_string(), - degree: sat.degree().to_string(), - name: sat.name(), - block: sat.height().0, - cycle: sat.cycle(), - epoch: sat.epoch().0, - period: sat.period(), - offset: sat.third(), - rarity: sat.rarity(), - percentile: sat.percentile(), - satpoint, - timestamp: blocktime.timestamp().timestamp(), - inscriptions, + task::block_in_place(|| { + let inscriptions = index.get_inscription_ids_by_sat(sat)?; + let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| { + inscriptions.first().and_then(|&first_inscription_id| { + index + .get_inscription_satpoint_by_id(first_inscription_id) + .ok() + .flatten() + }) + }); + let blocktime = index.block_time(sat.height())?; + Ok(if accept_json { + Json(SatJson { + number: sat.0, + decimal: sat.decimal().to_string(), + degree: sat.degree().to_string(), + name: sat.name(), + block: sat.height().0, + cycle: sat.cycle(), + epoch: sat.epoch().0, + period: sat.period(), + offset: sat.third(), + rarity: sat.rarity(), + percentile: sat.percentile(), + satpoint, + timestamp: blocktime.timestamp().timestamp(), + inscriptions, + }) + .into_response() + } else { + SatHtml { + sat, + satpoint, + blocktime, + inscriptions, + } + .page(server_config) + .into_response() }) - .into_response() - } else { - SatHtml { - sat, - satpoint, - blocktime, - inscriptions, - } - .page(server_config) - .into_response() }) } @@ -537,59 +545,61 @@ impl Server { Path(outpoint): Path, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let list = index.list(outpoint)?; + task::block_in_place(|| { + let list = index.list(outpoint)?; - let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { - let mut value = 0; + let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { + let mut value = 0; - if let Some(List::Unspent(ranges)) = &list { - for (start, end) in ranges { - value += end - start; + if let Some(List::Unspent(ranges)) = &list { + for (start, end) in ranges { + value += end - start; + } } - } - - TxOut { - value, - script_pubkey: ScriptBuf::new(), - } - } else { - 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)?; - Ok(if accept_json { - Json(OutputJson::new( - outpoint, - list, - server_config.chain, - output, - inscriptions, - runes + TxOut { + value, + script_pubkey: ScriptBuf::new(), + } + } else { + index + .get_transaction(outpoint.txid)? + .ok_or_not_found(|| format!("output {outpoint}"))? + .output .into_iter() - .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) - .collect(), - )) - .into_response() - } else { - OutputHtml { - outpoint, - inscriptions, - list, - chain: server_config.chain, - output, - runes, - } - .page(server_config) - .into_response() + .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)?; + + Ok(if accept_json { + Json(OutputJson::new( + outpoint, + list, + server_config.chain, + output, + inscriptions, + runes + .into_iter() + .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) + .collect(), + )) + .into_response() + } else { + OutputHtml { + outpoint, + inscriptions, + list, + chain: server_config.chain, + output, + runes, + } + .page(server_config) + .into_response() + }) }) } @@ -610,66 +620,87 @@ impl Server { } async fn rare_txt(Extension(index): Extension>) -> ServerResult { - Ok(RareTxt(index.rare_sat_satpoints()?)) + task::block_in_place(|| Ok(RareTxt(index.rare_sat_satpoints()?))) } async fn rune( Extension(server_config): Extension>, Extension(index): Extension>, Path(DeserializeFromStr(spaced_rune)): Path>, - ) -> ServerResult> { - if !index.has_rune_index() { - return Err(ServerError::NotFound( - "this server has no rune index".to_string(), - )); - } + AcceptJson(accept_json): AcceptJson, + ) -> ServerResult { + task::block_in_place(|| { + if !index.has_rune_index() { + return Err(ServerError::NotFound( + "this server has no rune index".to_string(), + )); + } - Ok( - index - .rune_html(spaced_rune.rune)? - .ok_or_not_found(|| format!("rune {spaced_rune}"))? - .page(server_config), - ) + let (id, entry, parent) = index + .rune(spaced_rune.rune)? + .ok_or_not_found(|| format!("rune {spaced_rune}"))?; + + Ok(if accept_json { + Json(RuneJson { entry, id, parent }).into_response() + } else { + RuneHtml { entry, id, parent } + .page(server_config) + .into_response() + }) + }) } async fn runes( Extension(server_config): Extension>, Extension(index): Extension>, - ) -> ServerResult> { - Ok( - RunesHtml { - entries: index.runes()?, - } - .page(server_config), - ) + AcceptJson(accept_json): AcceptJson, + ) -> ServerResult { + task::block_in_place(|| { + Ok(if accept_json { + Json(RunesJson { + entries: index.runes()?, + }) + .into_response() + } else { + RunesHtml { + entries: index.runes()?, + } + .page(server_config) + .into_response() + }) + }) } async fn home( Extension(server_config): Extension>, Extension(index): Extension>, ) -> ServerResult> { - Ok( - HomeHtml { - inscriptions: index.get_home_inscriptions()?, - } - .page(server_config), - ) + task::block_in_place(|| { + Ok( + HomeHtml { + inscriptions: index.get_home_inscriptions()?, + } + .page(server_config), + ) + }) } async fn blocks( Extension(server_config): Extension>, Extension(index): Extension>, ) -> ServerResult> { - let blocks = index.blocks(100)?; - let mut featured_blocks = BTreeMap::new(); - for (height, hash) in blocks.iter().take(5) { - let (inscriptions, _total_num) = - index.get_highest_paying_inscriptions_in_block(*height, 8)?; - - featured_blocks.insert(*hash, inscriptions); - } + task::block_in_place(|| { + let blocks = index.blocks(100)?; + let mut featured_blocks = BTreeMap::new(); + for (height, hash) in blocks.iter().take(5) { + let (inscriptions, _total_num) = + index.get_highest_paying_inscriptions_in_block(*height, 8)?; + + featured_blocks.insert(*hash, inscriptions); + } - Ok(BlocksHtml::new(blocks, featured_blocks).page(server_config)) + Ok(BlocksHtml::new(blocks, featured_blocks).page(server_config)) + }) } async fn install_script() -> Redirect { @@ -682,48 +713,50 @@ impl Server { Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let (block, height) = match query { - BlockQuery::Height(height) => { - let block = index - .get_block_by_height(height)? - .ok_or_not_found(|| format!("block {height}"))?; - - (block, height) - } - BlockQuery::Hash(hash) => { - let info = index - .block_header_info(hash)? - .ok_or_not_found(|| format!("block {hash}"))?; + task::block_in_place(|| { + let (block, height) = match query { + BlockQuery::Height(height) => { + let block = index + .get_block_by_height(height)? + .ok_or_not_found(|| format!("block {height}"))?; + + (block, height) + } + BlockQuery::Hash(hash) => { + let info = index + .block_header_info(hash)? + .ok_or_not_found(|| format!("block {hash}"))?; - let block = index - .get_block_by_hash(hash)? - .ok_or_not_found(|| format!("block {hash}"))?; + let block = index + .get_block_by_hash(hash)? + .ok_or_not_found(|| format!("block {hash}"))?; - (block, u32::try_from(info.height).unwrap()) - } - }; + (block, u32::try_from(info.height).unwrap()) + } + }; - Ok(if accept_json { - let inscriptions = index.get_inscriptions_in_block(height)?; - Json(BlockJson::new( - block, - Height(height), - Self::index_height(&index)?, - inscriptions, - )) - .into_response() - } else { - let (featured_inscriptions, total_num) = - index.get_highest_paying_inscriptions_in_block(height, 8)?; - BlockHtml::new( - block, - Height(height), - Self::index_height(&index)?, - total_num, - featured_inscriptions, - ) - .page(server_config) - .into_response() + Ok(if accept_json { + let inscriptions = index.get_inscriptions_in_block(height)?; + Json(BlockJson::new( + block, + Height(height), + Self::index_height(&index)?, + inscriptions, + )) + .into_response() + } else { + let (featured_inscriptions, total_num) = + index.get_highest_paying_inscriptions_in_block(height, 8)?; + BlockHtml::new( + block, + Height(height), + Self::index_height(&index)?, + total_num, + featured_inscriptions, + ) + .page(server_config) + .into_response() + }) }) } @@ -732,99 +765,112 @@ impl Server { Extension(index): Extension>, Path(txid): Path, ) -> ServerResult> { - let transaction = index - .get_transaction(txid)? - .ok_or_not_found(|| format!("transaction {txid}"))?; + task::block_in_place(|| { + let transaction = index + .get_transaction(txid)? + .ok_or_not_found(|| format!("transaction {txid}"))?; - let inscription_count = index.inscription_count(txid)?; + let inscription_count = index.inscription_count(txid)?; - let blockhash = index.get_transaction_blockhash(txid)?; + let blockhash = index.get_transaction_blockhash(txid)?; - Ok( - TransactionHtml { - blockhash, - transaction, - txid, - inscription_count, - chain: server_config.chain, - etching: index.get_etching(txid)?, - } - .page(server_config), - ) + Ok( + TransactionHtml { + blockhash, + transaction, + txid, + inscription_count, + chain: server_config.chain, + etching: index.get_etching(txid)?, + } + .page(server_config), + ) + }) } async fn metadata( Extension(index): Extension>, Path(inscription_id): Path, ) -> ServerResult> { - let metadata = index - .get_inscription_by_id(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))? - .metadata - .ok_or_not_found(|| format!("inscription {inscription_id} metadata"))?; - - Ok(Json(hex::encode(metadata))) + task::block_in_place(|| { + let metadata = index + .get_inscription_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))? + .metadata + .ok_or_not_found(|| format!("inscription {inscription_id} metadata"))?; + + Ok(Json(hex::encode(metadata))) + }) } async fn status( Extension(server_config): Extension>, Extension(index): Extension>, - ) -> ServerResult> { - Ok(index.status()?.page(server_config)) + AcceptJson(accept_json): AcceptJson, + ) -> ServerResult { + task::block_in_place(|| { + Ok(if accept_json { + Json(index.status()?).into_response() + } else { + index.status()?.page(server_config).into_response() + }) + }) } async fn search_by_query( Extension(index): Extension>, Query(search): Query, ) -> ServerResult { - Self::search(&index, &search.query).await + Self::search(index, search.query).await } async fn search_by_path( Extension(index): Extension>, Path(search): Path, ) -> ServerResult { - Self::search(&index, &search.query).await + Self::search(index, search.query).await } - async fn search(index: &Index, query: &str) -> ServerResult { - Self::search_inner(index, query) + async fn search(index: Arc, query: String) -> ServerResult { + Self::search_inner(index, query).await } - fn search_inner(index: &Index, query: &str) -> ServerResult { - lazy_static! { - static ref HASH: Regex = Regex::new(r"^[[:xdigit:]]{64}$").unwrap(); - static ref INSCRIPTION_ID: Regex = Regex::new(r"^[[:xdigit:]]{64}i\d+$").unwrap(); - static ref OUTPOINT: Regex = Regex::new(r"^[[:xdigit:]]{64}:\d+$").unwrap(); - static ref RUNE: Regex = Regex::new(r"^[A-Z•.]+$").unwrap(); - static ref RUNE_ID: Regex = Regex::new(r"^[0-9]+/[0-9]+$").unwrap(); - } + async fn search_inner(index: Arc, query: String) -> ServerResult { + task::block_in_place(|| { + lazy_static! { + static ref HASH: Regex = Regex::new(r"^[[:xdigit:]]{64}$").unwrap(); + static ref INSCRIPTION_ID: Regex = Regex::new(r"^[[:xdigit:]]{64}i\d+$").unwrap(); + static ref OUTPOINT: Regex = Regex::new(r"^[[:xdigit:]]{64}:\d+$").unwrap(); + static ref RUNE: Regex = Regex::new(r"^[A-Z•.]+$").unwrap(); + static ref RUNE_ID: Regex = Regex::new(r"^[0-9]+/[0-9]+$").unwrap(); + } - let query = query.trim(); + let query = query.trim(); - if HASH.is_match(query) { - if index.block_header(query.parse().unwrap())?.is_some() { - Ok(Redirect::to(&format!("/block/{query}"))) + if HASH.is_match(query) { + if index.block_header(query.parse().unwrap())?.is_some() { + Ok(Redirect::to(&format!("/block/{query}"))) + } else { + Ok(Redirect::to(&format!("/tx/{query}"))) + } + } else if OUTPOINT.is_match(query) { + Ok(Redirect::to(&format!("/output/{query}"))) + } else if INSCRIPTION_ID.is_match(query) { + Ok(Redirect::to(&format!("/inscription/{query}"))) + } else if RUNE.is_match(query) { + Ok(Redirect::to(&format!("/rune/{query}"))) + } else if RUNE_ID.is_match(query) { + let id = query + .parse::() + .map_err(|err| ServerError::BadRequest(err.to_string()))?; + + let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?; + + Ok(Redirect::to(&format!("/rune/{rune}"))) } else { - Ok(Redirect::to(&format!("/tx/{query}"))) + Ok(Redirect::to(&format!("/sat/{query}"))) } - } else if OUTPOINT.is_match(query) { - Ok(Redirect::to(&format!("/output/{query}"))) - } else if INSCRIPTION_ID.is_match(query) { - Ok(Redirect::to(&format!("/inscription/{query}"))) - } else if RUNE.is_match(query) { - Ok(Redirect::to(&format!("/rune/{query}"))) - } else if RUNE_ID.is_match(query) { - let id = query - .parse::() - .map_err(|err| ServerError::BadRequest(err.to_string()))?; - - let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?; - - Ok(Redirect::to(&format!("/rune/{rune}"))) - } else { - Ok(Redirect::to(&format!("/sat/{query}"))) - } + }) } async fn favicon(user_agent: Option>) -> ServerResult { @@ -859,42 +905,44 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, ) -> ServerResult { - let mut builder = rss::ChannelBuilder::default(); + task::block_in_place(|| { + let mut builder = rss::ChannelBuilder::default(); - let chain = server_config.chain; - match chain { - Chain::Mainnet => builder.title("Inscriptions".to_string()), - _ => builder.title(format!("Inscriptions – {chain:?}")), - }; + let chain = server_config.chain; + match chain { + Chain::Mainnet => builder.title("Inscriptions".to_string()), + _ => builder.title(format!("Inscriptions – {chain:?}")), + }; - builder.generator(Some("ord".to_string())); - - for (number, id) in index.get_feed_inscriptions(300)? { - builder.item( - rss::ItemBuilder::default() - .title(Some(format!("Inscription {number}"))) - .link(Some(format!("/inscription/{id}"))) - .guid(Some(rss::Guid { - value: format!("/inscription/{id}"), - permalink: true, - })) - .build(), - ); - } + builder.generator(Some("ord".to_string())); + + for (number, id) in index.get_feed_inscriptions(300)? { + builder.item( + rss::ItemBuilder::default() + .title(Some(format!("Inscription {number}"))) + .link(Some(format!("/inscription/{id}"))) + .guid(Some(rss::Guid { + value: format!("/inscription/{id}"), + permalink: true, + })) + .build(), + ); + } - Ok( - ( - [ - (header::CONTENT_TYPE, "application/rss+xml"), - ( - header::CONTENT_SECURITY_POLICY, - "default-src 'unsafe-inline'", - ), - ], - builder.build().to_string(), + Ok( + ( + [ + (header::CONTENT_TYPE, "application/rss+xml"), + ( + header::CONTENT_SECURITY_POLICY, + "default-src 'unsafe-inline'", + ), + ], + builder.build().to_string(), + ) + .into_response(), ) - .into_response(), - ) + }) } async fn static_asset(Path(path): Path) -> ServerResult { @@ -915,67 +963,79 @@ impl Server { } async fn block_count(Extension(index): Extension>) -> ServerResult { - Ok(index.block_count()?.to_string()) + task::block_in_place(|| Ok(index.block_count()?.to_string())) } async fn block_height(Extension(index): Extension>) -> ServerResult { - Ok( - index - .block_height()? - .ok_or_not_found(|| "blockheight")? - .to_string(), - ) + task::block_in_place(|| { + Ok( + index + .block_height()? + .ok_or_not_found(|| "blockheight")? + .to_string(), + ) + }) } async fn block_hash(Extension(index): Extension>) -> ServerResult { - Ok( - index - .block_hash(None)? - .ok_or_not_found(|| "blockhash")? - .to_string(), - ) + task::block_in_place(|| { + Ok( + index + .block_hash(None)? + .ok_or_not_found(|| "blockhash")? + .to_string(), + ) + }) } async fn block_hash_json(Extension(index): Extension>) -> ServerResult> { - Ok(Json( - index - .block_hash(None)? - .ok_or_not_found(|| "blockhash")? - .to_string(), - )) + task::block_in_place(|| { + Ok(Json( + index + .block_hash(None)? + .ok_or_not_found(|| "blockhash")? + .to_string(), + )) + }) } async fn block_hash_from_height( Extension(index): Extension>, Path(height): Path, ) -> ServerResult { - Ok( - index - .block_hash(Some(height))? - .ok_or_not_found(|| "blockhash")? - .to_string(), - ) + task::block_in_place(|| { + Ok( + index + .block_hash(Some(height))? + .ok_or_not_found(|| "blockhash")? + .to_string(), + ) + }) } async fn block_hash_from_height_json( Extension(index): Extension>, Path(height): Path, ) -> ServerResult> { - Ok(Json( - index - .block_hash(Some(height))? - .ok_or_not_found(|| "blockhash")? - .to_string(), - )) + task::block_in_place(|| { + Ok(Json( + index + .block_hash(Some(height))? + .ok_or_not_found(|| "blockhash")? + .to_string(), + )) + }) } async fn block_time(Extension(index): Extension>) -> ServerResult { - Ok( - index - .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)? - .unix_timestamp() - .to_string(), - ) + task::block_in_place(|| { + Ok( + index + .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)? + .unix_timestamp() + .to_string(), + ) + }) } async fn input( @@ -983,25 +1043,27 @@ impl Server { Extension(index): Extension>, Path(path): Path<(u32, usize, usize)>, ) -> ServerResult> { - let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2); + task::block_in_place(|| { + let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2); - let block = index - .get_block_by_height(path.0)? - .ok_or_not_found(not_found)?; + let block = index + .get_block_by_height(path.0)? + .ok_or_not_found(not_found)?; - let transaction = block - .txdata - .into_iter() - .nth(path.1) - .ok_or_not_found(not_found)?; + let transaction = block + .txdata + .into_iter() + .nth(path.1) + .ok_or_not_found(not_found)?; - let input = transaction - .input - .into_iter() - .nth(path.2) - .ok_or_not_found(not_found)?; + let input = transaction + .input + .into_iter() + .nth(path.2) + .ok_or_not_found(not_found)?; - Ok(InputHtml { path, input }.page(server_config)) + Ok(InputHtml { path, input }.page(server_config)) + }) } async fn faq() -> Redirect { @@ -1019,25 +1081,27 @@ impl Server { Path(inscription_id): Path, accept_encoding: AcceptEncoding, ) -> ServerResult { - if config.is_hidden(inscription_id) { - return Ok(PreviewUnknownHtml.into_response()); - } + task::block_in_place(|| { + if config.is_hidden(inscription_id) { + return Ok(PreviewUnknownHtml.into_response()); + } - let mut inscription = index - .get_inscription_by_id(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + let mut inscription = index + .get_inscription_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - if let Some(delegate) = inscription.delegate() { - inscription = index - .get_inscription_by_id(delegate)? - .ok_or_not_found(|| format!("delegate {inscription_id}"))? - } + if let Some(delegate) = inscription.delegate() { + inscription = index + .get_inscription_by_id(delegate)? + .ok_or_not_found(|| format!("delegate {inscription_id}"))? + } - Ok( - Self::content_response(inscription, accept_encoding, &server_config)? - .ok_or_not_found(|| format!("inscription {inscription_id} content"))? - .into_response(), - ) + Ok( + Self::content_response(inscription, accept_encoding, &server_config)? + .ok_or_not_found(|| format!("inscription {inscription_id} content"))? + .into_response(), + ) + }) } fn content_response( @@ -1117,94 +1181,96 @@ impl Server { Path(inscription_id): Path, accept_encoding: AcceptEncoding, ) -> ServerResult { - if config.is_hidden(inscription_id) { - return Ok(PreviewUnknownHtml.into_response()); - } + task::block_in_place(|| { + if config.is_hidden(inscription_id) { + return Ok(PreviewUnknownHtml.into_response()); + } - let mut inscription = index - .get_inscription_by_id(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + let mut inscription = index + .get_inscription_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - if let Some(delegate) = inscription.delegate() { - inscription = index - .get_inscription_by_id(delegate)? - .ok_or_not_found(|| format!("delegate {inscription_id}"))? - } + if let Some(delegate) = inscription.delegate() { + inscription = index + .get_inscription_by_id(delegate)? + .ok_or_not_found(|| format!("delegate {inscription_id}"))? + } - match inscription.media() { - Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()), - Media::Code(language) => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://cdn.jsdelivr.net", - )], - PreviewCodeHtml { - inscription_id, - language, - }, - ) - .into_response(), - ), - Media::Font => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self'; style-src 'self' 'unsafe-inline';", - )], - PreviewFontHtml { inscription_id }, - ) - .into_response(), - ), - Media::Iframe => Ok( - Self::content_response(inscription, accept_encoding, &server_config)? - .ok_or_not_found(|| format!("inscription {inscription_id} content"))? - .into_response(), - ), - Media::Image => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "default-src 'self' 'unsafe-inline'", - )], - PreviewImageHtml { inscription_id }, - ) - .into_response(), - ), - Media::Markdown => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://cdn.jsdelivr.net", - )], - PreviewMarkdownHtml { inscription_id }, - ) - .into_response(), - ), - Media::Model => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://ajax.googleapis.com", - )], - PreviewModelHtml { inscription_id }, - ) - .into_response(), - ), - Media::Pdf => Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - "script-src-elem 'self' https://cdn.jsdelivr.net", - )], - PreviewPdfHtml { inscription_id }, - ) - .into_response(), - ), - Media::Text => Ok(PreviewTextHtml { inscription_id }.into_response()), - Media::Unknown => Ok(PreviewUnknownHtml.into_response()), - Media::Video => Ok(PreviewVideoHtml { inscription_id }.into_response()), - } + match inscription.media() { + Media::Audio => Ok(PreviewAudioHtml { inscription_id }.into_response()), + Media::Code(language) => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self' https://cdn.jsdelivr.net", + )], + PreviewCodeHtml { + inscription_id, + language, + }, + ) + .into_response(), + ), + Media::Font => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self'; style-src 'self' 'unsafe-inline';", + )], + PreviewFontHtml { inscription_id }, + ) + .into_response(), + ), + Media::Iframe => Ok( + Self::content_response(inscription, accept_encoding, &server_config)? + .ok_or_not_found(|| format!("inscription {inscription_id} content"))? + .into_response(), + ), + Media::Image => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "default-src 'self' 'unsafe-inline'", + )], + PreviewImageHtml { inscription_id }, + ) + .into_response(), + ), + Media::Markdown => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self' https://cdn.jsdelivr.net", + )], + PreviewMarkdownHtml { inscription_id }, + ) + .into_response(), + ), + Media::Model => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self' https://ajax.googleapis.com", + )], + PreviewModelHtml { inscription_id }, + ) + .into_response(), + ), + Media::Pdf => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self' https://cdn.jsdelivr.net", + )], + PreviewPdfHtml { inscription_id }, + ) + .into_response(), + ), + Media::Text => Ok(PreviewTextHtml { inscription_id }.into_response()), + Media::Unknown => Ok(PreviewUnknownHtml.into_response()), + Media::Video => Ok(PreviewVideoHtml { inscription_id }.into_response()), + } + }) } async fn inscription( @@ -1213,59 +1279,66 @@ impl Server { Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let info = - Index::inscription_info(&index, query)?.ok_or_not_found(|| format!("inscription {query}"))?; - - Ok(if accept_json { - Json(InscriptionJson { - inscription_id: info.entry.id, - children: info.children, - inscription_number: info.entry.inscription_number, - genesis_height: info.entry.height, - parent: info.parent, - genesis_fee: info.entry.fee, - output_value: info.output.as_ref().map(|o| o.value), - address: info - .output - .as_ref() - .and_then(|o| { - server_config - .chain - .address_from_script(&o.script_pubkey) - .ok() - }) - .map(|address| address.to_string()), - sat: info.entry.sat, - satpoint: info.satpoint, - content_type: info.inscription.content_type().map(|s| s.to_string()), - content_length: info.inscription.content_length(), - timestamp: timestamp(info.entry.timestamp).timestamp(), - previous: info.previous, - next: info.next, - rune: info.rune, + task::block_in_place(|| { + let info = Index::inscription_info(&index, query)? + .ok_or_not_found(|| format!("inscription {query}"))?; + + Ok(if accept_json { + Json(InscriptionJson { + inscription_id: info.entry.id, + charms: Charm::ALL + .iter() + .filter(|charm| charm.is_set(info.charms)) + .map(|charm| charm.title().into()) + .collect(), + children: info.children, + inscription_number: info.entry.inscription_number, + genesis_height: info.entry.height, + parent: info.parent, + genesis_fee: info.entry.fee, + output_value: info.output.as_ref().map(|o| o.value), + address: info + .output + .as_ref() + .and_then(|o| { + server_config + .chain + .address_from_script(&o.script_pubkey) + .ok() + }) + .map(|address| address.to_string()), + sat: info.entry.sat, + satpoint: info.satpoint, + content_type: info.inscription.content_type().map(|s| s.to_string()), + content_length: info.inscription.content_length(), + timestamp: timestamp(info.entry.timestamp).timestamp(), + previous: info.previous, + next: info.next, + rune: info.rune, + }) + .into_response() + } else { + InscriptionHtml { + chain: server_config.chain, + charms: info.charms, + children: info.children, + genesis_fee: info.entry.fee, + genesis_height: info.entry.height, + inscription: info.inscription, + inscription_id: info.entry.id, + inscription_number: info.entry.inscription_number, + next: info.next, + output: info.output, + parent: info.parent, + previous: info.previous, + rune: info.rune, + sat: info.entry.sat, + satpoint: info.satpoint, + timestamp: timestamp(info.entry.timestamp), + } + .page(server_config) + .into_response() }) - .into_response() - } else { - InscriptionHtml { - chain: server_config.chain, - charms: info.charms, - children: info.children, - genesis_fee: info.entry.fee, - genesis_height: info.entry.height, - inscription: info.inscription, - inscription_id: info.entry.id, - inscription_number: info.entry.inscription_number, - next: info.next, - output: info.output, - parent: info.parent, - previous: info.previous, - rune: info.rune, - sat: info.entry.sat, - satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp), - } - .page(server_config) - .into_response() }) } @@ -1281,21 +1354,23 @@ impl Server { Extension(index): Extension>, Path(page_index): Path, ) -> ServerResult { - let (collections, more_collections) = index.get_collections_paginated(100, page_index)?; + task::block_in_place(|| { + let (collections, more_collections) = index.get_collections_paginated(100, page_index)?; - let prev = page_index.checked_sub(1); + let prev = page_index.checked_sub(1); - let next = more_collections.then_some(page_index + 1); + let next = more_collections.then_some(page_index + 1); - Ok( - CollectionsHtml { - inscriptions: collections, - prev, - next, - } - .page(server_config) - .into_response(), - ) + Ok( + CollectionsHtml { + inscriptions: collections, + prev, + next, + } + .page(server_config) + .into_response(), + ) + }) } async fn children( @@ -1316,30 +1391,32 @@ impl Server { Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, ) -> ServerResult { - let entry = index - .get_inscription_entry(parent)? - .ok_or_not_found(|| format!("inscription {parent}"))?; + task::block_in_place(|| { + let entry = index + .get_inscription_entry(parent)? + .ok_or_not_found(|| format!("inscription {parent}"))?; - let parent_number = entry.inscription_number; + let parent_number = entry.inscription_number; - let (children, more_children) = - index.get_children_by_sequence_number_paginated(entry.sequence_number, 100, page)?; + let (children, more_children) = + index.get_children_by_sequence_number_paginated(entry.sequence_number, 100, page)?; - let prev_page = page.checked_sub(1); + let prev_page = page.checked_sub(1); - let next_page = more_children.then_some(page + 1); + let next_page = more_children.then_some(page + 1); - Ok( - ChildrenHtml { - parent, - parent_number, - children, - prev_page, - next_page, - } - .page(server_config) - .into_response(), - ) + Ok( + ChildrenHtml { + parent, + parent_number, + children, + prev_page, + next_page, + } + .page(server_config) + .into_response(), + ) + }) } async fn children_recursive( @@ -1353,15 +1430,17 @@ impl Server { Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, ) -> ServerResult { - let parent_sequence_number = index - .get_inscription_entry(parent)? - .ok_or_not_found(|| format!("inscription {parent}"))? - .sequence_number; + task::block_in_place(|| { + let parent_sequence_number = index + .get_inscription_entry(parent)? + .ok_or_not_found(|| format!("inscription {parent}"))? + .sequence_number; - let (ids, more) = - index.get_children_by_sequence_number_paginated(parent_sequence_number, 100, page)?; + let (ids, more) = + index.get_children_by_sequence_number_paginated(parent_sequence_number, 100, page)?; - Ok(Json(ChildrenJson { ids, more, page }).into_response()) + Ok(Json(ChildrenJson { ids, more, page }).into_response()) + }) } async fn inscriptions( @@ -1381,30 +1460,32 @@ impl Server { async fn inscriptions_paginated( Extension(server_config): Extension>, Extension(index): Extension>, - Path(page_index): Path, + Path(page_index): Path, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let (inscriptions, more_inscriptions) = index.get_inscriptions_paginated(100, page_index)?; + task::block_in_place(|| { + let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?; - let prev = page_index.checked_sub(1); + let prev = page_index.checked_sub(1); - let next = more_inscriptions.then_some(page_index + 1); + let next = more.then_some(page_index + 1); - Ok(if accept_json { - Json(InscriptionsJson { - inscriptions, - page_index, - more: more_inscriptions, + Ok(if accept_json { + Json(InscriptionsJson { + inscriptions, + page_index, + more, + }) + .into_response() + } else { + InscriptionsHtml { + inscriptions, + next, + prev, + } + .page(server_config) + .into_response() }) - .into_response() - } else { - InscriptionsHtml { - inscriptions, - next, - prev, - } - .page(server_config) - .into_response() }) } @@ -1426,41 +1507,46 @@ impl Server { async fn inscriptions_in_block_paginated( Extension(server_config): Extension>, Extension(index): Extension>, - Path((block_height, page_index)): Path<(u32, usize)>, + Path((block_height, page_index)): Path<(u32, u32)>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let page_size = 100; + task::block_in_place(|| { + let page_size = 100; - let mut inscriptions = index - .get_inscriptions_in_block(block_height)? - .into_iter() - .skip(page_index.saturating_mul(page_size)) - .take(page_size.saturating_add(1)) - .collect::>(); + let page_index_usize = usize::try_from(page_index).unwrap_or(usize::MAX); + let page_size_usize = usize::try_from(page_size).unwrap_or(usize::MAX); - let more = inscriptions.len() > page_size; + let mut inscriptions = index + .get_inscriptions_in_block(block_height)? + .into_iter() + .skip(page_index_usize.saturating_mul(page_size_usize)) + .take(page_size_usize.saturating_add(1)) + .collect::>(); - if more { - inscriptions.pop(); - } + let more = inscriptions.len() > page_size_usize; - Ok(if accept_json { - Json(InscriptionsJson { - inscriptions, - page_index, - more, + if more { + inscriptions.pop(); + } + + Ok(if accept_json { + Json(InscriptionsJson { + inscriptions, + page_index, + more, + }) + .into_response() + } else { + InscriptionsBlockHtml::new( + block_height, + index.block_height()?.unwrap_or(Height(0)).n(), + inscriptions, + more, + page_index, + )? + .page(server_config) + .into_response() }) - .into_response() - } else { - InscriptionsBlockHtml::new( - block_height, - index.block_height()?.unwrap_or(Height(0)).n(), - inscriptions, - more, - page_index, - )? - .page(server_config) - .into_response() }) } @@ -1475,30 +1561,34 @@ impl Server { Extension(index): Extension>, Path((sat, page)): Path<(u64, u64)>, ) -> ServerResult> { - if !index.has_sat_index() { - return Err(ServerError::NotFound( - "this server has no sat index".to_string(), - )); - } + task::block_in_place(|| { + if !index.has_sat_index() { + return Err(ServerError::NotFound( + "this server has no sat index".to_string(), + )); + } - let (ids, more) = index.get_inscription_ids_by_sat_paginated(Sat(sat), 100, page)?; + let (ids, more) = index.get_inscription_ids_by_sat_paginated(Sat(sat), 100, page)?; - Ok(Json(SatInscriptionsJson { ids, more, page })) + Ok(Json(SatInscriptionsJson { ids, more, page })) + }) } async fn sat_inscription_at_index( Extension(index): Extension>, Path((DeserializeFromStr(sat), inscription_index)): Path<(DeserializeFromStr, isize)>, ) -> ServerResult> { - if !index.has_sat_index() { - return Err(ServerError::NotFound( - "this server has no sat index".to_string(), - )); - } + task::block_in_place(|| { + if !index.has_sat_index() { + return Err(ServerError::NotFound( + "this server has no sat index".to_string(), + )); + } - let id = index.get_inscription_id_by_sat_indexed(sat, inscription_index)?; + let id = index.get_inscription_id_by_sat_indexed(sat, inscription_index)?; - Ok(Json(SatInscriptionJson { id })) + Ok(Json(SatInscriptionJson { id })) + }) } async fn redirect_http_to_https( @@ -2552,6 +2642,8 @@ mod tests { StatusCode::OK, ".*

Status

+
chain
+
mainnet
height
0
inscriptions
@@ -4359,6 +4451,45 @@ next ); } + #[test] + fn charm_vindicated() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(110); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (1, 0, 0, Witness::default()), + (2, 0, 0, inscription("text/plain", "cursed").to_witness()), + ], + outputs: 2, + ..Default::default() + }); + + let id = InscriptionId { txid, index: 0 }; + + server.mine_blocks(1); + + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
charms
+
+ ❤️‍🔥 +
+ .* +
+.* +" + ), + ); + } + #[test] fn charm_coin() { let server = TestServer::new_with_regtest_with_index_sats(); diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index de9ae4c254..73faadbb33 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -158,13 +158,8 @@ pub(crate) fn get_change_address(client: &Client, chain: Chain) -> Result Result { - check_version(options.bitcoin_rpc_client(None)?)?.create_wallet( - &wallet, - None, - None, - None, - None, - )?; + check_version(options.bitcoin_rpc_client(None)?)? + .create_wallet(&wallet, None, None, None, None)?; let _client = options.bitcoin_rpc_client(Some(wallet))?; diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs index b02ea68015..1c00a36370 100644 --- a/src/subcommand/wallet/etch.rs +++ b/src/subcommand/wallet/etch.rs @@ -16,6 +16,7 @@ pub(crate) struct Etch { #[derive(Serialize, Deserialize, Debug)] pub struct Output { + pub rune: SpacedRune, pub transaction: Txid, } @@ -123,6 +124,9 @@ impl Etch { let transaction = client.send_raw_transaction(&signed_transaction)?; - Ok(Box::new(Output { transaction })) + Ok(Box::new(Output { + rune: self.rune, + transaction, + })) } } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 71dc9b039a..a27f88e438 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -85,10 +85,11 @@ pub(crate) struct Inscribe { pub(crate) json_metadata: Option, #[clap(long, help = "Set inscription metaprotocol to .")] pub(crate) metaprotocol: Option, - #[arg(long, help = "Do not back up recovery key.")] + #[arg(long, alias = "nobackup", help = "Do not back up recovery key.")] pub(crate) no_backup: bool, #[arg( long, + alias = "nolimit", help = "Do not check that transactions are equal to or below the MAX_STANDARD_TX_WEIGHT of 400,000 weight units. Transactions over this limit are currently nonstandard and will not be relayed by bitcoind in its default configuration. Do not use this flag unless you understand the implications." )] pub(crate) no_limit: bool, diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 24575ceac2..bf1d98b0c4 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -182,7 +182,7 @@ impl Send { Self::lock_non_cardinal_outputs(client, &inscriptions, &runic_outputs, unspent_outputs)?; - let (id, entry) = index + let (id, entry, _parent) = index .rune(spaced_rune.rune)? .with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?; diff --git a/src/templates.rs b/src/templates.rs index aae0b5f953..8906adca8a 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -20,8 +20,8 @@ pub(crate) use { }, range::RangeHtml, rare::RareTxt, - rune::RuneHtml, - runes::RunesHtml, + rune::{RuneHtml, RuneJson}, + runes::{RunesHtml, RunesJson}, sat::{SatHtml, SatInscriptionJson, SatInscriptionsJson, SatJson}, server_config::ServerConfig, status::StatusHtml, @@ -44,10 +44,10 @@ pub mod output; mod preview; mod range; mod rare; -mod rune; -mod runes; +pub mod rune; +pub mod runes; pub mod sat; -mod status; +pub mod status; mod transaction; #[derive(Boilerplate)] @@ -74,7 +74,7 @@ where fn superscript(&self) -> String { if self.config.chain == Chain::Mainnet { - "alpha".into() + "beta".into() } else { self.config.chain.to_string() } @@ -142,7 +142,7 @@ mod tests {