diff --git a/CHANGELOG.md b/CHANGELOG.md index cca7b3b170..112b7292c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,39 @@ 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-gm1](https://github.com/gmart7t2/ord/releases/tag/0.14.1-gm1) - 2024-01-05 ---------------------------------------------------------------------------------- ### Added -- Merged 0.13.1 from upstream. +- Merged 0.14.1 from upstream. [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 1ae0b37272..4882ab9a53 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" @@ -532,7 +532,7 @@ dependencies = [ "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite 2.1.0", + "futures-lite 2.2.0", "piper", "tracing", ] @@ -548,7 +548,7 @@ dependencies = [ "new_mime_guess", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -671,9 +671,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", @@ -681,9 +681,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", @@ -700,7 +700,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -771,9 +771,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", ] @@ -825,34 +825,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" @@ -919,7 +913,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -941,7 +935,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1071,7 +1065,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1132,9 +1126,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", @@ -1147,7 +1141,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", ] @@ -1277,9 +1271,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", @@ -1293,7 +1287,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1303,7 +1297,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", ] @@ -1457,7 +1451,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", @@ -1811,9 +1805,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" @@ -2119,7 +2113,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2148,13 +2142,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.14.1-gm1" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", "axum", "axum-server", - "base64 0.21.5", + "base64 0.21.6", "bech32", "bip39", "bitcoin", @@ -2190,7 +2184,7 @@ dependencies = [ "reqwest", "rss", "rust-embed", - "rustls 0.22.1", + "rustls 0.22.2", "rustls-acme", "serde", "serde_json", @@ -2295,7 +2289,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2410,9 +2404,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", ] @@ -2584,7 +2578,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", @@ -2678,7 +2672,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.46", + "syn 2.0.48", "walkdir", ] @@ -2757,14 +2751,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", ] @@ -2804,7 +2798,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]] @@ -2825,9 +2819,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", @@ -2932,29 +2926,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", @@ -3123,9 +3117,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", @@ -3152,9 +3146,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", @@ -3241,7 +3235,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3323,7 +3317,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -3629,7 +3623,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -3663,7 +3657,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 c1980ca6fb..ff97614705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.14.1-gm1" +version = "0.15.0" license = "CC0-1.0" edition = "2021" autotests = false diff --git a/README.md b/README.md index 8b38c20842..e0a1d96a9d 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,9 @@ 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://discord.gg/87cjuz4FYg) to chat with fellow ordinal degenerates. diff --git a/deploy/setup b/deploy/setup index 36d384e54e..b32ac5d891 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/bitcoind.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/bitcoind.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/bitcoind + CSP_ORIGIN=ordinals.com ufw allow 8333 ;; regtest) + COOKIE_FILE_DIR=/var/lib/bitcoind/regtest + CSP_ORIGIN=regtest.ordinals.com ufw allow 18444 ;; signet) + COOKIE_FILE_DIR=/var/lib/bitcoind/signet + CSP_ORIGIN=signet.ordinals.com ufw allow 38333 ;; test) + COOKIE_FILE_DIR=/var/lib/bitcoind/testnet3 + CSP_ORIGIN=testnet.ordinals.com ufw allow 18333 ;; *) @@ -81,6 +60,18 @@ case $CHAIN in ;; esac +mkdir -p \ + /etc/systemd/system/bitcoind.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/bitcoind.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 bitcoind; then @@ -117,25 +108,6 @@ systemctl daemon-reload systemctl enable bitcoind systemctl restart bitcoind -case $CHAIN in - main) - COOKIE_FILE_DIR=/var/lib/bitcoind - ;; - regtest) - COOKIE_FILE_DIR=/var/lib/bitcoind/regtest - ;; - signet) - COOKIE_FILE_DIR=/var/lib/bitcoind/signet - ;; - test) - COOKIE_FILE_DIR=/var/lib/bitcoind/testnet3 - ;; - *) - echo "Unknown chain: $CHAIN" - exit 1 - ;; -esac - while [[ ! -f $COOKIE_FILE_DIR/.cookie ]]; do echo "Waiting for bitcoind…" 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 6f81b94fe4..ce2395594d 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; @@ -88,18 +88,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 { @@ -441,6 +441,7 @@ impl Index { Ok(StatusHtml { blessed_inscriptions, + chain: self.options.chain(), cursed_inscriptions, height, inscriptions: blessed_inscriptions + cursed_inscriptions, @@ -865,29 +866,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 @@ -917,11 +899,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> { @@ -1770,23 +1748,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(); @@ -3568,7 +3555,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); @@ -3586,6 +3573,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() { @@ -5788,4 +5802,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 013f73a1c4..34cc3c8bcd 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 b8eaa2a23a..b81a3ae9ca 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, @@ -79,6 +80,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.ignore_cursed || self.height >= self.chain.jubilee_height(); let mut total_input_value = 0; let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); @@ -146,9 +148,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { index: id_counter, }; - let curse = if self.ignore_cursed || 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) @@ -171,17 +171,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) @@ -205,13 +206,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, }, }); @@ -414,6 +416,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(); @@ -477,6 +480,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 721e042852..32ca8fd1f9 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 b9bf2b2983..7b4bb9e0a7 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::{ @@ -37,8 +37,6 @@ use { AcmeConfig, }, std::{cmp::Ordering, io::Read, str, sync::Arc}, - // std::{cmp::Ordering, collections::HashMap, str, sync::Arc}, - tokio::time::sleep, tokio_stream::StreamExt, tower_http::{ compression::CompressionLayer, @@ -123,12 +121,20 @@ struct MyInscriptionJson { number: i32, sequence_number: u32, id: InscriptionId, + #[serde(default, skip_serializing_if = "Option::is_none")] parent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] address: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] output_value: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] sat: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + delegate: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] content_length: Option, - content_type: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + content_type: Option, timestamp: u32, genesis_height: u32, genesis_fee: u64, @@ -136,6 +142,7 @@ struct MyInscriptionJson { location: String, output: String, offset: u64, + #[serde(default, skip_serializing_if = "Vec::is_empty")] children: Vec, } @@ -229,6 +236,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 { @@ -240,8 +249,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)); }); @@ -547,26 +558,30 @@ impl Server { } async fn children_all(Extension(index): Extension>) -> ServerResult { - log::info!("GET /children"); - let mut result = "parent child\n".to_string(); - for (parent, child) in index.get_children()? { - result += format!("{} {}\n", parent, child).as_str(); - } - Ok(result) + task::block_in_place(|| { + log::info!("GET /children"); + let mut result = "parent child\n".to_string(); + for (parent, child) in index.get_children()? { + result += format!("{} {}\n", parent, child).as_str(); + } + Ok(result) + }) } async fn clock(Extension(index): Extension>) -> ServerResult { - log::info!("GET /clock"); - Ok( - ( - [( - header::CONTENT_SECURITY_POLICY, - HeaderValue::from_static("default-src 'unsafe-inline'"), - )], - ClockSvg::new(Self::index_height(&index)?), + task::block_in_place(|| { + log::info!("GET /clock"); + 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( @@ -575,49 +590,50 @@ impl Server { Path(DeserializeFromStr(sat)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - log::info!("GET /sat/{sat}"); - 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(|| { + log::info!("GET /sat/{sat}"); + 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() }) } async fn ordinal(Path(sat): Path) -> Redirect { - log::info!("GET /ordinal/{sat}"); Redirect::to(&format!("/sat/{sat}")) } @@ -627,60 +643,62 @@ impl Server { Path(outpoint): Path, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - log::info!("GET /output/{outpoint}"); - let list = index.list(outpoint)?; + task::block_in_place(|| { + log::info!("GET /output/{outpoint}"); + 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() + }) }) } @@ -689,73 +707,73 @@ impl Server { Extension(index): Extension>, Json(data): Json ) -> ServerResult { - log::info!("POST /outputs"); - - if !data.is_array() { - return Err(ServerError::BadRequest("expected array".to_string())); - } - - let mut result = Vec::new(); + task::block_in_place(|| { + log::info!("POST /outputs"); - for outpoint in data.as_array().unwrap() { - if !outpoint.is_string() { - return Err(ServerError::BadRequest("expected array of strings".to_string())); + if !data.is_array() { + return Err(ServerError::BadRequest("expected array".to_string())); } - match OutPoint::from_str(outpoint.as_str().unwrap()) { - Ok(outpoint) => { - sleep(Duration::from_millis(0)).await; + let mut result = Vec::new(); - let list = index.list(outpoint)?; - - 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; - } - } + for outpoint in data.as_array().unwrap() { + if !outpoint.is_string() { + return Err(ServerError::BadRequest("expected array of strings".to_string())); + } - 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}"))? - }; + match OutPoint::from_str(outpoint.as_str().unwrap()) { + Ok(outpoint) => { + let list = index.list(outpoint)?; - let inscriptions = index.get_inscriptions_on_output(outpoint)?; + let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { + let mut value = 0; - let runes = index.get_rune_balances_for_outpoint(outpoint)?; + if let Some(List::Unspent(ranges)) = &list { + for (start, end) in ranges { + value += end - start; + } + } - result.push( - Outputs {output: outpoint, details: - 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(), + .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)?; + + result.push( + Outputs {output: outpoint, details: + OutputJson::new( + outpoint, + list, + server_config.chain, + output, + inscriptions, + runes + .into_iter() + .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount)) + .collect(), + ) + } ) - } - ) + } + _ => return Err(ServerError::BadRequest(format!("expected array of OutPoint strings ({} is bad)", outpoint))), } - _ => return Err(ServerError::BadRequest(format!("expected array of OutPoint strings ({} is bad)", outpoint))), } - } - Ok(Json(result).into_response()) + Ok(Json(result).into_response()) + }) } async fn range( @@ -779,116 +797,138 @@ impl Server { Extension(index): Extension>, Json(data): Json ) -> ServerResult { - log::info!("POST /ranges"); + task::block_in_place(|| { + log::info!("POST /ranges"); - if !index.has_sat_index() { - return Err(ServerError::BadRequest("the /ranges endpoint needs the server to have a sat index".to_string())); - } + if !index.has_sat_index() { + return Err(ServerError::BadRequest("the /ranges endpoint needs the server to have a sat index".to_string())); + } - if !data.is_array() { - return Err(ServerError::BadRequest("expected array".to_string())); - } + if !data.is_array() { + return Err(ServerError::BadRequest("expected array".to_string())); + } - let mut result = Vec::new(); - let mut range_count = 0; - let mut outpoint_count = 0; - let start_time = Instant::now(); + let mut result = Vec::new(); + let mut range_count = 0; + let mut outpoint_count = 0; + let start_time = Instant::now(); - for outpoint in data.as_array().unwrap() { - if start_time.elapsed() > Duration::from_secs(5) { - return Err(ServerError::BadRequest("request timed out".to_string())); - } + for outpoint in data.as_array().unwrap() { + if start_time.elapsed() > Duration::from_secs(5) { + return Err(ServerError::BadRequest("request timed out".to_string())); + } - if !outpoint.is_string() { - return Err(ServerError::BadRequest("expected array of strings".to_string())); - } + if !outpoint.is_string() { + return Err(ServerError::BadRequest("expected array of strings".to_string())); + } - match OutPoint::from_str(outpoint.as_str().unwrap()) { - Ok(outpoint) => { - sleep(Duration::from_millis(0)).await; - match index.ranges(outpoint) { - Ok(ranges) => { - range_count += ranges.len(); - outpoint_count += 1; - result.push(Ranges {output: outpoint, ranges}); + match OutPoint::from_str(outpoint.as_str().unwrap()) { + Ok(outpoint) => { + match index.ranges(outpoint) { + Ok(ranges) => { + range_count += ranges.len(); + outpoint_count += 1; + result.push(Ranges {output: outpoint, ranges}); + } + _ => println!("no ranges for {}", outpoint), } - _ => println!("no ranges for {}", outpoint), } + _ => return Err(ServerError::BadRequest(format!("expected array of OutPoint strings ({} is bad)", outpoint))), } - _ => return Err(ServerError::BadRequest(format!("expected array of OutPoint strings ({} is bad)", outpoint))), } - } - println!(" {} ranges from {} outputs in {:?}", range_count, outpoint_count, start_time.elapsed()); + println!(" {} ranges from {} outputs in {:?}", range_count, outpoint_count, start_time.elapsed()); - Ok(Json(result).into_response()) + Ok(Json(result).into_response()) + }) } async fn rare_txt(Extension(index): Extension>) -> ServerResult { log::info!("GET /rare.txt"); - 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> { - log::info!("GET /rune/{spaced_rune}"); - 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(|| { + log::info!("GET /rune/{spaced_rune}"); + 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> { - log::info!("GET /runes"); - Ok( - RunesHtml { - entries: index.runes()?, - } - .page(server_config), - ) + AcceptJson(accept_json): AcceptJson, + ) -> ServerResult { + task::block_in_place(|| { + log::info!("GET /runes"); + 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> { - log::info!("GET /"); - Ok( - HomeHtml { - inscriptions: index.get_home_inscriptions()?, - } - .page(server_config), - ) + task::block_in_place(|| { + log::info!("GET /"); + 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 { @@ -902,50 +942,52 @@ impl Server { Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - let (block, height) = match query { - BlockQuery::Height(height) => { - log::info!("GET /block/{height}/"); - let block = index - .get_block_by_height(height)? - .ok_or_not_found(|| format!("block {height}"))?; - - (block, height) - } - BlockQuery::Hash(hash) => { - log::info!("GET /block/{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) => { + log::info!("GET /block/{height}/"); + let block = index + .get_block_by_height(height)? + .ok_or_not_found(|| format!("block {height}"))?; + + (block, height) + } + BlockQuery::Hash(hash) => { + log::info!("GET /block/{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() + }) }) } @@ -1006,18 +1048,19 @@ impl Server { index: Arc, inscription_ids: Vec, ) -> ServerResult { - let mut ret = String::from(""); - let mut tx_cache = HashMap::new(); - for inscription_id in inscription_ids { - sleep(Duration::from_millis(0)).await; - let satpoint = index - .get_inscription_satpoint_by_id(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let address = Self::outpoint_to_address(chain, &index, satpoint.outpoint, &mut tx_cache)?; - ret += &format!("{} {}\n", inscription_id, address); - } + task::block_in_place(|| { + let mut ret = String::from(""); + let mut tx_cache = HashMap::new(); + for inscription_id in inscription_ids { + let satpoint = index + .get_inscription_satpoint_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + let address = Self::outpoint_to_address(chain, &index, satpoint.outpoint, &mut tx_cache)?; + ret += &format!("{} {}\n", inscription_id, address); + } - Ok(ret) + Ok(ret) + }) } fn outpoint_to_address( @@ -1057,62 +1100,75 @@ impl Server { Extension(index): Extension>, Path(txid): Path, ) -> ServerResult> { - log::info!("GET /tx/{txid}"); - let transaction = index - .get_transaction(txid)? - .ok_or_not_found(|| format!("transaction {txid}"))?; + task::block_in_place(|| { + log::info!("GET /tx/{txid}"); + 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> { - log::info!("GET /r/metadata/{inscription_id}"); - 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(|| { + log::info!("GET /r/metadata/{inscription_id}"); + 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 stats(Extension(index): Extension>) -> ServerResult { - log::info!("GET /stats"); - let stats = index.get_stats()?; - Ok( - serde_json::to_string_pretty(&StatsJson { - version: env!("CARGO_PKG_VERSION").to_string(), - highest_block_indexed: stats.0, - lowest_inscription_number: stats.1, - highest_inscription_number: stats.2, - }) - .ok() - .unwrap(), - ) + task::block_in_place(|| { + log::info!("GET /stats"); + let stats = index.get_stats()?; + Ok( + serde_json::to_string_pretty(&StatsJson { + version: env!("CARGO_PKG_VERSION").to_string(), + highest_block_indexed: stats.0, + lowest_inscription_number: stats.1, + highest_inscription_number: stats.2, + }) + .ok() + .unwrap(), + ) + }) } 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( @@ -1120,7 +1176,7 @@ impl Server { Query(search): Query, ) -> ServerResult { log::info!("GET /search"); - Self::search(&index, &search.query).await + Self::search(index, search.query).await } async fn search_by_path( @@ -1128,47 +1184,49 @@ impl Server { Path(search): Path, ) -> ServerResult { log::info!("GET /search/{}", search.query); - 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 { @@ -1204,43 +1262,45 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, ) -> ServerResult { - log::info!("GET /feed.xml"); - 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:?}")), - }; + task::block_in_place(|| { + log::info!("GET /feed.xml"); + 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:?}")), + }; - 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 { @@ -1264,73 +1324,85 @@ impl Server { async fn block_count(Extension(index): Extension>) -> ServerResult { log::info!("GET /blockcount"); - Ok(index.block_count()?.to_string()) + task::block_in_place(|| Ok(index.block_count()?.to_string())) } async fn block_height(Extension(index): Extension>) -> ServerResult { - log::info!("GET /blockheight"); - Ok( - index - .block_height()? - .ok_or_not_found(|| "blockheight")? - .to_string(), - ) + task::block_in_place(|| { + log::info!("GET /blockheight"); + Ok( + index + .block_height()? + .ok_or_not_found(|| "blockheight")? + .to_string(), + ) + }) } async fn block_hash(Extension(index): Extension>) -> ServerResult { - log::info!("GET /blockhash"); - Ok( - index - .block_hash(None)? - .ok_or_not_found(|| "blockhash")? - .to_string(), - ) + task::block_in_place(|| { + log::info!("GET /blockhash"); + Ok( + index + .block_hash(None)? + .ok_or_not_found(|| "blockhash")? + .to_string(), + ) + }) } async fn block_hash_json(Extension(index): Extension>) -> ServerResult> { - log::info!("GET /r/blockhash"); - Ok(Json( - index - .block_hash(None)? - .ok_or_not_found(|| "blockhash")? - .to_string(), - )) + task::block_in_place(|| { + log::info!("GET /r/blockhash"); + 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 { - log::info!("GET /blockhash/{height}"); - Ok( - index - .block_hash(Some(height))? - .ok_or_not_found(|| "blockhash")? - .to_string(), - ) + task::block_in_place(|| { + log::info!("GET /blockhash/{height}"); + 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> { - log::info!("GET /r/blockhash/{height}"); - Ok(Json( - index - .block_hash(Some(height))? - .ok_or_not_found(|| "blockhash")? - .to_string(), - )) + task::block_in_place(|| { + log::info!("GET /r/blockhash/{height}"); + Ok(Json( + index + .block_hash(Some(height))? + .ok_or_not_found(|| "blockhash")? + .to_string(), + )) + }) } async fn block_time(Extension(index): Extension>) -> ServerResult { - log::info!("GET /blocktime"); - Ok( - index - .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)? - .unix_timestamp() - .to_string(), - ) + task::block_in_place(|| { + log::info!("GET /blocktime"); + Ok( + index + .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)? + .unix_timestamp() + .to_string(), + ) + }) } async fn input( @@ -1338,26 +1410,28 @@ impl Server { Extension(index): Extension>, Path(path): Path<(u32, usize, usize)>, ) -> ServerResult> { - log::info!("GET /input/{}/{}/{}", path.0, path.1, path.2); - let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2); + task::block_in_place(|| { + log::info!("GET /input/{}/{}/{}", path.0, path.1, path.2); + 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 { @@ -1377,26 +1451,28 @@ impl Server { Path(inscription_id): Path, accept_encoding: AcceptEncoding, ) -> ServerResult { - log::info!("GET /content/{inscription_id}"); - if config.is_hidden(inscription_id) { - return Ok(PreviewUnknownHtml.into_response()); - } + task::block_in_place(|| { + log::info!("GET /content/{inscription_id}"); + 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( @@ -1476,95 +1552,97 @@ impl Server { Path(inscription_id): Path, accept_encoding: AcceptEncoding, ) -> ServerResult { - log::info!("GET /preview/{inscription_id}"); - if config.is_hidden(inscription_id) { - return Ok(PreviewUnknownHtml.into_response()); - } + task::block_in_place(|| { + log::info!("GET /preview/{inscription_id}"); + 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( @@ -1573,64 +1651,70 @@ impl Server { Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - match query { - InscriptionQuery::Id(id) => log::info!("GET /inscription/{id}"), - InscriptionQuery::Number(inscription_number) => log::info!("GET /inscription/{inscription_number}"), - }; - - 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(|| { + match query { + InscriptionQuery::Id(id) => log::info!("GET /inscription/{id}"), + InscriptionQuery::Number(inscription_number) => log::info!("GET /inscription/{inscription_number}"), + }; + 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() }) } @@ -1646,21 +1730,24 @@ 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(|| { + log::info!("GET /collections/{page_index}"); + 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( @@ -1681,31 +1768,33 @@ impl Server { Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, ) -> ServerResult { - log::info!("GET /children/{parent}/{page}"); - let entry = index - .get_inscription_entry(parent)? - .ok_or_not_found(|| format!("inscription {parent}"))?; + task::block_in_place(|| { + log::info!("GET /children/{parent}/{page}"); + 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( @@ -1719,15 +1808,18 @@ 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(|| { + log::info!("GET /r/children/{parent}/{page}"); + 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( @@ -1735,7 +1827,6 @@ impl Server { Extension(index): Extension>, accept_json: AcceptJson, ) -> ServerResult { - log::info!("GET /inscriptions"); Self::inscriptions_paginated( Extension(server_config), Extension(index), @@ -1748,31 +1839,33 @@ 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 { - log::info!("GET /inscriptions/{page_index}"); - let (inscriptions, more_inscriptions) = index.get_inscriptions_paginated(100, page_index)?; + task::block_in_place(|| { + log::info!("GET /inscriptions/{page_index}"); + 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() }) } @@ -1782,7 +1875,6 @@ impl Server { Path(block_height): Path, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { - log::info!("GET /inscriptions/block/{block_height}"); Self::inscriptions_in_block_paginated( Extension(server_config), Extension(index), @@ -1795,42 +1887,47 @@ 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 { - log::info!("GET /inscriptions/block/{block_height}/{page_index}"); - let page_size = 100; + task::block_in_place(|| { + log::info!("GET /inscriptions/block/{block_height}/{page_index}"); + 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() }) } @@ -1840,7 +1937,7 @@ impl Server { Path(start): Path, ) -> ServerResult { log::info!("GET /inscriptions_json/{start}"); - Self::inscriptions_json(server_config.chain, index, start, start + 1).await + Self::inscriptions_json_inner(server_config.chain, index, start, start + 1).await } async fn inscriptions_json_start_end( @@ -1849,170 +1946,169 @@ impl Server { Path(path): Path<(i32, i32)>, ) -> ServerResult { log::info!("GET /inscriptions_json/{}/{}", path.0, path.1); - Self::inscriptions_json(server_config.chain, index, path.0, path.1).await + Self::inscriptions_json_inner(server_config.chain, index, path.0, path.1).await } - async fn inscriptions_json( + async fn inscriptions_json_inner( chain: Chain, index: Arc, start: i32, end: i32, ) -> ServerResult { - const MAX_JSON_INSCRIPTIONS: i32 = 1000; + task::block_in_place(|| { + const MAX_JSON_INSCRIPTIONS: i32 = 1000; + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + if end - start > MAX_JSON_INSCRIPTIONS { + return Err(ServerError::BadRequest(format!( + "range length > {MAX_JSON_INSCRIPTIONS}" + ))); + } - match start.cmp(&end) { - Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), - Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), - Ordering::Less => { - if end - start > MAX_JSON_INSCRIPTIONS { - return Err(ServerError::BadRequest(format!( - "range length > {MAX_JSON_INSCRIPTIONS}" - ))); - } + let mut ret = Vec::new(); - let mut ret = Vec::new(); - - for i in start..end { - sleep(Duration::from_millis(0)).await; - match index.get_inscription_id_by_inscription_number(i) { - Err(_) => return Err(ServerError::BadRequest(format!("no inscription {i}"))), - Ok(inscription_id) => match inscription_id { - Some(inscription_id) => { - let entry = index - .get_inscription_entry(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - - let tx = index.get_transaction(inscription_id.txid)?.unwrap(); - let inscription = index - .get_inscription_by_id(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - - let satpoint = index - .get_inscription_satpoint_by_id(inscription_id)? - .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - - let output = if satpoint.outpoint.txid == unbound_outpoint().txid { - None - } else { - Some( - if satpoint.outpoint.txid == inscription_id.txid { - tx - } else { - index - .get_transaction(satpoint.outpoint.txid)? + for i in start..end { + match index.get_inscription_id_by_inscription_number(i) { + Err(_) => return Err(ServerError::BadRequest(format!("no inscription {i}"))), + Ok(inscription_id) => match inscription_id { + Some(inscription_id) => { + let entry = index + .get_inscription_entry(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let tx = index.get_transaction(inscription_id.txid)?.unwrap(); + let inscription = index + .get_inscription_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let satpoint = index + .get_inscription_satpoint_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let output = if satpoint.outpoint.txid == unbound_outpoint().txid { + None + } else { + Some( + if satpoint.outpoint.txid == inscription_id.txid { + tx + } else { + index + .get_transaction(satpoint.outpoint.txid)? + .ok_or_not_found(|| { + format!("inscription {inscription_id} current transaction") + })? + } + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) .ok_or_not_found(|| { - format!("inscription {inscription_id} current transaction") - })? + format!("inscription {inscription_id} current transaction output") + })?, + ) + }; + + let mut address = None; + if let Some(output) = &output { + if let Ok(a) = chain.address_from_script(&output.script_pubkey) { + address = Some(a.to_string()); } - .output - .into_iter() - .nth(satpoint.outpoint.vout.try_into().unwrap()) - .ok_or_not_found(|| { - format!("inscription {inscription_id} current transaction output") - })?, - ) - }; - - let mut address = None; - if let Some(output) = &output { - if let Ok(a) = chain.address_from_script(&output.script_pubkey) { - address = Some(a.to_string()); } - } - let sequence_number = entry.sequence_number; - - let sat = entry.sat.map(|s| SatoshiJson { - number: s.n(), - decimal: s.decimal().to_string(), - degree: s.degree().to_string(), - percentile: s.percentile().to_string(), - name: s.name(), - cycle: s.cycle(), - epoch: s.epoch().0, - period: s.period(), - block: s.height().0, - offset: s.third(), - rarity: s.rarity(), - // timestamp: index.block_time(s.height())?.unix_timestamp(), - }); - - let content_type = inscription.content_type(); - let unbound_suffix = if satpoint.outpoint == unbound_outpoint() { - " (unbound)" - } else { - "" - }; - - let parent = match entry.parent { - Some(parent) => index.get_inscription_id_by_sequence_number(parent)?, - None => None, - }; - - ret.push(MyInscriptionJson { - number: i, - sequence_number, - id: inscription_id, - parent, - address, - output_value: if output.is_some() { - Some(output.unwrap().value) - } else { - None - }, - sat, - content_length: inscription.content_length(), - content_type: if content_type.is_some() { - content_type.unwrap().to_string() + let sequence_number = entry.sequence_number; + + let sat = entry.sat.map(|s| SatoshiJson { + number: s.n(), + decimal: s.decimal().to_string(), + degree: s.degree().to_string(), + percentile: s.percentile().to_string(), + name: s.name(), + cycle: s.cycle(), + epoch: s.epoch().0, + period: s.period(), + block: s.height().0, + offset: s.third(), + rarity: s.rarity(), + // timestamp: index.block_time(s.height())?.unix_timestamp(), + }); + + let content_type = inscription.content_type(); + let unbound_suffix = if satpoint.outpoint == unbound_outpoint() { + " (unbound)" } else { - "".to_string() - }, - timestamp: entry.timestamp, - genesis_height: entry.height, - genesis_fee: entry.fee, - genesis_transaction: inscription_id.txid, - location: satpoint.to_string() + unbound_suffix, - output: satpoint.outpoint.to_string() + unbound_suffix, - offset: satpoint.offset, - children: index.get_children_by_sequence_number(sequence_number)?, - }); - } - None => return Err(ServerError::BadRequest(format!("no inscription {i}"))), - }, + "" + }; + + let parent = match entry.parent { + Some(parent) => index.get_inscription_id_by_sequence_number(parent)?, + None => None, + }; + + ret.push(MyInscriptionJson { + number: i, + sequence_number, + id: inscription_id, + parent, + address, + output_value: if output.is_some() { + Some(output.unwrap().value) + } else { + None + }, + sat, + delegate: inscription.delegate(), + content_length: inscription.content_length(), + content_type: content_type.map(|c| c.to_string()), + timestamp: entry.timestamp, + genesis_height: entry.height, + genesis_fee: entry.fee, + genesis_transaction: inscription_id.txid, + location: satpoint.to_string() + unbound_suffix, + output: satpoint.outpoint.to_string() + unbound_suffix, + offset: satpoint.offset, + children: index.get_children_by_sequence_number(sequence_number)?, + }); + } + None => return Err(ServerError::BadRequest(format!("no inscription {i}"))), + }, + } } - } - Ok(serde_json::to_string_pretty(&ret).ok().unwrap()) + Ok(serde_json::to_string_pretty(&ret).ok().unwrap()) + } } - } + }) } async fn inscriptions_sequence_numbers( Extension(index): Extension>, Path(path): Path<(i32, i32)>, ) -> ServerResult { - log::info!("GET /inscriptions_sequence_numbers/{}/{}", path.0, path.1); - - let start = path.0; - let end = path.1; - - match start.cmp(&end) { - Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), - Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), - Ordering::Less => { - let mut ret = String::new(); - - for i in start..end { - sleep(Duration::from_millis(0)).await; - match index.get_sequence_number_by_inscription_number(i) { - Err(_) => return Err(ServerError::BadRequest(format!("no inscription {i}"))), - Ok(sequence_number) => ret += format!("{i},{sequence_number}\n").as_str(), + task::block_in_place(|| { + log::info!("GET /inscriptions_sequence_numbers/{}/{}", path.0, path.1); + + let start = path.0; + let end = path.1; + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + let mut ret = String::new(); + + for i in start..end { + match index.get_sequence_number_by_inscription_number(i) { + Err(_) => return Err(ServerError::BadRequest(format!("no inscription {i}"))), + Ok(sequence_number) => ret += format!("{i},{sequence_number}\n").as_str(), + } } - } - Ok(ret) + Ok(ret) + } } - } + }) } async fn sat_inscriptions( @@ -2026,30 +2122,36 @@ 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(|| { + log::info!("GET /r/sat/{sat}/{page}"); + 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(|| { + log::info!("GET /r/sat/{sat}/at/{inscription_index}"); + 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( @@ -3103,6 +3205,8 @@ mod tests { StatusCode::OK, ".*

Status

+
chain
+
mainnet
height
0
inscriptions
@@ -4910,6 +5014,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/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 e702c1f520..85a203df98 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -105,10 +105,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 456c3881dd..03c64de679 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -225,7 +225,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 {