commit cd531a2218e1bb159b53353d85bcb601685cf16f Author: viraladmin <00purple@gmail.com> Date: Sun Aug 2 15:08:45 2026 -0600 Initial Contractless Web3 wallet releases diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..286b1e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +core/target/ +extension/node_modules/ +extension/web-ext-artifacts/ +tests/storage/dual-signatures/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..715c1b6 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# Contractless Web3 Wallet + +This repository contains the Contractless browser wallet, its browser-safe +cryptography and transaction codecs, and the JavaScript SDK used by websites +to communicate with the wallet. + +## Repository Structure + +```text +web3_wallet/ +|-- core/ Rust and WebAssembly wallet cryptography and transaction codecs +|-- extension/ Browser extension source and distributable wallet interface +|-- sdk/ JavaScript library for website and application integration +|-- tests/ Integration tests and complete working SDK usage examples +`-- mockups/ Design references used while developing the wallet interface +``` + +The components remain separated internally because they have different +responsibilities: + +- `core` handles deterministic hashing, keys, wallet encryption, transaction + inspection, encoding, signing, and message proofs. +- `extension` owns wallet storage, approvals, application sessions, API + communication, and the browser interface. +- `sdk` gives websites a stable interface for detecting, connecting to, and + requesting actions from the installed wallet. +- `tests` verifies wallet integration and provides complete working example + code for every supported SDK operation. + +The SDK can be published independently later without separating the wallet's +source today. + +## Build the Browser Extensions + +Install Rust, `wasm-pack`, Node.js, and pnpm. From `extension`: + +```text +pnpm install +pnpm run build +``` + +The build compiles separate testnet and mainnet versions of `core` to +`extension/src/wasm/testnet` and `extension/src/wasm/mainnet`. Both cores are +bundled into each browser package so the wallet can enforce the selected +network's address format and transaction codecs locally. Vite then creates +separate unpacked packages: + +```text +extension/dist/chrome/ +extension/dist/firefox/ +``` + +Build a single browser target with: + +```text +pnpm run build:chrome +pnpm run build:firefox +``` + +The wallet stores testnet and mainnet API URLs separately. Testnet defaults to +`https://api.contractless.dev`; mainnet remains unconfigured until a mainnet +API is available. Wallets and active website sessions are isolated by network, +and changing networks locks the active wallet. + +See `extension/README.md` for temporary installation, Firefox validation, +packaging, and Mozilla source-submission instructions. + +## Development Checks + +Run the Rust tests from `core`: + +```text +cargo test +``` + +Run the TypeScript check from `extension`: + +```text +pnpm run check +``` + +## Website Integration + +Websites should use `sdk/contractless.js` instead of communicating with the +injected browser provider directly. See `sdk/README.md` for connection, +session, message-signing, and transaction-signing examples. + +Private keys remain inside the extension. Websites receive only approved +addresses, signatures, signed transaction bytes, and session-scoped access +keys. + +## Integration Test + +The `tests/tests.html` page verifies that a website can detect the extension, +request a connection, retrieve the authorized wallet address, and disconnect +the temporary application session. It also contains editable, copyable, +end-to-end transaction examples for transfers, tokens, NFTs/RWAs, marketing, +burning, and vanity addresses. Successful requests display the +returned transaction ID, complete signed bytes, byte count, and API broadcast +reply. + +The tests are also the complete example-code documentation for using the SDK. +Each available wallet action includes working source that developers can view, +copy, and adapt directly for their own applications. This covers wallet +detection, connection and session handling, address and balance requests, +message signing, single-signature transactions, data transactions, and the +complete dual-signature swap and loan process. + +The dual-signature examples use the included PHP JSON storage endpoint. Serve +the repository from its root directory with PHP: + +```text +php -S 127.0.0.1:8080 -t . +``` + +Then open: + +```text +http://127.0.0.1:8080/tests/tests.html +``` + +PHP creates `tests/storage/dual-signatures/.json` when the +first wallet signs. The second wallet loads and updates that record, and the +broadcast example loads both signatures from it. The web-server user must be +able to write to `tests/storage`. JSON is used only to keep the example easy +to follow; applications can use any backend language or database. + +The unpacked browser extension must be installed and the wallet must be +unlocked before approving the connection request. Opening the page through a +`file://` URL is intentionally unsupported because it has no trustworthy +website origin. diff --git a/core/Cargo.lock b/core/Cargo.lock new file mode 100644 index 0000000..b6a5298 --- /dev/null +++ b/core/Cargo.lock @@ -0,0 +1,1066 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "contractless-browser-core" +version = "0.1.0" +dependencies = [ + "aes", + "aes-gcm", + "base64", + "cbc", + "falcon-rs", + "fn-dsa", + "getrandom", + "hex", + "hmac", + "image", + "pbkdf2", + "rand", + "ripemd", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sha2", + "skein", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-test", + "zeroize", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "falcon-rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f667f04b165f849d6b7cba41b58b8b092556b253051e30062b1233aed7489969" +dependencies = [ + "getrandom", + "libm", + "zeroize", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fn-dsa" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb39d94be3f4538ccfdf23dc69a89904c9b802331b1975538fcc251973593f9" +dependencies = [ + "fn-dsa-comm", + "fn-dsa-kgen", + "fn-dsa-sign", + "fn-dsa-vrfy", +] + +[[package]] +name = "fn-dsa-comm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28104bba227c79bff8ffe34dff7c5a5631fd790dfd4d4f0a84b01952ef7c6f46" +dependencies = [ + "cpufeatures", + "rand_core", +] + +[[package]] +name = "fn-dsa-kgen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a04fe82799cc1550d32778b415648d1962a7b4b4860ff5f2e6ca32494fc9aa" +dependencies = [ + "fn-dsa-comm", + "zeroize", +] + +[[package]] +name = "fn-dsa-sign" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffb7d9c94e56de7e64019849b3b6110b1495690da2e9e1decef8f1744b3bab4" +dependencies = [ + "fn-dsa-comm", + "zeroize", +] + +[[package]] +name = "fn-dsa-vrfy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc96b904b213160c8804cc2642ccd15cb968318a64648fc5ca3edab9153cda2b" +dependencies = [ + "fn-dsa-comm", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ripemd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd4211456b4172d7e44261920c25acf07367c4f04bb5f5d54fc21b090d9b159" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "skein" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a90a220ab98dbbfeabeae7c558a79f37839c10b9ef55c77082a741a463cab7" +dependencies = [ + "digest 0.10.7", + "threefish", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "threefish" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a693d0c8cf16973fac5a93fbe47b8c6452e7097d4fcac49f3d7a18e39c76e62e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..7999f3d --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "contractless-browser-core" +version = "0.1.0" +edition = "2021" +description = "Browser-compatible cryptography and transaction codecs for Contractless" +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["testnet"] +mainnet = [] +testnet = [] + +[dependencies] +aes = "0.8" +aes-gcm = "0.10" +base64 = "0.22" +cbc = { version = "0.1", features = ["alloc", "block-padding"] } +falcon-rs = "0.2.4" +fn-dsa = "0.3.0" +getrandom = { version = "0.2", features = ["js"] } +hex = "0.4" +hmac = "0.12" +image = { version = "0.23", default-features = false, features = ["png"] } +pbkdf2 = "0.12" +rand = "0.8" +ripemd = { version = "0.2.0-rc.0" } +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +serde_json = { version = "1", features = ["arbitrary_precision", "preserve_order"] } +sha2 = "0.10" +skein = "0.1.0" +thiserror = "2" +wasm-bindgen = "0.2" +zeroize = { version = "1", features = ["zeroize_derive"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..5fa26d6 --- /dev/null +++ b/core/README.md @@ -0,0 +1,84 @@ +# Contractless Browser Core + +The `core` directory contains the deterministic cryptography and transaction +codec layer for the Contractless Web3 wallet. Its Rust crate and generated +WASM package retain the name `contractless-browser-core`. + +The crate is intentionally independent from the Contractless node runtime. It +does not connect to RPC servers, broadcast transactions, access PostgreSQL or +Sled, run Tokio tasks, or read files from the operating system. + +The browser wallet is responsible for: + +- obtaining wallet files through browser file APIs; +- keeping decrypted private keys inside the extension; +- presenting transaction details for user approval; +- calling this package to hash, sign, verify, and encode transactions; and +- sending signed transaction bytes to a configured Contractless PHP API. + +The initial API exposes: + +- Skein-128, Skein-256, and Skein-512 hashing; +- FN-DSA signing and verification; +- public-key to short-address derivation; and +- short-address byte encoding and decoding. +- signed transfer creation and fixed 750-byte transfer decoding. +- strict inspection and signing for every externally submitted transaction type; +- signer-role checks against the wallet public key; +- staged signing for swaps and loans; and +- exact fixed-width wire encoding for completed transactions. +- domain-separated website message signing and verification. +- browser-native wallet generation and AES-256-GCM encrypted storage; +- direct import from existing Contractless private-key images; +- direct import from hexadecimal FN-DSA private keys; +- export compatibility with existing Contractless wallet images; and +- wallet-registration payload creation. + +Browser transfer inputs use decimal strings for `value` and `txfee`. This +prevents JavaScript's numeric precision limit from changing a signed `u64` +value. + +Fixed transaction codecs will be added individually and tested against the +native Contractless implementation before they are used by the wallet. + +## Wallet storage + +New browser wallets encrypt their FN-DSA private key with AES-256-GCM. The key +is derived from the wallet password with PBKDF2-SHA256 and a unique random +salt. Only the encrypted wallet JSON is stored persistently by the extension. + +Existing Contractless wallet files can be imported with their encryption key. +Browser wallets can also be exported back to the existing wallet image format +for use with the CLI or desktop wallet. The encrypted browser-wallet backup and +its password are both required for recovery. + +## Transaction request flow + +Websites submit unsigned transaction JSON to `inspect_transaction_request`. +The core rejects unsupported types, missing fields, extra fields, invalid +addresses, incorrect fixed-width values, and malformed numeric values. The +returned review object is the canonical data the wallet must show to the user. + +After approval, `sign_transaction_request` receives the wallet private and +public keys. The public key determines the wallet address, and that address +must match the signer role stored in the transaction. + +For swaps and loans, signer two must also provide signer one's registered +public key. The core verifies the first signature before adding the second. +Only a complete transaction receives `bytes_hex` suitable for broadcasting. + +Genesis and mining reward transactions are intentionally excluded from the +signing registry because users and websites must never create them. + +## Website message proofs + +`sign_message_request` signs a short-lived website proof without creating a +transaction. The signed payload contains the requesting origin, wallet +address, message, random nonce, issue time, and expiration time. + +Message proofs use a Contractless-specific domain prefix. Their signatures +cannot be reused as transaction signatures. Proofs are limited to 15 minutes, +and production origins must use HTTPS. + +`verify_message_proof` verifies the public key, derived wallet address, +signature, expected origin, expected nonce, and current validity window. diff --git a/core/src/address.rs b/core/src/address.rs new file mode 100644 index 0000000..12a9d65 --- /dev/null +++ b/core/src/address.rs @@ -0,0 +1,131 @@ +use ripemd::{Digest, Ripemd160}; + +use crate::crypto::skein_256; +use crate::error::BrowserCoreError; +use crate::{PUBLIC_KEY_LENGTH, REQUIRED_PUBLIC_KEY_HASH_BYTE}; + +const SHORT_ADDRESS_HASH_BYTES: usize = 20; +const SHORT_ADDRESS_BYTES: usize = 22; + +pub fn network_byte() -> u8 { + if cfg!(feature = "mainnet") { + 0b0000_0001 + } else { + 0b0000_0010 + } +} + +pub fn network_suffix() -> &'static str { + if cfg!(feature = "mainnet") { + "clc" + } else { + "cltc" + } +} + +pub fn valid_public_key(public_key: &[u8]) -> bool { + public_key.len() == PUBLIC_KEY_LENGTH + && public_key.first() == Some(&9) + && skein_256(public_key) + .first() + .is_some_and(|byte| *byte == REQUIRED_PUBLIC_KEY_HASH_BYTE) +} + +pub fn public_key_to_short_address(public_key: &[u8]) -> Result { + if !valid_public_key(public_key) { + return Err(BrowserCoreError::InvalidPublicKey); + } + let payload = Ripemd160::digest(skein_256(public_key)); + Ok(format!("{}.{}", hex::encode(payload), network_suffix())) +} + +pub fn short_address_to_bytes(address: &str) -> Result, BrowserCoreError> { + let (payload, suffix) = address + .rsplit_once('.') + .ok_or_else(|| BrowserCoreError::InvalidAddress(address.to_string()))?; + if !suffix.eq_ignore_ascii_case(network_suffix()) || payload.len() != 40 { + return Err(BrowserCoreError::InvalidAddress(address.to_string())); + } + let decoded = + hex::decode(payload).map_err(|_| BrowserCoreError::InvalidAddress(address.to_string()))?; + if decoded.len() != SHORT_ADDRESS_HASH_BYTES { + return Err(BrowserCoreError::InvalidAddress(address.to_string())); + } + let mut bytes = Vec::with_capacity(SHORT_ADDRESS_BYTES); + bytes.extend_from_slice(&decoded); + bytes.push(b'.'); + bytes.push(network_byte()); + Ok(bytes) +} + +pub fn bytes_to_short_address(bytes: &[u8]) -> Result { + if bytes.len() != SHORT_ADDRESS_BYTES + || bytes[SHORT_ADDRESS_HASH_BYTES] != b'.' + || bytes[SHORT_ADDRESS_BYTES - 1] != network_byte() + { + return Err(BrowserCoreError::InvalidAddress(hex::encode(bytes))); + } + Ok(format!( + "{}.{}", + hex::encode(&bytes[..SHORT_ADDRESS_HASH_BYTES]), + network_suffix() + )) +} + +pub fn vanity_address_to_bytes(address: &str) -> Result, BrowserCoreError> { + let normalized = normalize_vanity_address(address)?; + let (payload, _) = normalized + .rsplit_once('.') + .ok_or_else(|| BrowserCoreError::InvalidAddress(address.to_string()))?; + let mut bytes = payload.as_bytes().to_vec(); + bytes.push(b'.'); + bytes.push(network_byte()); + Ok(bytes) +} + +pub fn normalize_vanity_address(address: &str) -> Result { + let (payload, suffix) = address + .rsplit_once('.') + .ok_or_else(|| BrowserCoreError::InvalidAddress(address.to_string()))?; + let visible = payload.trim(); + if !suffix.eq_ignore_ascii_case(network_suffix()) + || visible.is_empty() + || visible.len() > SHORT_ADDRESS_HASH_BYTES + || !visible + .chars() + .all(|character| character.is_ascii_alphabetic()) + { + return Err(BrowserCoreError::InvalidAddress(address.to_string())); + } + + Ok(format!( + "{:>width$}.{}", + visible.to_ascii_lowercase(), + network_suffix(), + width = SHORT_ADDRESS_HASH_BYTES + )) +} + +#[cfg(test)] +mod tests { + use super::{network_suffix, normalize_vanity_address, vanity_address_to_bytes}; + + #[test] + fn vanity_address_is_left_padded_and_lowercased() { + let input = format!("BruceBates.{}", network_suffix()); + let normalized = normalize_vanity_address(&input).unwrap(); + + assert_eq!( + normalized, + format!(" brucebates.{}", network_suffix()) + ); + assert_eq!(vanity_address_to_bytes(&input).unwrap().len(), 22); + } + + #[test] + fn vanity_address_rejects_non_letters() { + let input = format!("bruce123.{}", network_suffix()); + + assert!(normalize_vanity_address(&input).is_err()); + } +} diff --git a/core/src/crypto.rs b/core/src/crypto.rs new file mode 100644 index 0000000..8c1545c --- /dev/null +++ b/core/src/crypto.rs @@ -0,0 +1,54 @@ +use fn_dsa::{ + SigningKey, SigningKeyStandard, VerifyingKey, VerifyingKeyStandard, DOMAIN_NONE, HASH_ID_RAW, +}; +use rand::thread_rng; +use skein::digest::consts::{U32, U64}; +use skein::digest::{Digest, Output}; +use skein::{Skein256, Skein512}; + +use crate::error::BrowserCoreError; +use crate::SIGNATURE_LENGTH; + +pub fn skein_128(data: &[u8]) -> Vec { + let mut hasher = Skein256::new(); + hasher.update(data); + let result: Output = hasher.finalize(); + result.iter().step_by(2).copied().collect() +} + +pub fn skein_256(data: &[u8]) -> Vec { + let mut hasher = Skein256::::new(); + hasher.update(data); + hasher.finalize().to_vec() +} + +pub fn skein_512(data: &[u8]) -> Vec { + let mut hasher = Skein512::::new(); + hasher.update(data); + hasher.finalize().to_vec() +} + +pub fn sign(message: &[u8], private_key: &[u8]) -> Result, BrowserCoreError> { + let mut signing_key = + SigningKeyStandard::decode(private_key).ok_or(BrowserCoreError::InvalidPrivateKey)?; + let mut signature = vec![0u8; SIGNATURE_LENGTH]; + let mut rng = thread_rng(); + signing_key.sign( + &mut rng, + &DOMAIN_NONE, + &HASH_ID_RAW, + message, + &mut signature, + ); + Ok(signature) +} + +pub fn verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> bool { + if signature.len() != SIGNATURE_LENGTH { + return false; + } + let Some(verifying_key) = VerifyingKeyStandard::decode(public_key) else { + return false; + }; + verifying_key.verify(signature, &DOMAIN_NONE, &HASH_ID_RAW, message) +} diff --git a/core/src/error.rs b/core/src/error.rs new file mode 100644 index 0000000..7f06f1f --- /dev/null +++ b/core/src/error.rs @@ -0,0 +1,30 @@ +use thiserror::Error; +use wasm_bindgen::JsValue; + +#[derive(Debug, Error)] +pub enum BrowserCoreError { + #[error("invalid hex value: {0}")] + InvalidHex(String), + #[error("invalid FN-DSA private key")] + InvalidPrivateKey, + #[error("invalid FN-DSA public key")] + InvalidPublicKey, + #[error("invalid FN-DSA signature")] + InvalidSignature, + #[error("invalid Contractless address: {0}")] + InvalidAddress(String), + #[error("invalid transfer transaction: {0}")] + InvalidTransfer(String), + #[error("JSON serialization failed: {0}")] + Json(String), + #[error("unsupported transaction type: {0}")] + UnsupportedTransaction(u8), + #[error("invalid transaction request: {0}")] + InvalidRequest(String), +} + +impl From for JsValue { + fn from(error: BrowserCoreError) -> Self { + JsValue::from_str(&error.to_string()) + } +} diff --git a/core/src/legacy_wallet.rs b/core/src/legacy_wallet.rs new file mode 100644 index 0000000..9073dbe --- /dev/null +++ b/core/src/legacy_wallet.rs @@ -0,0 +1,264 @@ +use aes::Aes128; +use base64::{engine::general_purpose::STANDARD, Engine}; +use cbc::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use hmac::{Hmac, Mac}; +use image::{png::PngEncoder, ColorType, ImageBuffer, Rgba}; +use sha2::Sha256; + +use crate::error::BrowserCoreError; + +type Aes128CbcEnc = cbc::Encryptor; +type Aes128CbcDec = cbc::Decryptor; +type HmacSha256 = Hmac; + +const WIDTH: u32 = 350; +const HEIGHT: u32 = 350; +const ANCHORS: [u32; 11] = [0, 35, 70, 105, 140, 175, 210, 245, 280, 315, 349]; +const HEADER: usize = 4; +const CAPACITY: usize = ANCHORS.len() * WIDTH as usize - HEADER; + +const COLORS: &[(char, (u8, u8, u8))] = &[ + ('a', (204, 180, 194)), + ('A', (255, 255, 255)), + ('b', (197, 186, 201)), + ('B', (221, 206, 212)), + ('c', (181, 185, 193)), + ('C', (184, 201, 223)), + ('d', (224, 218, 192)), + ('D', (185, 191, 195)), + ('e', (181, 197, 198)), + ('E', (193, 206, 255)), + ('f', (252, 193, 211)), + ('F', (183, 192, 229)), + ('g', (180, 191, 192)), + ('G', (187, 219, 189)), + ('h', (195, 187, 234)), + ('H', (182, 216, 189)), + ('i', (197, 183, 248)), + ('I', (200, 182, 204)), + ('j', (255, 235, 196)), + ('J', (194, 186, 228)), + ('k', (199, 238, 239)), + ('K', (208, 247, 234)), + ('l', (244, 214, 189)), + ('L', (187, 243, 239)), + ('m', (188, 231, 238)), + ('M', (187, 197, 227)), + ('n', (186, 240, 191)), + ('N', (187, 198, 206)), + ('o', (205, 193, 184)), + ('O', (191, 187, 197)), + ('p', (194, 200, 206)), + ('P', (195, 183, 229)), + ('q', (182, 219, 196)), + ('Q', (238, 216, 184)), + ('r', (199, 181, 208)), + ('R', (239, 231, 198)), + ('s', (189, 188, 230)), + ('S', (242, 192, 230)), + ('t', (199, 199, 199)), + ('T', (188, 190, 230)), + ('u', (230, 180, 253)), + ('U', (241, 247, 247)), + ('v', (242, 190, 199)), + ('V', (230, 247, 234)), + ('w', (197, 186, 249)), + ('W', (194, 247, 249)), + ('x', (242, 182, 246)), + ('X', (188, 222, 193)), + ('y', (188, 194, 183)), + ('Y', (197, 195, 197)), + ('z', (187, 249, 240)), + ('Z', (233, 231, 242)), + ('0', (195, 184, 218)), + ('1', (232, 180, 196)), + ('2', (191, 193, 196)), + ('3', (185, 186, 186)), + ('4', (191, 247, 180)), + ('5', (187, 199, 248)), + ('6', (248, 198, 184)), + ('7', (243, 195, 184)), + ('8', (232, 192, 208)), + ('9', (239, 197, 183)), + ('/', (199, 187, 241)), + ('+', (195, 216, 223)), + ('=', (193, 211, 184)), +]; + +pub fn decrypt_private_key(image_base64: &str, password: &str) -> Result { + let encrypted = decode_image(image_base64)?; + let packed = STANDARD.decode(encrypted).map_err(|_| { + BrowserCoreError::InvalidRequest("legacy wallet encryption is invalid".into()) + })?; + if packed.len() < 49 { + return Err(BrowserCoreError::InvalidRequest( + "legacy wallet payload is too short".into(), + )); + } + let key = normalized_key(password); + let (iv, remainder) = packed.split_at(16); + let (expected_hmac, ciphertext) = remainder.split_at(32); + let mut mac = HmacSha256::new_from_slice(&key).unwrap(); + mac.update(ciphertext); + mac.verify_slice(expected_hmac).map_err(|_| { + BrowserCoreError::InvalidRequest("wallet password or integrity check failed".into()) + })?; + let plaintext = Aes128CbcDec::new_from_slices(&key, iv) + .unwrap() + .decrypt_padded_vec_mut::(ciphertext) + .map_err(|_| BrowserCoreError::InvalidRequest("wallet decryption failed".into()))?; + String::from_utf8(plaintext) + .map_err(|_| BrowserCoreError::InvalidRequest("wallet private key is not UTF-8".into())) +} + +pub fn encrypt_private_key(private_key: &str, password: &str) -> Result { + if private_key.len() < 10 { + return Err(BrowserCoreError::InvalidPrivateKey); + } + let key = normalized_key(password); + let iv_text = STANDARD.encode(&private_key.as_bytes()[..10]); + let iv = iv_text.as_bytes(); + let ciphertext = Aes128CbcEnc::new_from_slices(&key, iv) + .unwrap() + .encrypt_padded_vec_mut::(private_key.as_bytes()); + let mut mac = HmacSha256::new_from_slice(&key).unwrap(); + mac.update(&ciphertext); + let mut packed = Vec::with_capacity(48 + ciphertext.len()); + packed.extend_from_slice(iv); + packed.extend_from_slice(&mac.finalize().into_bytes()); + packed.extend_from_slice(&ciphertext); + encode_image(&STANDARD.encode(packed)) +} + +fn normalized_key(password: &str) -> [u8; 16] { + let mut key = [0u8; 16]; + let bytes = password.as_bytes(); + key[..bytes.len().min(16)].copy_from_slice(&bytes[..bytes.len().min(16)]); + key +} + +fn vivid((r, g, b): (u8, u8, u8)) -> (u8, u8, u8) { + let average = (r as f32 + g as f32 + b as f32) / 3.0; + let apply = |value: u8| { + let saturated = average + ((value as f32 - average) * 2.35); + (((saturated - 128.0) * 1.12) + 128.0 - 18.0) + .clamp(0.0, 255.0) + .round() as u8 + }; + (apply(r), apply(g), apply(b)) +} + +fn color(character: char) -> Option<(u8, u8, u8)> { + COLORS + .iter() + .find(|(candidate, _)| *candidate == character) + .map(|(_, value)| vivid(*value)) +} + +fn character(rgb: (u8, u8, u8)) -> Option { + COLORS + .iter() + .find(|(_, value)| vivid(*value) == rgb) + .map(|(character, _)| *character) +} + +fn decode_image(image_base64: &str) -> Result { + let bytes = STANDARD + .decode(image_base64) + .map_err(|_| BrowserCoreError::InvalidRequest("wallet image is not valid base64".into()))?; + let image = image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) + .map_err(|_| BrowserCoreError::InvalidRequest("wallet image is not a PNG".into()))? + .to_rgba8(); + if image.width() != WIDTH || image.height() != HEIGHT { + return Err(BrowserCoreError::InvalidRequest( + "wallet image dimensions are invalid".into(), + )); + } + let mut text = String::with_capacity(ANCHORS.len() * WIDTH as usize); + for row in ANCHORS { + for x in 0..WIDTH { + let pixel = image.get_pixel(x, row).0; + text.push(character((pixel[0], pixel[1], pixel[2])).ok_or_else(|| { + BrowserCoreError::InvalidRequest( + "wallet image contains an unknown anchor color".into(), + ) + })?); + } + } + let length = text + .get(..HEADER) + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| { + BrowserCoreError::InvalidRequest("wallet image length header is invalid".into()) + })?; + text.get(HEADER..HEADER + length) + .map(str::to_owned) + .ok_or_else(|| { + BrowserCoreError::InvalidRequest("wallet image payload is incomplete".into()) + }) +} + +fn encode_image(ciphertext: &str) -> Result { + if ciphertext.len() > CAPACITY { + return Err(BrowserCoreError::InvalidRequest( + "encrypted wallet exceeds image capacity".into(), + )); + } + let payload = format!("{:04}{}", ciphertext.len(), ciphertext); + let chars: Vec = payload.chars().collect(); + let slot_count = ANCHORS.len() * WIDTH as usize; + let slots: Vec = (0..slot_count) + .map(|index| chars[index % chars.len()]) + .collect(); + let mut image: ImageBuffer, Vec> = ImageBuffer::new(WIDTH, HEIGHT); + for x in 0..WIDTH as usize { + for anchor in 0..ANCHORS.len() - 1 { + let upper_y = ANCHORS[anchor]; + let lower_y = ANCHORS[anchor + 1]; + let upper = color(slots[anchor * WIDTH as usize + x]).unwrap(); + let lower = color(slots[(anchor + 1) * WIDTH as usize + x]).unwrap(); + let distance = lower_y - upper_y; + for y in upper_y..=lower_y { + let ratio = (y - upper_y) as f32 / distance as f32; + let blend = |start: u8, end: u8| { + (start as f32 + ((end as f32 - start as f32) * ratio)).round() as u8 + }; + image.put_pixel( + x as u32, + y, + Rgba([ + blend(upper.0, lower.0), + blend(upper.1, lower.1), + blend(upper.2, lower.2), + 255, + ]), + ); + } + } + } + let mut png = Vec::new(); + PngEncoder::new(&mut png) + .encode(image.as_raw(), WIDTH, HEIGHT, ColorType::Rgba8) + .map_err(|_| BrowserCoreError::InvalidRequest("failed to encode wallet image".into()))?; + Ok(STANDARD.encode(png)) +} + +#[cfg(test)] +mod tests { + use super::{decrypt_private_key, encrypt_private_key}; + + #[test] + fn legacy_private_key_image_round_trips() { + let private_key = "ab".repeat(1281); + let encrypted = encrypt_private_key(&private_key, "correct horse battery staple").unwrap(); + let decrypted = decrypt_private_key(&encrypted, "correct horse battery staple").unwrap(); + assert_eq!(decrypted, private_key); + } + + #[test] + fn legacy_private_key_image_rejects_wrong_password() { + let private_key = "cd".repeat(1281); + let encrypted = encrypt_private_key(&private_key, "correct horse battery staple").unwrap(); + assert!(decrypt_private_key(&encrypted, "this password is incorrect").is_err()); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..1c88b0e --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,347 @@ +#[cfg(all(feature = "mainnet", feature = "testnet"))] +compile_error!("contractless-browser-core must be built for exactly one network"); + +#[cfg(not(any(feature = "mainnet", feature = "testnet")))] +compile_error!("contractless-browser-core requires either the mainnet or testnet feature"); + +mod address; +mod crypto; +mod error; +mod legacy_wallet; +mod message; +mod registry; +mod transfer; +mod wallet; + +use wasm_bindgen::prelude::*; + +use error::BrowserCoreError; + +pub const PUBLIC_KEY_LENGTH: usize = 897; +pub const PRIVATE_KEY_LENGTH: usize = 1281; +pub const SIGNATURE_LENGTH: usize = 666; +pub const REQUIRED_PUBLIC_KEY_HASH_BYTE: u8 = 239; + +fn decode_hex(value: &str) -> Result, BrowserCoreError> { + hex::decode(value).map_err(|error| BrowserCoreError::InvalidHex(error.to_string())) +} + +#[wasm_bindgen] +pub fn skein_128_hex(data: &[u8]) -> String { + hex::encode(crypto::skein_128(data)) +} + +#[wasm_bindgen] +pub fn skein_256_hex(data: &[u8]) -> String { + hex::encode(crypto::skein_256(data)) +} + +#[wasm_bindgen] +pub fn skein_512_hex(data: &[u8]) -> String { + hex::encode(crypto::skein_512(data)) +} + +#[wasm_bindgen] +pub fn verify_bytes_hex( + message: &[u8], + signature_hex: &str, + public_key_hex: &str, +) -> Result { + let signature = decode_hex(signature_hex)?; + let public_key = decode_hex(public_key_hex)?; + Ok(crypto::verify(message, &signature, &public_key)) +} + +#[wasm_bindgen] +pub fn verify_hash_hex( + hash_hex: &str, + signature_hex: &str, + public_key_hex: &str, +) -> Result { + let hash = decode_hex(hash_hex)?; + verify_bytes_hex(&hash, signature_hex, public_key_hex) +} + +#[wasm_bindgen] +pub fn public_key_to_short_address(public_key_hex: &str) -> Result { + let public_key = decode_hex(public_key_hex)?; + Ok(address::public_key_to_short_address(&public_key)?) +} + +#[wasm_bindgen] +pub fn short_address_to_bytes(address: &str) -> Result, JsValue> { + Ok(address::short_address_to_bytes(address)?) +} + +#[wasm_bindgen] +pub fn bytes_to_short_address(bytes: &[u8]) -> Result { + Ok(address::bytes_to_short_address(bytes)?) +} + +#[wasm_bindgen] +pub fn decode_transfer(transaction_hex: &str) -> Result { + let bytes = decode_hex(transaction_hex)?; + let transfer = transfer::SignedTransfer::from_bytes(&bytes)?; + serde_json::to_string(&transfer.browser_output()?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn decode_transaction(transaction_hex: &str) -> Result { + let bytes = decode_hex(transaction_hex)?; + if bytes.is_empty() { + return Err(BrowserCoreError::InvalidRequest("transaction bytes were empty".into()).into()); + } + + let decoded = match bytes[0] { + 0 => { + if bytes.len() != 49 { + return Err(BrowserCoreError::InvalidRequest( + "genesis transaction must contain exactly 49 bytes".into(), + ) + .into()); + } + let message = std::str::from_utf8(&bytes[1..]) + .map_err(|_| BrowserCoreError::InvalidRequest("invalid genesis message".into()))? + .trim_end_matches(|character| character == '\0' || character == ' '); + serde_json::json!({ + "txtype": 0, + "transaction_name": "Genesis", + "fields": { + "txtype": 0, + "message": message + }, + "stored_hash": null, + "signature": null, + "signature1": null, + "signature2": null + }) + } + 1 => { + if bytes.len() != 13 { + return Err(BrowserCoreError::InvalidRequest( + "reward transaction must contain exactly 13 bytes".into(), + ) + .into()); + } + let timestamp = u32::from_le_bytes(bytes[1..5].try_into().unwrap()); + let value = u64::from_le_bytes(bytes[5..13].try_into().unwrap()); + serde_json::json!({ + "txtype": 1, + "transaction_name": "Mining Reward", + "fields": { + "txtype": 1, + "timestamp": timestamp, + "value": value.to_string() + }, + "stored_hash": null, + "signature": null, + "signature1": null, + "signature2": null + }) + } + _ => serde_json::to_value(registry::decode_transaction(&bytes)?) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?, + }; + + serde_json::to_string(&decoded) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn inspect_transaction_request(request_json: &str) -> Result { + let request: serde_json::Value = serde_json::from_str(request_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + serde_json::to_string(®istry::inspect(&request)?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn sign_transaction_request( + request_json: &str, + private_key_hex: &str, + public_key_hex: &str, + signer_slot: u8, +) -> Result { + let request: serde_json::Value = serde_json::from_str(request_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let private_key = decode_hex(private_key_hex)?; + let public_key = decode_hex(public_key_hex)?; + serde_json::to_string(®istry::sign_request( + &request, + &private_key, + &public_key, + signer_slot, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn complete_dual_transaction_request( + request_json: &str, + signature1_hex: &str, + public_key1_hex: &str, + signature2_hex: &str, + public_key2_hex: &str, +) -> Result { + let request: serde_json::Value = serde_json::from_str(request_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let public_key1 = decode_hex(public_key1_hex)?; + let public_key2 = decode_hex(public_key2_hex)?; + serde_json::to_string(®istry::complete_dual_request( + &request, + signature1_hex, + &public_key1, + signature2_hex, + &public_key2, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn transaction_type_registry() -> Result { + serde_json::to_string(®istry::entries()) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn generate_browser_wallet(password: &str) -> Result { + serde_json::to_string_pretty(&wallet::generate(password)?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn unlock_browser_wallet(wallet_json: &str, password: &str) -> Result { + let wallet: wallet::BrowserWallet = serde_json::from_str(wallet_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + serde_json::to_string(&wallet::unlock(&wallet, password)?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn import_legacy_wallet( + legacy_wallet_json: &str, + legacy_password: &str, + browser_password: &str, +) -> Result { + let legacy: wallet::LegacySavedWallet = serde_json::from_str(legacy_wallet_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + serde_json::to_string_pretty(&wallet::import_legacy( + &legacy, + legacy_password, + browser_password, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn import_private_key_image( + image_base64: &str, + image_password: &str, + browser_password: &str, +) -> Result { + serde_json::to_string_pretty(&wallet::import_private_key_image( + image_base64, + image_password, + browser_password, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn import_private_key( + private_key_hex: &str, + browser_password: &str, +) -> Result { + serde_json::to_string_pretty(&wallet::import_private_key( + private_key_hex, + browser_password, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn export_legacy_wallet( + browser_wallet_json: &str, + browser_password: &str, + legacy_password: &str, +) -> Result { + let wallet: wallet::BrowserWallet = serde_json::from_str(browser_wallet_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + serde_json::to_string_pretty(&wallet::export_legacy( + &wallet, + browser_password, + legacy_password, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn create_wallet_registration( + browser_wallet_json: &str, + password: &str, +) -> Result { + let wallet: wallet::BrowserWallet = serde_json::from_str(browser_wallet_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + serde_json::to_string(&wallet::registration(&wallet, password)?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn create_wallet_registration_from_keys( + short_address: &str, + public_key_hex: &str, + private_key_hex: &str, +) -> Result { + serde_json::to_string(&wallet::registration_from_keys( + short_address, + public_key_hex, + private_key_hex, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn create_handshake_proof( + public_key_hex: &str, + private_key_hex: &str, +) -> Result { + Ok(wallet::handshake_proof(public_key_hex, private_key_hex)?) +} + +#[wasm_bindgen] +pub fn sign_message_request( + request_json: &str, + private_key_hex: &str, + public_key_hex: &str, +) -> Result { + let request: message::MessageRequest = serde_json::from_str(request_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let private_key = decode_hex(private_key_hex)?; + let public_key = decode_hex(public_key_hex)?; + serde_json::to_string(&message::sign(request, &private_key, &public_key)?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} + +#[wasm_bindgen] +pub fn verify_message_proof( + proof_json: &str, + expected_origin: &str, + expected_nonce: &str, + current_time: &str, +) -> Result { + let proof: message::MessageProof = serde_json::from_str(proof_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let current_time = current_time.parse::().map_err(|_| { + BrowserCoreError::InvalidRequest( + "current_time must be an unsigned decimal timestamp".into(), + ) + })?; + serde_json::to_string(&message::verify( + proof, + expected_origin, + expected_nonce, + current_time, + )?) + .map_err(|error| BrowserCoreError::Json(error.to_string()).into()) +} diff --git a/core/src/message.rs b/core/src/message.rs new file mode 100644 index 0000000..5dd7af3 --- /dev/null +++ b/core/src/message.rs @@ -0,0 +1,263 @@ +use serde::{Deserialize, Serialize}; + +use crate::address; +use crate::crypto; +use crate::error::BrowserCoreError; +use crate::{PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; + +const MESSAGE_DOMAIN: &[u8] = b"CONTRACTLESS_WEB_MESSAGE_V1\0"; +const MAX_MESSAGE_BYTES: usize = 4096; +const MAX_ORIGIN_BYTES: usize = 255; +const MIN_NONCE_BYTES: usize = 16; +const MAX_PROOF_LIFETIME_SECONDS: u64 = 15 * 60; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MessageRequest { + pub origin: String, + pub message: String, + pub nonce: String, + pub issued_at: u64, + pub expires_at: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MessagePayload { + pub version: u8, + pub origin: String, + pub address: String, + pub message: String, + pub nonce: String, + pub issued_at: u64, + pub expires_at: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MessageProof { + pub payload: MessagePayload, + pub public_key: String, + pub hash: String, + pub signature: String, +} + +#[derive(Debug, Serialize)] +pub struct MessageVerification { + pub valid: bool, + pub address: String, + pub origin: String, + pub message: String, + pub nonce: String, + pub issued_at: u64, + pub expires_at: u64, +} + +pub fn sign( + request: MessageRequest, + private_key: &[u8], + public_key: &[u8], +) -> Result { + if private_key.len() != PRIVATE_KEY_LENGTH { + return Err(BrowserCoreError::InvalidPrivateKey); + } + if public_key.len() != PUBLIC_KEY_LENGTH { + return Err(BrowserCoreError::InvalidPublicKey); + } + + validate_request(&request)?; + let payload = MessagePayload { + version: 1, + origin: request.origin, + address: address::public_key_to_short_address(public_key)?, + message: request.message, + nonce: request.nonce.to_ascii_lowercase(), + issued_at: request.issued_at, + expires_at: request.expires_at, + }; + let hash = payload_hash(&payload)?; + let signature = crypto::sign(&hash, private_key)?; + if !crypto::verify(&hash, &signature, public_key) { + return Err(BrowserCoreError::InvalidRequest( + "private key does not match the wallet public key".into(), + )); + } + + Ok(MessageProof { + payload, + public_key: hex::encode(public_key), + hash: hex::encode(hash), + signature: hex::encode(signature), + }) +} + +pub fn verify( + proof: MessageProof, + expected_origin: &str, + expected_nonce: &str, + current_time: u64, +) -> Result { + if proof.payload.version != 1 { + return Err(BrowserCoreError::InvalidRequest( + "unsupported message proof version".into(), + )); + } + validate_request(&MessageRequest { + origin: proof.payload.origin.clone(), + message: proof.payload.message.clone(), + nonce: proof.payload.nonce.clone(), + issued_at: proof.payload.issued_at, + expires_at: proof.payload.expires_at, + })?; + let public_key = + hex::decode(&proof.public_key).map_err(|_| BrowserCoreError::InvalidPublicKey)?; + let derived_address = address::public_key_to_short_address(&public_key)?; + if derived_address != proof.payload.address { + return Err(BrowserCoreError::InvalidRequest( + "message address does not match its public key".into(), + )); + } + if proof.payload.origin != expected_origin { + return Err(BrowserCoreError::InvalidRequest( + "message proof was issued for a different origin".into(), + )); + } + if !proof.payload.nonce.eq_ignore_ascii_case(expected_nonce) { + return Err(BrowserCoreError::InvalidRequest( + "message proof nonce does not match".into(), + )); + } + if current_time < proof.payload.issued_at || current_time > proof.payload.expires_at { + return Err(BrowserCoreError::InvalidRequest( + "message proof is not currently valid".into(), + )); + } + + let hash = payload_hash(&proof.payload)?; + if proof.hash != hex::encode(&hash) { + return Err(BrowserCoreError::InvalidRequest( + "message proof hash does not match its payload".into(), + )); + } + let signature = + hex::decode(&proof.signature).map_err(|_| BrowserCoreError::InvalidSignature)?; + if signature.len() != SIGNATURE_LENGTH || !crypto::verify(&hash, &signature, &public_key) { + return Err(BrowserCoreError::InvalidSignature); + } + + Ok(MessageVerification { + valid: true, + address: proof.payload.address, + origin: proof.payload.origin, + message: proof.payload.message, + nonce: proof.payload.nonce, + issued_at: proof.payload.issued_at, + expires_at: proof.payload.expires_at, + }) +} + +fn validate_request(request: &MessageRequest) -> Result<(), BrowserCoreError> { + let valid_origin = request + .origin + .strip_prefix("https://") + .is_some_and(valid_https_authority) + || request + .origin + .strip_prefix("http://localhost") + .is_some_and(valid_local_port) + || request + .origin + .strip_prefix("http://127.0.0.1") + .is_some_and(valid_local_port); + if request.origin.is_empty() + || request.origin.len() > MAX_ORIGIN_BYTES + || !request.origin.is_ascii() + || request.origin.chars().any(char::is_whitespace) + || !valid_origin + { + return Err(BrowserCoreError::InvalidRequest( + "origin must be HTTPS or a local development origin".into(), + )); + } + if request.message.is_empty() || request.message.as_bytes().len() > MAX_MESSAGE_BYTES { + return Err(BrowserCoreError::InvalidRequest(format!( + "message must contain 1 to {MAX_MESSAGE_BYTES} bytes" + ))); + } + let nonce = hex::decode(&request.nonce) + .map_err(|_| BrowserCoreError::InvalidRequest("nonce must be hexadecimal".into()))?; + if nonce.len() < MIN_NONCE_BYTES { + return Err(BrowserCoreError::InvalidRequest(format!( + "nonce must contain at least {MIN_NONCE_BYTES} random bytes" + ))); + } + if request.expires_at <= request.issued_at { + return Err(BrowserCoreError::InvalidRequest( + "expires_at must be after issued_at".into(), + )); + } + if request.expires_at - request.issued_at > MAX_PROOF_LIFETIME_SECONDS { + return Err(BrowserCoreError::InvalidRequest(format!( + "message proofs may remain valid for at most {MAX_PROOF_LIFETIME_SECONDS} seconds" + ))); + } + Ok(()) +} + +fn valid_https_authority(authority: &str) -> bool { + !authority.is_empty() + && !authority.contains('/') + && !authority.contains('?') + && !authority.contains('#') +} + +fn valid_local_port(suffix: &str) -> bool { + suffix.is_empty() + || suffix.strip_prefix(':').is_some_and(|port| { + !port.is_empty() && port.chars().all(|character| character.is_ascii_digit()) + }) +} + +fn payload_hash(payload: &MessagePayload) -> Result, BrowserCoreError> { + let json = serde_json::to_string(payload) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let mut domain_payload = Vec::with_capacity(MESSAGE_DOMAIN.len() + json.len()); + domain_payload.extend_from_slice(MESSAGE_DOMAIN); + domain_payload.extend_from_slice(json.as_bytes()); + Ok(crypto::skein_256(&domain_payload)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn message_domain_differs_from_transaction_hashing() { + let payload = MessagePayload { + version: 1, + origin: "https://example.com".into(), + address: "11".repeat(20) + ".cltc", + message: "Welcome to XYZ".into(), + nonce: "22".repeat(16), + issued_at: 100, + expires_at: 200, + }; + let json = serde_json::to_string(&payload).unwrap(); + assert_ne!( + payload_hash(&payload).unwrap(), + crypto::skein_256(json.as_bytes()) + ); + } + + #[test] + fn message_requests_reject_long_lived_replay_windows() { + let request = MessageRequest { + origin: "https://example.com".into(), + message: "Welcome to XYZ".into(), + nonce: "22".repeat(16), + issued_at: 100, + expires_at: 100 + MAX_PROOF_LIFETIME_SECONDS + 1, + }; + assert!(validate_request(&request).is_err()); + } +} diff --git a/core/src/registry.rs b/core/src/registry.rs new file mode 100644 index 0000000..dacdf30 --- /dev/null +++ b/core/src/registry.rs @@ -0,0 +1,1459 @@ +use std::str::FromStr; + +use serde::Serialize; +use serde_json::{Map, Number, Value}; + +use crate::address; +use crate::crypto; +use crate::error::BrowserCoreError; +use crate::{PRIVATE_KEY_LENGTH, SIGNATURE_LENGTH}; + +#[derive(Clone, Copy)] +enum FieldKind { + U8, + U16, + U32, + U64, + U128, + I8, + I16, + I32, + I64, + I128, + Bool, + Address, + VanityAddress, + Hash(usize), + Text(usize), + PaddedText(usize), +} + +#[derive(Clone, Copy)] +struct Field { + name: &'static str, + kind: FieldKind, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SignatureMode { + Single, + Dual, +} + +struct Schema { + txtype: u8, + name: &'static str, + wrapper: &'static str, + fields: &'static [Field], + signatures: SignatureMode, + stores_hash: bool, +} + +macro_rules! f { + ($name:literal, $kind:expr) => { + Field { + name: $name, + kind: $kind, + } + }; +} + +const TRANSFER: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("value", FieldKind::U64), + f!("coin", FieldKind::PaddedText(15)), + f!("nft_series", FieldKind::U32), + f!("sender", FieldKind::Address), + f!("receiver", FieldKind::Address), + f!("txfee", FieldKind::U64), +]; +const TOKEN: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("creator", FieldKind::Address), + f!("ticker", FieldKind::PaddedText(15)), + f!("number", FieldKind::U64), + f!("hard_limit", FieldKind::U8), + f!("txfee", FieldKind::U64), +]; +const NFT: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("creator", FieldKind::Address), + f!("series", FieldKind::U8), + f!("ownership_type", FieldKind::U8), + f!("nft_name", FieldKind::PaddedText(15)), + f!("item_ipfs", FieldKind::PaddedText(100)), + f!("count", FieldKind::U32), + f!("desc", FieldKind::PaddedText(100)), + f!("txfee", FieldKind::U64), +]; +const MARKETING: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("campaign", FieldKind::U64), + f!("ad_type", FieldKind::PaddedText(6)), + f!("keyword", FieldKind::PaddedText(40)), + f!("displayed", FieldKind::PaddedText(100)), + f!("impression", FieldKind::U8), + f!("click", FieldKind::U8), + f!("impression_value", FieldKind::U16), + f!("click_value", FieldKind::U16), + f!("advertiser", FieldKind::Address), + f!("txfee", FieldKind::U64), +]; +const SWAP: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("timestamp", FieldKind::U32), + f!("offer_expiration", FieldKind::U32), + f!("ticker1", FieldKind::PaddedText(15)), + f!("nft_series1", FieldKind::U32), + f!("value1", FieldKind::U64), + f!("ticker2", FieldKind::PaddedText(15)), + f!("nft_series2", FieldKind::U32), + f!("value2", FieldKind::U64), + f!("sender1", FieldKind::Address), + f!("sender2", FieldKind::Address), + f!("tip1", FieldKind::U64), + f!("tip2", FieldKind::U64), + f!("txfee1", FieldKind::U64), + f!("txfee2", FieldKind::U64), +]; +const LOAN: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("timestamp", FieldKind::U32), + f!("loan_coin", FieldKind::PaddedText(15)), + f!("loan_amount", FieldKind::U64), + f!("lender", FieldKind::Address), + f!("collateral", FieldKind::PaddedText(21)), + f!("collateral_amount", FieldKind::U64), + f!("borrower", FieldKind::Address), + f!("payment_period", FieldKind::Text(1)), + f!("payment_number", FieldKind::U8), + f!("payment_amount", FieldKind::U64), + f!("grace_period", FieldKind::U8), + f!("max_late_value", FieldKind::U64), + f!("txfee", FieldKind::U64), +]; +const LOAN_PAYMENT: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("timestamp", FieldKind::U32), + f!("payback_amount", FieldKind::U64), + f!("contract_hash", FieldKind::Hash(32)), + f!("address", FieldKind::Address), + f!("tip", FieldKind::U64), + f!("txfee", FieldKind::U64), +]; +const COLLATERAL: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("contract_hash", FieldKind::Hash(32)), + f!("address", FieldKind::Address), + f!("txfee", FieldKind::U64), +]; +const BURN: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("address", FieldKind::Address), + f!("coin", FieldKind::PaddedText(15)), + f!("nft_series", FieldKind::U32), + f!("value", FieldKind::U64), + f!("txfee", FieldKind::U64), +]; +const ISSUE_TOKEN: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("creator", FieldKind::Address), + f!("ticker", FieldKind::PaddedText(15)), + f!("number", FieldKind::U64), + f!("txfee", FieldKind::U64), +]; +const VANITY: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("timestamp", FieldKind::U32), + f!("address", FieldKind::Address), + f!("vanity_address", FieldKind::VanityAddress), + f!("txfee", FieldKind::U64), +]; +const STORAGE_KEY: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("address", FieldKind::Address), + f!("txfee", FieldKind::U64), +]; + +macro_rules! storage_fields { + ($kind:expr) => { + &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("storagekey", FieldKind::Hash(32)), + f!("key", FieldKind::PaddedText(50)), + f!("value", $kind), + f!("address", FieldKind::Address), + f!("txfee", FieldKind::U64), + ] + }; +} + +const STORAGE_BOOL: &[Field] = storage_fields!(FieldKind::Bool); +const STORAGE_U8: &[Field] = storage_fields!(FieldKind::U8); +const STORAGE_U16: &[Field] = storage_fields!(FieldKind::U16); +const STORAGE_U32: &[Field] = storage_fields!(FieldKind::U32); +const STORAGE_U64: &[Field] = storage_fields!(FieldKind::U64); +const STORAGE_U128: &[Field] = storage_fields!(FieldKind::U128); +const STORAGE_I8: &[Field] = storage_fields!(FieldKind::I8); +const STORAGE_I16: &[Field] = storage_fields!(FieldKind::I16); +const STORAGE_I32: &[Field] = storage_fields!(FieldKind::I32); +const STORAGE_I64: &[Field] = storage_fields!(FieldKind::I64); +const STORAGE_I128: &[Field] = storage_fields!(FieldKind::I128); +const STORAGE_STRING: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("storagekey", FieldKind::Hash(32)), + f!("key", FieldKind::PaddedText(50)), + f!("value", FieldKind::PaddedText(180)), + f!("address", FieldKind::Address), + f!("previous", FieldKind::Hash(32)), + f!("txfee", FieldKind::U64), +]; +const DELETE_KEY: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("storagekey", FieldKind::Hash(32)), + f!("key", FieldKind::PaddedText(50)), + f!("address", FieldKind::Address), + f!("txfee", FieldKind::U64), +]; +const PROPOSAL_KEY: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("proposal_hash", FieldKind::Hash(32)), + f!("address", FieldKind::Address), + f!("txfee", FieldKind::U64), +]; +const PROPOSAL_VOTE: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("proposal_key", FieldKind::Hash(32)), + f!("address", FieldKind::Address), + f!("vote", FieldKind::U8), + f!("txfee", FieldKind::U64), +]; +const ACTIVATION_VOTE: &[Field] = &[ + f!("txtype", FieldKind::U8), + f!("time", FieldKind::U32), + f!("proposal_key", FieldKind::Hash(32)), + f!("development_hash", FieldKind::Hash(32)), + f!("development_location", FieldKind::PaddedText(100)), + f!("address", FieldKind::Address), + f!("vote", FieldKind::U8), + f!("txfee", FieldKind::U64), +]; + +const SCHEMAS: &[Schema] = &[ + Schema { + txtype: 2, + name: "Transfer", + wrapper: "unsigned_transfer", + fields: TRANSFER, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 3, + name: "Create Token", + wrapper: "unsigned_create_token", + fields: TOKEN, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 4, + name: "Create NFT/RWA", + wrapper: "unsigned_create_nft", + fields: NFT, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 5, + name: "Marketing", + wrapper: "unsigned_marketing", + fields: MARKETING, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 6, + name: "Swap", + wrapper: "unsigned_swap", + fields: SWAP, + signatures: SignatureMode::Dual, + stores_hash: false, + }, + Schema { + txtype: 7, + name: "Loan", + wrapper: "unsigned_loan_contract", + fields: LOAN, + signatures: SignatureMode::Dual, + stores_hash: true, + }, + Schema { + txtype: 8, + name: "Loan Payment", + wrapper: "unsigned_contract_payment", + fields: LOAN_PAYMENT, + signatures: SignatureMode::Single, + stores_hash: true, + }, + Schema { + txtype: 9, + name: "Collateral Claim", + wrapper: "unsigned_collateral_claim", + fields: COLLATERAL, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 10, + name: "Burn", + wrapper: "unsigned_burn", + fields: BURN, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 11, + name: "Issue Token", + wrapper: "unsigned_issue_token", + fields: ISSUE_TOKEN, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 12, + name: "Vanity Address", + wrapper: "unsigned_vanity_address", + fields: VANITY, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 100, + name: "Storage Key", + wrapper: "unsigned_storagekey", + fields: STORAGE_KEY, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 101, + name: "Storage Bool", + wrapper: "unsigned_storagebool", + fields: STORAGE_BOOL, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 102, + name: "Storage U8", + wrapper: "unsigned_storageu8", + fields: STORAGE_U8, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 103, + name: "Storage U16", + wrapper: "unsigned_storageu16", + fields: STORAGE_U16, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 104, + name: "Storage U32", + wrapper: "unsigned_storageu32", + fields: STORAGE_U32, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 105, + name: "Storage U64", + wrapper: "unsigned_storageu64", + fields: STORAGE_U64, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 106, + name: "Storage U128", + wrapper: "unsigned_storageu128", + fields: STORAGE_U128, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 107, + name: "Storage String", + wrapper: "unsigned_storage", + fields: STORAGE_STRING, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 108, + name: "Storage I8", + wrapper: "unsigned_storagei8", + fields: STORAGE_I8, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 109, + name: "Storage I16", + wrapper: "unsigned_storagei16", + fields: STORAGE_I16, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 110, + name: "Storage I32", + wrapper: "unsigned_storagei32", + fields: STORAGE_I32, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 111, + name: "Storage I64", + wrapper: "unsigned_storagei64", + fields: STORAGE_I64, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 112, + name: "Storage I128", + wrapper: "unsigned_storagei128", + fields: STORAGE_I128, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 113, + name: "Delete Storage Key", + wrapper: "unsigned_deletekey", + fields: DELETE_KEY, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 200, + name: "Proposal Key", + wrapper: "unsigned_proposalkey", + fields: PROPOSAL_KEY, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 201, + name: "Proposal Vote", + wrapper: "unsigned_proposalvote", + fields: PROPOSAL_VOTE, + signatures: SignatureMode::Single, + stores_hash: false, + }, + Schema { + txtype: 202, + name: "Activation Vote", + wrapper: "unsigned_activationvote", + fields: ACTIVATION_VOTE, + signatures: SignatureMode::Single, + stores_hash: false, + }, +]; + +#[derive(Serialize)] +pub struct Inspection { + pub txtype: u8, + pub transaction_name: &'static str, + pub canonical_unsigned_json: String, + pub hash: String, + pub unsigned_bytes_hex: String, + pub required_signatures: u8, + pub review: Value, +} + +#[derive(Serialize)] +pub struct RegistryEntry { + pub txtype: u8, + pub transaction_name: &'static str, + pub byte_length: usize, + pub required_signatures: u8, + pub signer1_field: &'static str, + pub signer2_field: Option<&'static str>, +} + +#[derive(Serialize)] +pub struct SigningResult { + pub complete: bool, + pub txtype: u8, + pub transaction_name: &'static str, + pub hash: String, + pub unsigned_bytes_hex: String, + pub signature1: Option, + pub signature2: Option, + pub signed_transaction: Value, + pub bytes_hex: Option, +} + +#[derive(Serialize)] +pub struct DecodedTransaction { + pub txtype: u8, + pub transaction_name: &'static str, + pub fields: Value, + pub stored_hash: Option, + pub signature: Option, + pub signature1: Option, + pub signature2: Option, +} + +fn schema(txtype: u8) -> Result<&'static Schema, BrowserCoreError> { + SCHEMAS + .iter() + .find(|schema| schema.txtype == txtype) + .ok_or(BrowserCoreError::UnsupportedTransaction(txtype)) +} + +fn request_object(request: &Value) -> Result<&Map, BrowserCoreError> { + request + .as_object() + .ok_or_else(|| BrowserCoreError::InvalidRequest("request must be a JSON object".into())) +} + +fn request_source<'a>( + request: &'a Value, + schema: &Schema, +) -> Result<&'a Map, BrowserCoreError> { + let object = request_object(request)?; + match object.get(schema.wrapper) { + Some(Value::Object(unsigned)) => Ok(unsigned), + Some(_) => Err(BrowserCoreError::InvalidRequest(format!( + "{} must be a JSON object", + schema.wrapper + ))), + None => Ok(object), + } +} + +fn request_type(request: &Value) -> Result { + let object = request_object(request)?; + if let Some(value) = object.get("txtype") { + return parse_integer::(value, "txtype"); + } + for schema in SCHEMAS { + if let Some(Value::Object(unsigned)) = object.get(schema.wrapper) { + let value = unsigned + .get("txtype") + .ok_or_else(|| BrowserCoreError::InvalidRequest("txtype is required".into()))?; + return parse_integer::(value, "txtype"); + } + } + Err(BrowserCoreError::InvalidRequest( + "txtype is required".into(), + )) +} + +fn canonical_unsigned(request: &Value, schema: &Schema) -> Result { + let source = request_source(request, schema)?; + let allowed_extra = ["signature", "signature1", "signature2", "hash"]; + for key in source.keys() { + if !schema.fields.iter().any(|field| field.name == key) + && !allowed_extra.contains(&key.as_str()) + { + return Err(BrowserCoreError::InvalidRequest(format!( + "unexpected field: {key}" + ))); + } + } + let mut canonical = Map::new(); + for field in schema.fields { + let value = source.get(field.name).ok_or_else(|| { + BrowserCoreError::InvalidRequest(format!("missing field: {}", field.name)) + })?; + canonical.insert(field.name.into(), canonical_value(value, field)?); + } + if canonical.get("txtype").and_then(Value::as_u64) != Some(schema.txtype as u64) { + return Err(BrowserCoreError::InvalidRequest(format!( + "txtype must be {}", + schema.txtype + ))); + } + let canonical = Value::Object(canonical); + validate_semantics(schema, &canonical)?; + Ok(canonical) +} + +fn validate_semantics(schema: &Schema, unsigned: &Value) -> Result<(), BrowserCoreError> { + let object = request_object(unsigned)?; + let byte_flag = |name: &str| { + let value = object.get(name).and_then(Value::as_u64); + if matches!(value, Some(0 | 1)) { + Ok(()) + } else { + Err(BrowserCoreError::InvalidRequest(format!( + "{name} must be 0 or 1" + ))) + } + }; + + match schema.txtype { + 2 => { + if object.get("sender") == object.get("receiver") { + return Err(BrowserCoreError::InvalidRequest( + "sender and receiver cannot be the same".into(), + )); + } + if object + .get("nft_series") + .and_then(Value::as_u64) + .unwrap_or(0) + > 0 + && object.get("value").and_then(Value::as_u64) != Some(100_000_000) + { + return Err(BrowserCoreError::InvalidRequest( + "series NFT transfers must have a value of 100000000".into(), + )); + } + } + 3 => byte_flag("hard_limit")?, + 4 => { + byte_flag("series")?; + byte_flag("ownership_type")?; + if object.get("count").and_then(Value::as_u64) == Some(0) { + return Err(BrowserCoreError::InvalidRequest( + "NFT count cannot be zero".into(), + )); + } + } + 6 => { + if object.get("sender1") == object.get("sender2") { + return Err(BrowserCoreError::InvalidRequest( + "swap participants cannot be the same".into(), + )); + } + if object.get("offer_expiration").and_then(Value::as_u64) + <= object.get("timestamp").and_then(Value::as_u64) + { + return Err(BrowserCoreError::InvalidRequest( + "swap expiration must be after its timestamp".into(), + )); + } + } + 7 => { + if object.get("lender") == object.get("borrower") { + return Err(BrowserCoreError::InvalidRequest( + "lender and borrower cannot be the same".into(), + )); + } + if !matches!( + object.get("payment_period").and_then(Value::as_str), + Some("d" | "w" | "m") + ) { + return Err(BrowserCoreError::InvalidRequest( + "payment_period must be d, w, or m".into(), + )); + } + } + 201 | 202 => byte_flag("vote")?, + _ => {} + } + Ok(()) +} + +fn canonical_value(value: &Value, field: &Field) -> Result { + let invalid = || BrowserCoreError::InvalidRequest(format!("invalid {}", field.name)); + match field.kind { + FieldKind::Bool => value.as_bool().map(Value::Bool).ok_or_else(invalid), + FieldKind::U8 => number_unsigned::(value, field.name), + FieldKind::U16 => number_unsigned::(value, field.name), + FieldKind::U32 => number_unsigned::(value, field.name), + FieldKind::U64 => number_unsigned::(value, field.name), + FieldKind::U128 => number_from_text::(value, field.name), + FieldKind::I8 => number_signed::(value, field.name), + FieldKind::I16 => number_signed::(value, field.name), + FieldKind::I32 => number_signed::(value, field.name), + FieldKind::I64 => number_signed::(value, field.name), + FieldKind::I128 => number_from_text::(value, field.name), + FieldKind::Address => { + let text = value.as_str().ok_or_else(invalid)?; + address::short_address_to_bytes(text)?; + Ok(Value::String(text.into())) + } + FieldKind::VanityAddress => { + let text = value.as_str().ok_or_else(invalid)?; + Ok(Value::String(address::normalize_vanity_address(text)?)) + } + FieldKind::Hash(length) => { + let text = value.as_str().ok_or_else(invalid)?; + let bytes = hex::decode(text).map_err(|_| invalid())?; + if bytes.len() != length { + return Err(invalid()); + } + Ok(Value::String(text.to_ascii_lowercase())) + } + FieldKind::Text(length) => { + let text = value.as_str().ok_or_else(invalid)?; + if !text.is_ascii() || text.len() != length { + return Err(invalid()); + } + Ok(Value::String(text.into())) + } + FieldKind::PaddedText(length) => { + let text = value.as_str().ok_or_else(invalid)?.trim(); + if text.is_empty() || !text.is_ascii() || text.len() > length { + return Err(invalid()); + } + Ok(Value::String(format!("{text:(value: &Value, name: &str) -> Result { + let text = match value { + Value::String(text) => text.clone(), + Value::Number(number) => number.to_string(), + _ => { + return Err(BrowserCoreError::InvalidRequest(format!( + "{name} must be an integer" + ))) + } + }; + text.parse::() + .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}"))) +} + +fn number_unsigned(value: &Value, name: &str) -> Result +where + T: FromStr + Into, +{ + let parsed = parse_integer::(value, name)?; + let number = Number::from_str(&parsed.into().to_string()) + .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))?; + Ok(Value::Number(number)) +} + +fn number_signed(value: &Value, name: &str) -> Result +where + T: FromStr + Into, +{ + let parsed = parse_integer::(value, name)?; + let number = Number::from_str(&parsed.into().to_string()) + .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))?; + Ok(Value::Number(number)) +} + +fn number_from_text( + value: &Value, + name: &str, +) -> Result { + let parsed = parse_integer::(value, name)?; + let number = Number::from_str(&parsed.to_string()) + .map_err(|_| BrowserCoreError::InvalidRequest(format!("invalid {name}")))?; + Ok(Value::Number(number)) +} + +pub fn inspect(request: &Value) -> Result { + let txtype = request_type(request)?; + let schema = schema(txtype)?; + let unsigned = canonical_unsigned(request, schema)?; + let canonical_unsigned_json = serde_json::to_string(&unsigned) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let hash = hex::encode(crypto::skein_256(canonical_unsigned_json.as_bytes())); + let unsigned_bytes_hex = hex::encode(encode_unsigned_prefix(schema, &unsigned, &hash)?); + Ok(Inspection { + txtype, + transaction_name: schema.name, + canonical_unsigned_json, + hash, + unsigned_bytes_hex, + required_signatures: if schema.signatures == SignatureMode::Dual { + 2 + } else { + 1 + }, + review: unsigned, + }) +} + +pub fn sign_request( + request: &Value, + private_key: &[u8], + signer_public_key: &[u8], + signer_slot: u8, +) -> Result { + if private_key.len() != PRIVATE_KEY_LENGTH { + return Err(BrowserCoreError::InvalidPrivateKey); + } + let inspection = inspect(request)?; + let schema = schema(inspection.txtype)?; + let source = request_object(request)?; + let signature_source = if source.contains_key(schema.wrapper) { + source + } else { + request_source(request, schema)? + }; + let unsigned: Value = serde_json::from_str(&inspection.canonical_unsigned_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let signer_address = address::public_key_to_short_address(signer_public_key)?; + validate_signer(schema, &unsigned, &signer_address, signer_slot)?; + let hash_bytes = hex::decode(&inspection.hash).unwrap(); + let new_signature_bytes = crypto::sign(&hash_bytes, private_key)?; + if !crypto::verify(&hash_bytes, &new_signature_bytes, signer_public_key) { + return Err(BrowserCoreError::InvalidRequest( + "private key does not match the wallet public key".into(), + )); + } + let new_signature = hex::encode(new_signature_bytes); + let mut signature1 = signature_source + .get("signature1") + .or_else(|| source.get("signature")) + .and_then(Value::as_str) + .map(str::to_owned); + let mut signature2 = signature_source + .get("signature2") + .and_then(Value::as_str) + .map(str::to_owned); + + if schema.signatures == SignatureMode::Dual && (signature1.is_some() || signature2.is_some()) { + return Err(BrowserCoreError::InvalidRequest( + "dual-signature wallets must sign the unsigned transaction independently".into(), + )); + } + + match schema.signatures { + SignatureMode::Single => { + if signer_slot != 1 { + return Err(BrowserCoreError::InvalidRequest( + "single-signature transactions use signer slot 1".into(), + )); + } + signature1 = Some(new_signature); + } + SignatureMode::Dual => match signer_slot { + 1 if signature1.is_none() => signature1 = Some(new_signature), + 2 if signature2.is_none() => signature2 = Some(new_signature), + 1 => { + return Err(BrowserCoreError::InvalidRequest( + "signature1 already exists".into(), + )) + } + 2 => { + return Err(BrowserCoreError::InvalidRequest( + "signature2 already exists".into(), + )) + } + _ => { + return Err(BrowserCoreError::InvalidRequest( + "signer slot must be 1 or 2".into(), + )) + } + }, + } + + if signature1.is_some() { + validate_signature(signature1.as_deref())?; + } + if signature2.is_some() { + validate_signature(signature2.as_deref())?; + } + let complete = signature1.is_some() + && (schema.signatures == SignatureMode::Single || signature2.is_some()); + let mut signed = Map::new(); + signed.insert(schema.wrapper.into(), unsigned.clone()); + if schema.stores_hash { + signed.insert("hash".into(), Value::String(inspection.hash.clone())); + } + if schema.signatures == SignatureMode::Single { + signed.insert( + "signature".into(), + Value::String(signature1.clone().unwrap()), + ); + } else { + if let Some(value) = &signature1 { + signed.insert("signature1".into(), Value::String(value.clone())); + } + if let Some(value) = &signature2 { + signed.insert("signature2".into(), Value::String(value.clone())); + } + } + let bytes_hex = if complete { + Some(hex::encode(encode_transaction( + schema, + &unsigned, + &inspection.hash, + signature1.as_deref(), + signature2.as_deref(), + )?)) + } else { + None + }; + Ok(SigningResult { + complete, + txtype: schema.txtype, + transaction_name: schema.name, + hash: inspection.hash, + unsigned_bytes_hex: inspection.unsigned_bytes_hex, + signature1, + signature2, + signed_transaction: Value::Object(signed), + bytes_hex, + }) +} + +pub fn complete_dual_request( + request: &Value, + signature1: &str, + public_key1: &[u8], + signature2: &str, + public_key2: &[u8], +) -> Result { + let inspection = inspect(request)?; + let schema = schema(inspection.txtype)?; + if schema.signatures != SignatureMode::Dual { + return Err(BrowserCoreError::InvalidRequest( + "only swaps and loan creation use dual signatures".into(), + )); + } + validate_signature(Some(signature1))?; + validate_signature(Some(signature2))?; + let unsigned: Value = serde_json::from_str(&inspection.canonical_unsigned_json) + .map_err(|error| BrowserCoreError::Json(error.to_string()))?; + let address1 = address::public_key_to_short_address(public_key1)?; + let address2 = address::public_key_to_short_address(public_key2)?; + validate_signer(schema, &unsigned, &address1, 1)?; + validate_signer(schema, &unsigned, &address2, 2)?; + let hash_bytes = hex::decode(&inspection.hash).unwrap(); + let signature1_bytes = + hex::decode(signature1).map_err(|_| BrowserCoreError::InvalidSignature)?; + let signature2_bytes = + hex::decode(signature2).map_err(|_| BrowserCoreError::InvalidSignature)?; + if !crypto::verify(&hash_bytes, &signature1_bytes, public_key1) { + return Err(BrowserCoreError::InvalidRequest( + "signature1 is not valid for signer 1".into(), + )); + } + if !crypto::verify(&hash_bytes, &signature2_bytes, public_key2) { + return Err(BrowserCoreError::InvalidRequest( + "signature2 is not valid for signer 2".into(), + )); + } + let mut signed = Map::new(); + signed.insert(schema.wrapper.into(), unsigned.clone()); + if schema.stores_hash { + signed.insert("hash".into(), Value::String(inspection.hash.clone())); + } + signed.insert("signature1".into(), Value::String(signature1.into())); + signed.insert("signature2".into(), Value::String(signature2.into())); + let bytes_hex = hex::encode(encode_transaction( + schema, + &unsigned, + &inspection.hash, + Some(signature1), + Some(signature2), + )?); + Ok(SigningResult { + complete: true, + txtype: schema.txtype, + transaction_name: schema.name, + hash: inspection.hash, + unsigned_bytes_hex: inspection.unsigned_bytes_hex, + signature1: Some(signature1.into()), + signature2: Some(signature2.into()), + signed_transaction: Value::Object(signed), + bytes_hex: Some(bytes_hex), + }) +} + +pub fn entries() -> Vec { + SCHEMAS + .iter() + .map(|schema| RegistryEntry { + txtype: schema.txtype, + transaction_name: schema.name, + byte_length: wire_length(schema), + required_signatures: if schema.signatures == SignatureMode::Dual { + 2 + } else { + 1 + }, + signer1_field: signer_field(schema.txtype, 1).unwrap(), + signer2_field: signer_field(schema.txtype, 2), + }) + .collect() +} + +pub fn decode_transaction(bytes: &[u8]) -> Result { + let txtype = *bytes + .first() + .ok_or_else(|| BrowserCoreError::InvalidRequest("transaction bytes were empty".into()))?; + let schema = schema(txtype)?; + if bytes.len() != wire_length(schema) { + return Err(BrowserCoreError::InvalidRequest(format!( + "{} transaction must contain exactly {} bytes", + schema.name, + wire_length(schema) + ))); + } + + let mut offset = 0usize; + let mut fields = Map::new(); + for field in schema.fields { + fields.insert(field.name.into(), decode_field(bytes, &mut offset, field)?); + } + + let stored_hash = if schema.stores_hash { + Some(hex::encode(read_bytes(bytes, &mut offset, 32)?)) + } else { + None + }; + let first_signature = hex::encode(read_bytes(bytes, &mut offset, SIGNATURE_LENGTH)?); + let (signature, signature1, signature2) = if schema.signatures == SignatureMode::Dual { + let second_signature = hex::encode(read_bytes(bytes, &mut offset, SIGNATURE_LENGTH)?); + (None, Some(first_signature), Some(second_signature)) + } else { + (Some(first_signature), None, None) + }; + + if offset != bytes.len() { + return Err(BrowserCoreError::InvalidRequest( + "transaction contained trailing bytes".into(), + )); + } + + Ok(DecodedTransaction { + txtype, + transaction_name: schema.name, + fields: Value::Object(fields), + stored_hash, + signature, + signature1, + signature2, + }) +} + +fn read_bytes<'a>( + bytes: &'a [u8], + offset: &mut usize, + length: usize, +) -> Result<&'a [u8], BrowserCoreError> { + let end = offset.checked_add(length).ok_or_else(|| { + BrowserCoreError::InvalidRequest("transaction field offset overflowed".into()) + })?; + let value = bytes.get(*offset..end).ok_or_else(|| { + BrowserCoreError::InvalidRequest("transaction bytes were truncated".into()) + })?; + *offset = end; + Ok(value) +} + +fn decode_field( + bytes: &[u8], + offset: &mut usize, + field: &Field, +) -> Result { + let raw = read_bytes(bytes, offset, field_width(field.kind))?; + let invalid = || BrowserCoreError::InvalidRequest(format!("invalid encoded {}", field.name)); + Ok(match field.kind { + FieldKind::Bool => match raw[0] { + 0 => Value::Bool(false), + 1 => Value::Bool(true), + _ => return Err(invalid()), + }, + FieldKind::U8 => Value::Number(Number::from(raw[0])), + FieldKind::U16 => Value::Number(Number::from(u16::from_le_bytes( + raw.try_into().map_err(|_| invalid())?, + ))), + FieldKind::U32 => Value::Number(Number::from(u32::from_le_bytes( + raw.try_into().map_err(|_| invalid())?, + ))), + FieldKind::U64 => { + Value::String(u64::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string()) + } + FieldKind::U128 => { + Value::String(u128::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string()) + } + FieldKind::I8 => Value::Number(Number::from(raw[0] as i8)), + FieldKind::I16 => Value::Number(Number::from(i16::from_le_bytes( + raw.try_into().map_err(|_| invalid())?, + ))), + FieldKind::I32 => Value::Number(Number::from(i32::from_le_bytes( + raw.try_into().map_err(|_| invalid())?, + ))), + FieldKind::I64 => { + Value::String(i64::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string()) + } + FieldKind::I128 => { + Value::String(i128::from_le_bytes(raw.try_into().map_err(|_| invalid())?).to_string()) + } + FieldKind::Address => Value::String(address::bytes_to_short_address(raw)?), + FieldKind::VanityAddress => { + if raw.len() != 22 || raw[20] != b'.' || raw[21] != address::network_byte() { + return Err(invalid()); + } + let name = std::str::from_utf8(&raw[..20]) + .map_err(|_| invalid())? + .trim() + .to_string(); + Value::String(format!("{name}.{}", address::network_suffix())) + } + FieldKind::Hash(_) => Value::String(hex::encode(raw)), + FieldKind::Text(_) | FieldKind::PaddedText(_) => { + let text = std::str::from_utf8(raw).map_err(|_| invalid())?; + Value::String( + text.trim_end_matches(|character| character == '\0' || character == ' ') + .to_string(), + ) + } + }) +} + +fn validate_signer( + schema: &Schema, + unsigned: &Value, + signer_address: &str, + signer_slot: u8, +) -> Result<(), BrowserCoreError> { + address::short_address_to_bytes(signer_address)?; + let field = signer_field(schema.txtype, signer_slot).ok_or_else(|| { + BrowserCoreError::InvalidRequest(format!( + "transaction type {} does not use signer slot {signer_slot}", + schema.txtype + )) + })?; + let expected = request_object(unsigned)? + .get(field) + .and_then(Value::as_str) + .ok_or_else(|| { + BrowserCoreError::InvalidRequest(format!("missing signer field: {field}")) + })?; + if !expected.eq_ignore_ascii_case(signer_address) { + return Err(BrowserCoreError::InvalidRequest(format!( + "wallet address does not match {field}" + ))); + } + Ok(()) +} + +fn signer_field(txtype: u8, slot: u8) -> Option<&'static str> { + match (txtype, slot) { + (2, 1) => Some("sender"), + (3 | 4 | 11, 1) => Some("creator"), + (5, 1) => Some("advertiser"), + (6, 1) => Some("sender1"), + (6, 2) => Some("sender2"), + (7, 1) => Some("lender"), + (7, 2) => Some("borrower"), + (8 | 9 | 10 | 12 | 100..=113 | 200..=202, 1) => Some("address"), + _ => None, + } +} + +fn validate_signature(signature: Option<&str>) -> Result<(), BrowserCoreError> { + let signature = signature.ok_or(BrowserCoreError::InvalidSignature)?; + if hex::decode(signature).map_or(true, |bytes| bytes.len() != SIGNATURE_LENGTH) { + return Err(BrowserCoreError::InvalidSignature); + } + Ok(()) +} + +fn encode_transaction( + schema: &Schema, + unsigned: &Value, + hash: &str, + signature1: Option<&str>, + signature2: Option<&str>, +) -> Result, BrowserCoreError> { + let mut bytes = encode_unsigned_prefix(schema, unsigned, hash)?; + bytes.extend_from_slice( + &hex::decode(signature1.ok_or(BrowserCoreError::InvalidSignature)?).unwrap(), + ); + if schema.signatures == SignatureMode::Dual { + bytes.extend_from_slice( + &hex::decode(signature2.ok_or(BrowserCoreError::InvalidSignature)?).unwrap(), + ); + } + Ok(bytes) +} + +fn encode_unsigned_prefix( + schema: &Schema, + unsigned: &Value, + hash: &str, +) -> Result, BrowserCoreError> { + let object = request_object(unsigned)?; + let mut bytes = Vec::new(); + for field in schema.fields { + encode_field(&mut bytes, object.get(field.name).unwrap(), field)?; + } + if schema.stores_hash { + bytes.extend_from_slice(&hex::decode(hash).unwrap()); + } + Ok(bytes) +} + +fn encode_field(bytes: &mut Vec, value: &Value, field: &Field) -> Result<(), BrowserCoreError> { + match field.kind { + FieldKind::Bool => bytes.push(u8::from(value.as_bool().unwrap())), + FieldKind::U8 => bytes.push(parse_integer::(value, field.name)?), + FieldKind::U16 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::U32 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::U64 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::U128 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::I8 => bytes.push(parse_integer::(value, field.name)? as u8), + FieldKind::I16 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::I32 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::I64 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::I128 => { + bytes.extend_from_slice(&parse_integer::(value, field.name)?.to_le_bytes()) + } + FieldKind::Address => { + bytes.extend_from_slice(&address::short_address_to_bytes(value.as_str().unwrap())?) + } + FieldKind::VanityAddress => { + bytes.extend_from_slice(&address::vanity_address_to_bytes(value.as_str().unwrap())?) + } + FieldKind::Hash(_) => { + bytes.extend_from_slice(&hex::decode(value.as_str().unwrap()).unwrap()) + } + FieldKind::Text(_) | FieldKind::PaddedText(_) => { + bytes.extend_from_slice(value.as_str().unwrap().as_bytes()) + } + } + Ok(()) +} + +fn field_width(kind: FieldKind) -> usize { + match kind { + FieldKind::U8 | FieldKind::I8 | FieldKind::Bool => 1, + FieldKind::U16 | FieldKind::I16 => 2, + FieldKind::U32 | FieldKind::I32 => 4, + FieldKind::U64 | FieldKind::I64 => 8, + FieldKind::U128 | FieldKind::I128 => 16, + FieldKind::Address | FieldKind::VanityAddress => 22, + FieldKind::Hash(length) | FieldKind::Text(length) | FieldKind::PaddedText(length) => length, + } +} + +fn wire_length(schema: &Schema) -> usize { + let fields: usize = schema + .fields + .iter() + .map(|field| field_width(field.kind)) + .sum(); + fields + + usize::from(schema.stores_hash) * 32 + + if schema.signatures == SignatureMode::Dual { + SIGNATURE_LENGTH * 2 + } else { + SIGNATURE_LENGTH + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registry_contains_every_externally_submitted_type() { + let types: Vec = SCHEMAS.iter().map(|schema| schema.txtype).collect(); + assert_eq!( + types, + vec![ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 100, 101, 102, 103, 104, 105, 106, 107, 108, + 109, 110, 111, 112, 113, 200, 201, 202 + ] + ); + } + + #[test] + fn node_only_types_are_not_signable() { + assert!(matches!( + schema(0), + Err(BrowserCoreError::UnsupportedTransaction(0)) + )); + assert!(matches!( + schema(1), + Err(BrowserCoreError::UnsupportedTransaction(1)) + )); + } + + #[test] + fn registry_wire_lengths_match_node_command_map() { + let expected = [ + (2, 750), + (3, 725), + (4, 922), + (5, 861), + (6, 1471), + (7, 1492), + (8, 781), + (9, 733), + (10, 728), + (11, 724), + (12, 723), + (100, 701), + (101, 784), + (102, 784), + (103, 785), + (104, 787), + (105, 791), + (106, 799), + (107, 995), + (108, 784), + (109, 785), + (110, 787), + (111, 791), + (112, 799), + (113, 783), + (200, 733), + (201, 734), + (202, 866), + ]; + for (txtype, expected_length) in expected { + assert_eq!( + wire_length(schema(txtype).unwrap()), + expected_length, + "txtype {txtype}" + ); + } + } + + #[test] + fn signed_transaction_bytes_decode_without_losing_fields() { + let schema = schema(2).unwrap(); + let suffix = crate::address::network_suffix(); + let request = serde_json::json!({ + "txtype": 2, + "time": 1_700_000_000, + "value": "9007199254740993", + "coin": "CLTC", + "nft_series": 0, + "sender": format!("1111111111111111111111111111111111111111.{suffix}"), + "receiver": format!("2222222222222222222222222222222222222222.{suffix}"), + "txfee": "2500" + }); + let unsigned = canonical_unsigned(&request, schema).unwrap(); + let signature = "33".repeat(SIGNATURE_LENGTH); + let bytes = encode_transaction(schema, &unsigned, &"00".repeat(32), Some(&signature), None) + .unwrap(); + + let decoded = decode_transaction(&bytes).unwrap(); + let fields = decoded.fields.as_object().unwrap(); + assert_eq!(decoded.transaction_name, "Transfer"); + assert_eq!(fields["value"], "9007199254740993"); + assert_eq!(fields["coin"], "CLTC"); + assert_eq!(fields["receiver"], format!("2222222222222222222222222222222222222222.{suffix}")); + assert_eq!(decoded.signature.as_deref(), Some(signature.as_str())); + assert!(decoded.signature1.is_none()); + assert!(decoded.signature2.is_none()); + } + + #[test] + fn storage_keys_are_right_padded_during_canonicalization() { + let schema = schema(101).unwrap(); + let suffix = crate::address::network_suffix(); + let request = serde_json::json!({ + "txtype": 101, + "time": 1_700_000_000, + "storagekey": "00".repeat(32), + "key": "profile_name", + "value": true, + "address": format!("1111111111111111111111111111111111111111.{suffix}"), + "txfee": "100000000" + }); + + let unsigned = canonical_unsigned(&request, schema).unwrap(); + let key = unsigned.get("key").and_then(Value::as_str).unwrap(); + + assert_eq!(key.len(), 50); + assert_eq!(key.trim_end(), "profile_name"); + } + + #[test] + fn dual_signatures_are_created_independently_and_combined() { + let first_wallet = crate::wallet::generate("first wallet password").unwrap(); + let second_wallet = crate::wallet::generate("second wallet password").unwrap(); + let first = crate::wallet::unlock(&first_wallet, "first wallet password").unwrap(); + let second = crate::wallet::unlock(&second_wallet, "second wallet password").unwrap(); + let request = serde_json::json!({ + "txtype": 6, + "timestamp": 1_800_000_000, + "offer_expiration": 1_800_003_600u32, + "ticker1": "CLTC", + "nft_series1": 0, + "value1": "100000000", + "ticker2": "TEST", + "nft_series2": 0, + "value2": "200000000", + "sender1": first.short_address, + "sender2": second.short_address, + "tip1": "1000000", + "tip2": "1000000", + "txfee1": "1000000", + "txfee2": "1000000" + }); + let second_private = hex::decode(&second.private_key).unwrap(); + let second_public = hex::decode(&second.public_key).unwrap(); + let first_private = hex::decode(&first.private_key).unwrap(); + let first_public = hex::decode(&first.public_key).unwrap(); + let first_result = sign_request(&request, &first_private, &first_public, 1).unwrap(); + let second_result = sign_request(&request, &second_private, &second_public, 2).unwrap(); + + assert!(!first_result.complete); + assert!(first_result.signature1.is_some()); + assert!(first_result.signature2.is_none()); + assert!(!second_result.complete); + assert!(second_result.signature1.is_none()); + assert!(second_result.signature2.is_some()); + assert_eq!(first_result.hash, second_result.hash); + assert_eq!( + first_result.unsigned_bytes_hex, + second_result.unsigned_bytes_hex + ); + + let completed = complete_dual_request( + &request, + first_result.signature1.as_deref().unwrap(), + &first_public, + second_result.signature2.as_deref().unwrap(), + &second_public, + ) + .unwrap(); + + assert!(completed.complete); + assert!(completed.signature1.is_some()); + assert!(completed.signature2.is_some()); + assert_eq!(completed.bytes_hex.unwrap().len() / 2, 1471); + } +} diff --git a/core/src/transfer.rs b/core/src/transfer.rs new file mode 100644 index 0000000..ed2c598 --- /dev/null +++ b/core/src/transfer.rs @@ -0,0 +1,216 @@ +use serde::{Deserialize, Serialize}; + +use crate::address; +use crate::crypto; +use crate::error::BrowserCoreError; +use crate::SIGNATURE_LENGTH; + +pub const TRANSFER_TYPE: u8 = 2; +pub const COIN_BYTES: usize = 15; +pub const TRANSFER_BYTES: usize = 750; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct UnsignedTransfer { + pub txtype: u8, + pub time: u32, + pub value: u64, + pub coin: String, + pub nft_series: u32, + pub sender: String, + pub receiver: String, + pub txfee: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SignedTransfer { + pub unsigned_transfer: UnsignedTransfer, + pub signature: String, +} + +#[derive(Debug, Serialize)] +pub struct BrowserTransferOutput { + pub txtype: u8, + pub time: u32, + pub value: String, + pub coin: String, + pub nft_series: u32, + pub sender: String, + pub receiver: String, + pub txfee: String, + pub hash: String, + pub signature: String, + pub bytes_hex: String, +} + +impl UnsignedTransfer { + pub fn validate(&self) -> Result<(), BrowserCoreError> { + if self.txtype != TRANSFER_TYPE { + return Err(BrowserCoreError::InvalidTransfer( + "transaction type must be 2".into(), + )); + } + if self.coin.len() != COIN_BYTES || !self.coin.is_ascii() { + return Err(BrowserCoreError::InvalidTransfer( + "coin must use the canonical 15-byte padded format".into(), + )); + } + address::short_address_to_bytes(&self.sender)?; + address::short_address_to_bytes(&self.receiver)?; + Ok(()) + } + + pub fn json(&self) -> Result { + serde_json::to_string(self).map_err(|error| BrowserCoreError::Json(error.to_string())) + } + + pub fn hash(&self) -> Result, BrowserCoreError> { + Ok(crypto::skein_256(self.json()?.as_bytes())) + } +} + +impl SignedTransfer { + pub fn validate_shape(&self) -> Result<(), BrowserCoreError> { + self.unsigned_transfer.validate()?; + let signature = + hex::decode(&self.signature).map_err(|_| BrowserCoreError::InvalidSignature)?; + if signature.len() != SIGNATURE_LENGTH { + return Err(BrowserCoreError::InvalidSignature); + } + Ok(()) + } + + pub fn to_bytes(&self) -> Result, BrowserCoreError> { + self.validate_shape()?; + let mut bytes = Vec::with_capacity(TRANSFER_BYTES); + bytes.push(self.unsigned_transfer.txtype); + bytes.extend_from_slice(&self.unsigned_transfer.time.to_le_bytes()); + bytes.extend_from_slice(&self.unsigned_transfer.value.to_le_bytes()); + bytes.extend_from_slice(self.unsigned_transfer.coin.as_bytes()); + bytes.extend_from_slice(&self.unsigned_transfer.nft_series.to_le_bytes()); + bytes.extend_from_slice(&address::short_address_to_bytes( + &self.unsigned_transfer.sender, + )?); + bytes.extend_from_slice(&address::short_address_to_bytes( + &self.unsigned_transfer.receiver, + )?); + bytes.extend_from_slice(&self.unsigned_transfer.txfee.to_le_bytes()); + bytes.extend_from_slice( + &hex::decode(&self.signature).map_err(|_| BrowserCoreError::InvalidSignature)?, + ); + debug_assert_eq!(bytes.len(), TRANSFER_BYTES); + Ok(bytes) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != TRANSFER_BYTES || bytes[0] != TRANSFER_TYPE { + return Err(BrowserCoreError::InvalidTransfer(format!( + "expected a {TRANSFER_BYTES}-byte type-2 transaction" + ))); + } + + let mut offset = 1; + let time = read_u32(bytes, &mut offset); + let value = read_u64(bytes, &mut offset); + let coin = String::from_utf8(bytes[offset..offset + COIN_BYTES].to_vec()) + .map_err(|_| BrowserCoreError::InvalidTransfer("coin is not UTF-8".into()))?; + offset += COIN_BYTES; + let nft_series = read_u32(bytes, &mut offset); + let sender = address::bytes_to_short_address(&bytes[offset..offset + 22])?; + offset += 22; + let receiver = address::bytes_to_short_address(&bytes[offset..offset + 22])?; + offset += 22; + let txfee = read_u64(bytes, &mut offset); + let signature = hex::encode(&bytes[offset..offset + SIGNATURE_LENGTH]); + + let transfer = Self { + unsigned_transfer: UnsignedTransfer { + txtype: TRANSFER_TYPE, + time, + value, + coin, + nft_series, + sender, + receiver, + txfee, + }, + signature, + }; + transfer.validate_shape()?; + Ok(transfer) + } + + pub fn browser_output(&self) -> Result { + let bytes_hex = hex::encode(self.to_bytes()?); + let hash = hex::encode(self.unsigned_transfer.hash()?); + Ok(BrowserTransferOutput { + txtype: self.unsigned_transfer.txtype, + time: self.unsigned_transfer.time, + value: self.unsigned_transfer.value.to_string(), + coin: self.unsigned_transfer.coin.clone(), + nft_series: self.unsigned_transfer.nft_series, + sender: self.unsigned_transfer.sender.clone(), + receiver: self.unsigned_transfer.receiver.clone(), + txfee: self.unsigned_transfer.txfee.to_string(), + hash, + signature: self.signature.clone(), + bytes_hex, + }) + } +} + +fn read_u32(bytes: &[u8], offset: &mut usize) -> u32 { + let value = u32::from_le_bytes(bytes[*offset..*offset + 4].try_into().unwrap()); + *offset += 4; + value +} + +fn read_u64(bytes: &[u8], offset: &mut usize) -> u64 { + let value = u64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap()); + *offset += 8; + value +} + +#[cfg(test)] +mod tests { + use super::*; + + fn address(byte: u8) -> String { + format!("{}.{}", hex::encode([byte; 20]), crate::address::network_suffix()) + } + + fn unsigned() -> UnsignedTransfer { + UnsignedTransfer { + txtype: TRANSFER_TYPE, + time: 1_700_000_000, + value: 123_456_789, + coin: "CLTC ".to_string(), + nft_series: 0, + sender: address(0x11), + receiver: address(0x22), + txfee: 1_234_568, + } + } + + #[test] + fn unsigned_json_matches_node_field_order() { + let serialized = unsigned().json().unwrap(); + assert!(serialized.contains(&format!("1111111111111111111111111111111111111111.{}", crate::address::network_suffix()))); + assert!(serialized.contains(&format!("2222222222222222222222222222222222222222.{}", crate::address::network_suffix()))); + #[cfg(feature = "testnet")] + assert_eq!( + hex::encode(unsigned().hash().unwrap()), + "7e6a1ef0232eee327dd6c18640f85b16e632762a7c424abfc720ee72fb002c32" + ); + } + + #[test] + fn transfer_wire_layout_round_trips() { + let transfer = SignedTransfer { + unsigned_transfer: unsigned(), + signature: hex::encode([0x33; SIGNATURE_LENGTH]), + }; + let bytes = transfer.to_bytes().unwrap(); + assert_eq!(bytes.len(), TRANSFER_BYTES); + assert_eq!(SignedTransfer::from_bytes(&bytes).unwrap(), transfer); + } +} diff --git a/core/src/wallet.rs b/core/src/wallet.rs new file mode 100644 index 0000000..5d97728 --- /dev/null +++ b/core/src/wallet.rs @@ -0,0 +1,338 @@ +use aes_gcm::aead::{Aead, KeyInit}; +use aes_gcm::{Aes256Gcm, Nonce}; +use falcon::FnDsaKeyPair; +use pbkdf2::pbkdf2_hmac; +use rand::{rngs::OsRng, RngCore}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use zeroize::Zeroize; + +use crate::address; +use crate::crypto; +use crate::error::BrowserCoreError; +use crate::legacy_wallet; +use crate::{PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH}; + +const KDF_ITERATIONS: u32 = 310_000; +const REGISTRATION_COMMAND: u8 = 38; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BrowserWallet { + pub version: u8, + pub network: String, + pub short_address: String, + pub vanity_address: Option, + pub public_key: String, + pub salt: String, + pub iterations: u32, + pub nonce: String, + pub encrypted_private_key: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LegacySavedWallet { + pub short_address: String, + pub vanity_address: Option, + pub public_key: String, + pub private_key: String, +} + +#[derive(Debug, Serialize)] +pub struct UnlockedWallet { + pub short_address: String, + pub vanity_address: Option, + pub public_key: String, + pub private_key: String, +} + +#[derive(Debug, Serialize)] +pub struct RegistrationPayload { + pub command: u8, + pub short_address: String, + pub public_key: String, + pub hash: String, + pub signature: String, + pub payload_hex: String, +} + +pub fn generate(password: &str) -> Result { + let (public_key, mut private_key) = generate_keypair()?; + let wallet = encrypt_wallet(&public_key, &private_key, None, password); + private_key.zeroize(); + wallet +} + +pub fn unlock(wallet: &BrowserWallet, password: &str) -> Result { + if wallet.version != 1 || wallet.iterations < 100_000 { + return Err(BrowserCoreError::InvalidRequest( + "unsupported browser wallet format".into(), + )); + } + let salt = decode_length(&wallet.salt, 32, "wallet salt")?; + let nonce = decode_length(&wallet.nonce, 12, "wallet nonce")?; + let ciphertext = hex::decode(&wallet.encrypted_private_key) + .map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + let key = derive_key(password, &salt, wallet.iterations); + let private_key = Aes256Gcm::new_from_slice(&key) + .unwrap() + .decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref()) + .map_err(|_| { + BrowserCoreError::InvalidRequest("wallet password or integrity check failed".into()) + })?; + validate_key_material(&private_key, &wallet.public_key, &wallet.short_address)?; + Ok(UnlockedWallet { + short_address: wallet.short_address.clone(), + vanity_address: wallet.vanity_address.clone(), + public_key: wallet.public_key.clone(), + private_key: hex::encode(private_key), + }) +} + +pub fn import_legacy( + legacy: &LegacySavedWallet, + legacy_password: &str, + browser_password: &str, +) -> Result { + let private_key_hex = legacy_wallet::decrypt_private_key(&legacy.private_key, legacy_password)?; + let mut private_key = + hex::decode(&private_key_hex).map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + validate_key_material(&private_key, &legacy.public_key, &legacy.short_address)?; + let public_key = + hex::decode(&legacy.public_key).map_err(|_| BrowserCoreError::InvalidPublicKey)?; + let wallet = encrypt_wallet( + &public_key, + &private_key, + legacy.vanity_address.clone(), + browser_password, + ); + private_key.zeroize(); + wallet +} + +pub fn import_private_key_image( + image_base64: &str, + image_password: &str, + browser_password: &str, +) -> Result { + let mut private_key_hex = legacy_wallet::decrypt_private_key(image_base64, image_password)?; + let result = import_private_key(&private_key_hex, browser_password); + private_key_hex.zeroize(); + result +} + +pub fn import_private_key( + private_key_hex: &str, + browser_password: &str, +) -> Result { + let mut private_key = + hex::decode(private_key_hex).map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + if private_key.len() != PRIVATE_KEY_LENGTH { + private_key.zeroize(); + return Err(BrowserCoreError::InvalidPrivateKey); + } + let result = wallet_from_private_key(&private_key, browser_password); + private_key.zeroize(); + result +} + +pub fn export_legacy( + wallet: &BrowserWallet, + browser_password: &str, + legacy_password: &str, +) -> Result { + let mut unlocked = unlock(wallet, browser_password)?; + let image = legacy_wallet::encrypt_private_key(&unlocked.private_key, legacy_password)?; + unlocked.private_key.zeroize(); + Ok(LegacySavedWallet { + short_address: wallet.short_address.clone(), + vanity_address: wallet.vanity_address.clone(), + public_key: wallet.public_key.clone(), + private_key: image, + }) +} + +pub fn registration( + wallet: &BrowserWallet, + password: &str, +) -> Result { + let mut unlocked = unlock(wallet, password)?; + let result = registration_from_keys( + &unlocked.short_address, + &unlocked.public_key, + &unlocked.private_key, + ); + unlocked.private_key.zeroize(); + result +} + +pub fn registration_from_keys( + short_address: &str, + public_key_hex: &str, + private_key_hex: &str, +) -> Result { + let address_bytes = address::short_address_to_bytes(short_address)?; + let public_key = decode_length(public_key_hex, PUBLIC_KEY_LENGTH, "public key")?; + let mut signed_payload = Vec::with_capacity(1 + 22 + PUBLIC_KEY_LENGTH); + signed_payload.push(REGISTRATION_COMMAND); + signed_payload.extend_from_slice(&address_bytes); + signed_payload.extend_from_slice(&public_key); + let hash = crypto::skein_256(&signed_payload); + let private_key = + hex::decode(private_key_hex).map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + validate_key_material(&private_key, public_key_hex, short_address)?; + let signature = crypto::sign(&hash, &private_key)?; + let mut payload = signed_payload.clone(); + payload.extend_from_slice(&signature); + Ok(RegistrationPayload { + command: REGISTRATION_COMMAND, + short_address: short_address.to_owned(), + public_key: public_key_hex.to_owned(), + hash: hex::encode(hash), + signature: hex::encode(signature), + payload_hex: hex::encode(payload), + }) +} + +pub fn handshake_proof( + public_key_hex: &str, + private_key_hex: &str, +) -> Result { + let public_key = decode_length(public_key_hex, PUBLIC_KEY_LENGTH, "public key")?; + let private_key = + hex::decode(private_key_hex).map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + let keypair = FnDsaKeyPair::from_private_key(&private_key) + .map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + if keypair.public_key() != public_key { + return Err(BrowserCoreError::InvalidRequest( + "wallet public key does not match its private key".into(), + )); + } + Ok(hex::encode(crypto::sign( + &crypto::skein_256(b"aced"), + &private_key, + )?)) +} + +fn generate_keypair() -> Result<(Vec, Vec), BrowserCoreError> { + loop { + let keypair = FnDsaKeyPair::generate(9).map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + let public_key = keypair.public_key().to_vec(); + if address::valid_public_key(&public_key) { + return Ok((public_key, keypair.private_key().to_vec())); + } + } +} + +fn wallet_from_private_key( + private_key: &[u8], + password: &str, +) -> Result { + let keypair = FnDsaKeyPair::from_private_key(private_key) + .map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + let public_key = keypair.public_key(); + address::public_key_to_short_address(public_key)?; + encrypt_wallet(public_key, private_key, None, password) +} + +fn encrypt_wallet( + public_key: &[u8], + private_key: &[u8], + vanity_address: Option, + password: &str, +) -> Result { + let short_address = address::public_key_to_short_address(public_key)?; + let mut salt = [0u8; 32]; + let mut nonce = [0u8; 12]; + OsRng.fill_bytes(&mut salt); + OsRng.fill_bytes(&mut nonce); + let mut key = derive_key(password, &salt, KDF_ITERATIONS); + let ciphertext = Aes256Gcm::new_from_slice(&key) + .unwrap() + .encrypt(Nonce::from_slice(&nonce), private_key) + .map_err(|_| BrowserCoreError::InvalidRequest("wallet encryption failed".into()))?; + key.zeroize(); + Ok(BrowserWallet { + version: 1, + network: address::network_suffix().into(), + short_address, + vanity_address, + public_key: hex::encode(public_key), + salt: hex::encode(salt), + iterations: KDF_ITERATIONS, + nonce: hex::encode(nonce), + encrypted_private_key: hex::encode(ciphertext), + }) +} + +fn derive_key(password: &str, salt: &[u8], iterations: u32) -> [u8; 32] { + let mut key = [0u8; 32]; + pbkdf2_hmac::(password.as_bytes(), salt, iterations, &mut key); + key +} + +fn validate_key_material( + private_key: &[u8], + public_key_hex: &str, + short_address: &str, +) -> Result<(), BrowserCoreError> { + if private_key.len() != PRIVATE_KEY_LENGTH { + return Err(BrowserCoreError::InvalidPrivateKey); + } + let keypair = FnDsaKeyPair::from_private_key(private_key) + .map_err(|_| BrowserCoreError::InvalidPrivateKey)?; + let public_key = keypair.public_key(); + if public_key.len() != PUBLIC_KEY_LENGTH || hex::encode(public_key) != public_key_hex { + return Err(BrowserCoreError::InvalidRequest( + "wallet public key does not match its private key".into(), + )); + } + if address::public_key_to_short_address(public_key)? != short_address { + return Err(BrowserCoreError::InvalidRequest( + "wallet address does not match its private key".into(), + )); + } + Ok(()) +} + +fn decode_length(value: &str, length: usize, name: &str) -> Result, BrowserCoreError> { + let bytes = hex::decode(value) + .map_err(|_| BrowserCoreError::InvalidRequest(format!("{name} is not hexadecimal")))?; + if bytes.len() != length { + return Err(BrowserCoreError::InvalidRequest(format!( + "{name} has an invalid length" + ))); + } + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::{export_legacy, generate, import_private_key, import_private_key_image, unlock}; + + #[test] + fn private_key_import_recreates_the_same_wallet() { + let original = generate("original password").unwrap(); + let unlocked = unlock(&original, "original password").unwrap(); + let imported = import_private_key(&unlocked.private_key, "new password").unwrap(); + let imported_unlocked = unlock(&imported, "new password").unwrap(); + + assert_eq!(imported.short_address, original.short_address); + assert_eq!(imported.public_key, original.public_key); + assert_eq!(imported_unlocked.private_key, unlocked.private_key); + } + + #[test] + fn private_key_image_import_recreates_the_same_wallet() { + let original = generate("browser password").unwrap(); + let legacy = export_legacy(&original, "browser password", "image password").unwrap(); + let imported = + import_private_key_image(&legacy.private_key, "image password", "new password") + .unwrap(); + + assert_eq!(imported.short_address, original.short_address); + assert_eq!(imported.public_key, original.public_key); + assert!(unlock(&imported, "new password").is_ok()); + } +} diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000..b359f97 --- /dev/null +++ b/extension/README.md @@ -0,0 +1,206 @@ +# Contractless Browser Wallet + +This is the WebExtension application for the Contractless Web3 wallet. It uses +the repository's `core` package for wallet generation, encryption, +transaction inspection, signing, message proofs, and wallet registration. + +The private key is encrypted in extension-local storage. Decrypted key +material exists only in the background service worker while the wallet is +unlocked and is cleared when the wallet locks or the worker is suspended. + +## Requirements + +Install: + +- Rust and Cargo +- `wasm-pack` +- Node.js 20 or later +- pnpm + +Install the JavaScript dependencies from this directory: + +```text +cargo install wasm-pack +pnpm install +``` + +## Build + +Build both browser packages: + +```text +pnpm run build +``` + +This compiles separate testnet and mainnet versions of the Rust core to +WebAssembly and creates: + +```text +dist/chrome/ +dist/firefox/ +``` + +Build only one browser when developing: + +```text +pnpm run build:chrome +pnpm run build:firefox +``` + +Chrome and Firefox use the same TypeScript, HTML, CSS, SDK provider, and two +network-specific WebAssembly cores. Each output receives its own manifest because Chrome uses a +Manifest V3 background service worker while Firefox uses a Manifest V3 +background event script. + +### Load in Chrome + +Open `chrome://extensions`, enable **Developer mode**, select **Load +unpacked**, and choose `dist/chrome`. + +### Load in Firefox + +Open `about:debugging`, select **This Firefox**, select **Load Temporary +Add-on**, and choose `dist/firefox/manifest.json`. + +The Firefox build requires Firefox 149 or later. Firefox 149 removed the user +gesture restriction from `action.openPopup`, allowing website connection and +transaction requests to open the wallet approval interface automatically in +the same way as the Chrome build. + +Run Mozilla's validator after building Firefox: + +```text +pnpm run lint:firefox +``` + +Create the Firefox submission archive: + +```text +pnpm run package:firefox +``` + +The archive is written to `web-ext-artifacts`. Firefox requires Mozilla signing +before the packaged extension can be installed permanently in standard +Firefox releases. + +Safari requires conversion through Apple's Safari Web Extension tooling and +Xcode. + +## Networks + +The wallet presents a Testnet/Mainnet selector before wallet creation and +unlock. Addresses, active wallets, WebAssembly transaction codecs, API URLs, +and website sessions are isolated by the selected network. Switching networks +locks the wallet and expires all application access keys. + +Testnet uses `.cltc` addresses and defaults to +`https://api.contractless.dev`. Mainnet uses `.clc` addresses and has no API +configured by default. A mainnet wallet may still be created, imported, and +backed up while that endpoint is empty; network lookups, registration, and +transaction broadcasts remain unavailable until a mainnet API is configured. + +## Provider + +Websites receive `window.contractless` with a Promise-based `request` method. +Applications should normally use the downloadable client in +`../sdk/contractless.js` instead of calling the provider directly: + +```js +import { ContractlessWallet } from "./contractless.js"; + +const wallet = new ContractlessWallet({ + appId: "example-application" +}); + +const connection = await wallet.connect({ + message: "Sign in to Example Application" +}); +``` + +Transaction and message requests are routed through the background worker and +wallet approval screen. Websites never receive wallet private keys or +unrestricted raw-signing access. Single-signer transactions are signed and +broadcast by the wallet. The transaction ID and signed bytes are returned only +after the configured API accepts the broadcast. + +Supported provider methods are: + +- `contractless_detect` +- `contractless_connect` +- `contractless_accounts` +- `contractless_balances` +- `contractless_disconnect` +- `contractless_signMessage` +- `contractless_sendTransaction` + +Connection, message signing, and transaction submission wait for an explicit +approval or rejection. A successful connection creates a random 256-bit +access key bound to the browser-derived origin, stable application ID, active +wallet, and current wallet unlock session. + +The website stores its copy in browser `sessionStorage`. The extension stores +the matching authorization in extension session storage. The key is reusable +until the wallet locks, changes active wallets, or the application +disconnects. It is not a permanent connected-site permission. + +Every authorized request includes the claimed origin, application ID, and +access key. The extension independently derives the real origin from the +browser message sender and rejects mismatches. + +See `../sdk/README.md` for complete website integration examples. + +## Wallet registration + +The **Register wallet** button creates the command-38 registration signature +and the fixed API handshake proof locally. It sends the address, public key, +and signatures to: + +```text +POST /api/v1/addresses/register +``` + +The private key and wallet encryption key never leave the extension. The API +requires the authenticated address and public key to match the registration +being submitted before forwarding it to a Contractless node. + +The API operator must include the installed extension origin in +`API_CORS_ORIGINS`, for example: + +```text +API_CORS_ORIGINS=chrome-extension://extension-id +``` + +Keep any existing allowed website origins in the comma-separated list. +Changing the API endpoint in the wallet prompts the user for permission to +contact that HTTPS origin. + +Firefox extension pages use a `moz-extension://` origin. The production API +must allow the installed Firefox extension to make the same wallet requests as +the Chrome build. + +## Firefox Submission Source + +Mozilla reviewers must receive the readable source because Vite bundles the +TypeScript and `wasm-pack` generates the WebAssembly package. Submit the built +Firefox archive as the add-on and a separate source archive containing at +least: + +```text +core/ +extension/src/ +extension/package.json +extension/pnpm-lock.yaml +extension/pnpm-workspace.yaml +extension/tsconfig.json +extension/vite.config.ts +README.md +``` + +Do not include `extension/node_modules`, `extension/dist`, or `core/target` in +the source archive. Reviewers can reproduce the Firefox build by entering the +`extension` directory and running: + +```text +pnpm install --frozen-lockfile +pnpm run build:firefox +``` diff --git a/extension/dist/assets/contractless-logo-BgfMS-Kf.png b/extension/dist/assets/contractless-logo-BgfMS-Kf.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/dist/assets/contractless-logo-BgfMS-Kf.png differ diff --git a/extension/dist/assets/contractless_browser_core_bg-C_vtp2Os.wasm b/extension/dist/assets/contractless_browser_core_bg-C_vtp2Os.wasm new file mode 100644 index 0000000..7264d84 Binary files /dev/null and b/extension/dist/assets/contractless_browser_core_bg-C_vtp2Os.wasm differ diff --git a/extension/dist/assets/popup-C5oUQoSX.css b/extension/dist/assets/popup-C5oUQoSX.css new file mode 100644 index 0000000..e5133e2 --- /dev/null +++ b/extension/dist/assets/popup-C5oUQoSX.css @@ -0,0 +1 @@ +:root{--blue: #2167a5;--blue-deep: #174d7c;--ink: #13202a;--muted-white: rgb(255 255 255 / 90%);--danger: #b9382e;--danger-soft: #fff0ee;--success: #1d8054;--dark: #0c151d;--surface: #111e28;--surface-raised: #172936;--surface-active: #1b3547;--dark-line: #314957;--dark-muted: #c4d0d7;--accent: #55c5dc;font:14px/1.45 Arial,Helvetica,sans-serif}*{box-sizing:border-box}body{width:390px;min-width:390px;min-height:600px;margin:0;background:var(--blue);color:#fff}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}.wallet-shell{position:relative;width:390px;height:600px;overflow:hidden;background:var(--blue)}.screen{width:100%;height:100%;background:var(--blue)}.screen[hidden]{display:none}.wallet-header{height:72px;display:flex;align-items:center;gap:11px;padding:0 20px;border-bottom:1px solid rgb(255 255 255 / 70%)}.wallet-header img{width:37px;filter:brightness(0) invert(1)}.wallet-header div{display:grid}.wallet-header strong{font-size:18px}.wallet-header small{color:var(--muted-white);font-size:14px}.wallet-header>span{margin-left:auto;font-size:13px;font-weight:700}.screen-content{padding:28px 24px 22px}.screen-content.compact{padding-top:18px}.eyebrow{font-size:11px;font-weight:700;text-transform:uppercase}h1{margin:7px 0 9px;font-size:27px;line-height:1.12;letter-spacing:0}.intro{margin:0 0 24px;color:var(--muted-white);line-height:1.5}label{display:block;margin-bottom:8px;font-size:12px;font-weight:700}.input-row{height:49px;display:grid;grid-template-columns:1fr auto;border:1px solid white;background:#fff}.input-row:focus-within,textarea:focus{outline:2px solid rgb(255 255 255 / 28%);outline-offset:2px}.input-row input{min-width:0;border:0;padding:0 13px;color:var(--ink);outline:0}.reveal{border:0;border-left:1px solid #d9e0e5;padding:0 13px;background:#fff;color:var(--blue);font-size:11px;font-weight:700}textarea{width:100%;min-height:82px;margin-bottom:13px;resize:none;border:1px solid white;padding:11px 13px;color:var(--ink);outline:0}.note{display:grid;gap:2px;margin:18px 0 23px;border-left:3px solid var(--accent);padding:13px;background:var(--surface-raised);color:#fff;font-size:12px}.note span{color:var(--dark-muted)}.compact-note{margin:12px 0 14px;padding:10px 13px}.warning{margin:18px 0 25px;border-left:3px solid var(--danger);padding:13px;background:#321c20;color:#ffbbb5;font-size:12px;line-height:1.4}.warning strong{display:block}.primary,.secondary{width:100%;min-height:48px;display:flex;align-items:center;justify-content:center;gap:10px;border:1px solid var(--accent);background:var(--accent);color:#08202b;font-weight:700}.primary span{font-size:18px}.secondary{min-height:42px;margin-top:9px;background:transparent;color:#dcecf3;font-size:12px}.secondary:disabled{border-color:#ffffff59;color:#ffffff7f}.text-button{width:100%;margin-top:8px;border:0;padding:8px;background:transparent;color:var(--muted-white)}.divider{display:flex;align-items:center;gap:10px;margin:17px 0 12px;color:#ffffffad;font-size:12px;text-transform:uppercase}.divider:before,.divider:after{height:1px;flex:1;background:#ffffff4d;content:""}.file-heading{margin-top:20px}.file-picker{min-height:86px;display:flex;align-items:center;gap:13px;margin-bottom:24px;border:1px dashed rgb(255 255 255 / 72%);padding:14px;background:#ffffff1a;cursor:pointer}.file-picker>span:last-child{display:grid;gap:4px}.file-picker small{color:#ffffffad;font-weight:400}.file-icon{width:34px;height:34px;display:grid;flex:0 0 34px;place-items:center;border:1px solid white;font-size:22px;font-weight:400}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}.loading-screen{position:relative}.loading-brand{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-content:center;justify-items:center;gap:34px}.loading-brand img,.large-logo{width:148px;filter:brightness(0) invert(1)}.spinner{width:28px;aspect-ratio:1;border:3px solid rgb(255 255 255 / 28%);border-top-color:#fff;border-radius:50%;animation:spin .85s linear infinite}.loading-screen>p{position:absolute;bottom:35px;width:100%;margin:0;color:#ffffffb8;font-size:12px;text-align:center;text-transform:uppercase}@keyframes spin{to{transform:rotate(360deg)}}.success-mark{width:38px;height:38px;display:grid;place-items:center;border-radius:50%;border:1px solid #55c98a;background:#123727;color:#66dfa0;font-size:22px;font-weight:700}.image-preview{position:relative;height:142px;margin:18px 0;overflow:hidden;border:1px solid var(--dark-line);background:var(--surface)}.image-preview img{width:100%;height:100%;display:block;object-fit:contain}.confirmation{display:grid;grid-template-columns:18px 1fr;gap:9px;align-items:start;margin:14px 0;color:#ffffffd6;font-size:11px;line-height:1.35}.confirmation input{width:16px;height:16px;margin:0}.centered-content{height:100%;display:flex;flex-direction:column;justify-content:center;padding:36px 28px;text-align:center}.centered-content{align-items:stretch}.centered-content .large-logo{align-self:center;width:92px;margin-bottom:25px}.centered-content h1{text-align:center}.centered-content .primary{margin-top:22px}.dashboard-header{height:66px}.account-button{min-width:105px;max-width:132px;height:34px;display:flex;align-items:center;gap:7px;margin-left:auto;overflow:hidden;border:1px solid rgb(255 255 255 / 45%);padding:0 10px;background:transparent;color:#fff;font-size:14px}.account-button>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.network-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:#63d18d}.assets-dashboard{height:calc(100% - 66px);overflow:hidden}.portfolio-balance{padding:22px 22px 19px;border-bottom:1px solid rgb(255 255 255 / 24%);background:var(--blue-deep)}.portfolio-balance>span{color:var(--muted-white);font-size:14px;text-transform:uppercase}.portfolio-balance>strong{display:block;margin-top:4px;font-size:31px;line-height:1.2}.portfolio-balance>strong small{font-size:13px}.portfolio-balance p{max-width:100%;margin:7px 0 0;overflow:hidden;color:#ffffffd1;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.wallet-actions{height:88px;display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid rgb(255 255 255 / 24%)}.wallet-actions button{display:grid;place-items:center;align-content:center;gap:5px;border:0;border-right:1px solid rgb(255 255 255 / 18%);background:transparent;color:#fff;font-size:14px;font-weight:700}.wallet-actions button:last-child{border-right:0}.action-icon{width:29px;height:29px;display:grid;place-items:center;border:1px solid rgb(255 255 255 / 65%);font-size:16px;font-weight:400}.asset-ledger{height:calc(100% - 209px);background:var(--dark);color:#fff}.asset-filter{height:42px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--dark-line);padding:5px 19px;background:#0f1a22}.asset-filter button{border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dark-muted);font-size:14px;font-weight:700}.asset-filter button.active{border-bottom-color:var(--accent);color:#fff}.asset-ledger-heading{height:45px;display:flex;align-items:center;justify-content:space-between;padding:0 19px;border-bottom:1px solid var(--dark-line)}.asset-ledger-heading h1{margin:0;font-size:19px}.asset-ledger-heading button{width:30px;height:30px;border:0;background:transparent;color:var(--accent);font-size:17px}.asset-row{min-height:66px;display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:11px;align-items:center;padding:10px 19px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.asset-symbol{width:36px;height:36px;display:grid;place-items:center;background:var(--blue);color:#fff;font-size:12px;font-weight:700}.contractless-symbol img{width:25px;filter:brightness(0) invert(1)}.asset-name,.asset-value{min-width:0;display:grid;gap:2px}.asset-name strong,.asset-value strong{overflow:hidden;font-size:16px;text-overflow:ellipsis;white-space:nowrap}.asset-name span,.asset-value span{color:var(--dark-muted);font-size:13px}.asset-value{max-width:125px;text-align:right}.token-list{max-height:calc(100% - 148px);overflow-y:auto}.empty-assets{display:grid;gap:5px;padding:27px 20px;color:var(--dark-muted);text-align:center}.empty-assets strong{color:#dce8ee;font-size:16px}.empty-assets span{font-size:14px}.receive-screen{background:var(--dark)}.receive-screen .wallet-header{background:var(--blue)}.send-screen,.send-review-screen,.send-success-screen{background:var(--dark);color:#fff}.send-screen .wallet-header,.send-review-screen .wallet-header,.send-success-screen .wallet-header{background:var(--blue)}.send-content,.send-review-content,.send-success-content{height:calc(100% - 72px);padding:22px;overflow-y:auto}.send-content{display:grid;align-content:start;gap:17px}.send-content label{color:#fff;font-size:14px}.send-content input,.send-content select{width:100%;min-height:48px;border:1px solid var(--dark-line);padding:0 13px;background:var(--surface);color:#fff;outline:0}.send-content input:focus,.send-content select:focus{border-color:var(--accent)}.send-amount-row{display:grid;grid-template-columns:minmax(0,1fr) auto}.send-amount-row input{border-right:0}.send-amount-row>span{min-width:64px;display:grid;place-items:center;border:1px solid var(--dark-line);background:var(--surface-raised);color:var(--accent);font-weight:700}.field-note{display:block;margin-top:7px;color:var(--dark-muted);font-size:13px}.send-continue{margin-top:5px}.send-review-content{padding:0 22px 24px}.send-review-amount{margin:0 -22px 20px;padding:24px 22px;background:var(--blue-deep)}.send-review-amount>span{color:var(--muted-white);font-size:13px;text-transform:uppercase}.send-review-amount strong{display:block;margin-top:5px;font-size:29px}.send-review-amount small{color:var(--accent);font-size:15px}.send-review-details{margin:0 0 18px}.send-review-details>div{padding:12px 0;border-bottom:1px solid var(--dark-line)}.send-review-details dt{margin-bottom:4px;color:var(--dark-muted);font-size:13px}.send-review-details dd{margin:0;overflow-wrap:anywhere;color:#fff;font-weight:700}.signing-notice{margin:18px 0;padding-left:12px;border-left:3px solid var(--accent);color:var(--dark-muted)}.send-success-content{text-align:center}.send-success-content p{color:var(--dark-muted)}.send-txid{margin:24px 0 14px;padding:14px;border:1px solid var(--dark-line);background:var(--surface);text-align:left}.send-txid span{display:block;margin-bottom:5px;color:var(--dark-muted);font-size:13px}.send-txid strong{overflow-wrap:anywhere}.send-success-content .secondary{margin-bottom:10px}.receive-content{height:calc(100% - 72px);overflow-y:auto;padding:25px 24px 22px;text-align:center}.receive-label{color:var(--accent);font-size:12px;font-weight:700;text-transform:uppercase}.receive-content h1{margin-top:8px}.receive-content>p{margin:0 auto 20px;color:var(--dark-muted);font-size:11px;line-height:1.45}.qr-frame{width:238px;height:238px;display:grid;place-items:center;margin:0 auto 19px;border:1px solid #708693;padding:8px;background:#fff}.qr-frame canvas{width:220px;height:220px;display:block}.receive-address{display:grid;gap:6px;border:1px solid var(--dark-line);padding:13px;background:var(--surface);text-align:left}.receive-address span{color:var(--dark-muted);font-size:11px;font-weight:700;text-transform:uppercase}.receive-address strong{overflow-wrap:anywhere;color:#e5f0f5;font:12px/1.4 Consolas,monospace}.receive-copy{margin-top:13px}.nfts-screen,.nft-detail-screen{background:var(--dark);color:#fff}.nfts-screen .wallet-header,.nft-detail-screen .wallet-header{background:var(--blue)}.header-refresh{width:34px;height:34px;display:grid;place-items:center;margin-left:auto;border:1px solid rgb(255 255 255 / 70%);background:transparent;color:#fff;font-size:18px}.nfts-content,.nft-detail-content{height:calc(100% - 72px);overflow-y:auto}.nfts-summary{min-height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:#0f1a22;color:var(--dark-muted);font-size:13px;font-weight:700;text-transform:uppercase}.nfts-summary span:first-child{color:var(--accent)}.nfts-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;padding:1px;background:var(--dark-line)}.nft-card{min-width:0;display:grid;align-content:start;border:0;padding:0;background:var(--surface);color:#fff;text-align:left}.nft-card:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.nft-card-media{width:100%;aspect-ratio:1;display:grid;place-items:center;overflow:hidden;background:#17303e;color:var(--accent);font-size:28px;font-weight:700}.nft-card-media img{width:100%;height:100%;display:block;object-fit:cover}.nft-card-identity{min-height:116px;display:grid;align-content:start;gap:3px;padding:11px 12px 13px}.nft-card-identity>span{color:var(--accent);font-size:11px;font-weight:700}.nft-card-identity strong{overflow:hidden;font-size:15px;text-overflow:ellipsis;white-space:nowrap}.nft-card-identity small{overflow:hidden;color:var(--dark-muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.nft-card-identity b{margin-top:5px;color:#fff;font-size:12px}.nft-card-identity em{color:#e9c667;font-size:11px;font-style:normal}.nfts-empty{grid-column:1 / -1;min-height:300px;display:grid;place-content:center;gap:6px;padding:30px;background:var(--surface);color:var(--dark-muted);text-align:center}.nfts-empty strong{color:#fff;font-size:17px}.nfts-pagination{min-height:52px;display:grid;grid-template-columns:42px 1fr 42px;align-items:center;gap:10px;padding:7px 14px;border-top:1px solid var(--dark-line);background:#0f1a22;text-align:center}.nfts-pagination[hidden]{display:none}.nfts-pagination button{width:38px;height:38px;border:1px solid var(--dark-line);background:var(--surface);color:var(--accent);font-size:17px}.nfts-pagination button:disabled{color:#61727c}.nfts-pagination span{color:var(--dark-muted);font-size:13px}.nft-detail-media{width:100%;aspect-ratio:16 / 11;display:grid;place-items:center;overflow:hidden;border-bottom:1px solid var(--dark-line);background:#142934}.nft-detail-media img{width:100%;height:100%;display:block;object-fit:contain}.nft-detail-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:15px;align-items:end;padding:18px 20px 14px}.nft-detail-heading>div{min-width:0}.nft-detail-heading span{color:var(--accent);font-size:11px;font-weight:700}.nft-detail-heading h1{overflow-wrap:anywhere;margin:4px 0 0;font-size:23px}.nft-detail-heading>strong{color:#63d18d;font-size:13px;text-align:right}.nft-detail-description{margin:0;padding:0 20px 17px;color:var(--dark-muted);font-size:14px;line-height:1.5}.nft-detail-attributes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;padding:0 20px 18px}.nft-detail-attributes[hidden]{display:none}.nft-detail-attributes>div{min-width:0;display:grid;gap:2px;border:1px solid var(--dark-line);padding:9px;background:var(--surface-raised)}.nft-detail-attributes span{overflow:hidden;color:var(--dark-muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.nft-detail-attributes strong{overflow-wrap:anywhere;font-size:13px}.nft-detail-fields{margin:0;border-top:1px solid var(--dark-line)}.nft-detail-fields>div{display:grid;gap:4px;padding:12px 20px;border-bottom:1px solid var(--dark-line)}.nft-detail-fields dt{color:var(--dark-muted);font-size:12px}.nft-detail-fields dd{margin:0;overflow-wrap:anywhere;color:#fff;font:12px/1.45 Consolas,monospace}.nft-detail-actions{padding:18px 20px 0}.nft-provenance{padding:18px 20px 25px}.nft-provenance h2{margin:0 0 11px;font-size:16px}.nft-provenance p{color:var(--dark-muted)}.nft-history-row{width:100%;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;border:0;padding:10px 0;border-bottom:1px solid var(--dark-line);background:transparent;color:#fff;text-align:left}.nft-history-row:hover,.nft-history-row:focus-visible{background:var(--surface-raised)}.nft-history-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.nft-history-row strong{font-size:13px}.nft-history-row span{color:var(--dark-muted);font-size:12px;text-align:right}.nft-history-row small{grid-column:1 / -1;color:var(--accent);font:11px Consolas,monospace}.activity-screen{background:var(--dark);color:#fff}.activity-screen .wallet-header{background:var(--blue)}.activity-content{height:calc(100% - 72px);overflow-y:auto}.activity-summary{min-height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:#0f1a22;color:var(--dark-muted);font-size:13px;text-transform:uppercase}.activity-summary strong{color:var(--accent);font-size:13px}.activity-summary>div{display:flex;align-items:center;gap:8px}.activity-summary button{width:30px;height:30px;border:1px solid var(--dark-line);background:transparent;color:var(--accent);font-size:17px}.activity-list{background:var(--surface)}.activity-row{width:100%;min-height:98px;display:grid;grid-template-columns:31px minmax(0,1fr) auto;gap:10px;align-items:center;padding:11px 17px;border-bottom:1px solid var(--dark-line);border-top:0;border-right:0;border-left:0;background:transparent;color:#fff;text-align:left}.activity-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.activity-marker{width:29px;height:29px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:13px;font-weight:700}.activity-marker.incoming,.activity-status .incoming{color:#63d18d}.activity-marker.outgoing,.activity-status .outgoing{color:#fff}.activity-details,.activity-status{min-width:0;display:grid;gap:2px}.activity-details strong{overflow:hidden;font-size:16px;text-overflow:ellipsis;white-space:nowrap}.activity-details span{color:#c1d0d8;font:13px Consolas,monospace}.activity-details small,.activity-status span{color:var(--dark-muted);font-size:13px}.activity-status{max-width:132px;text-align:right}.activity-status strong{font-size:14px;white-space:nowrap}.activity-status .confirmed{color:#63d18d}.activity-status .pending{color:#e9c667}.activity-empty{display:grid;gap:6px;padding:42px 24px;color:var(--dark-muted);font-size:14px;text-align:center}.activity-empty strong{color:#dce8ee;font-size:16px}.activity-error{color:#ffaaa4}.transaction-screen{background:var(--dark);color:#fff}.transaction-screen .wallet-header{background:var(--blue)}.transaction-confirmed{color:#84e7aa}.transaction-pending{color:#f1d27b}.transaction-content{height:calc(100% - 72px);overflow-y:auto;padding-bottom:24px}.transaction-identity{display:grid;gap:13px;padding:18px;border-bottom:1px solid var(--dark-line);background:var(--surface-raised)}.transaction-identity>div{display:grid;gap:7px}.transaction-identity span,.transaction-field span{color:var(--dark-muted);font-size:12px;font-weight:700;text-transform:uppercase}.transaction-identity strong{overflow-wrap:anywhere;color:#fff;font:13px/1.55 Consolas,monospace}.transaction-identity button{min-height:40px;border:1px solid var(--accent);background:transparent;color:var(--accent);font-size:14px;font-weight:700}.transaction-section{border-top:1px solid var(--dark-line)}.transaction-section h2{margin:0;padding:16px 18px 10px;font-size:16px}.transaction-fields{display:grid}.transaction-field{display:grid;gap:6px;padding:13px 18px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.transaction-field strong{overflow-wrap:anywhere;color:#fff;font-size:15px;font-weight:600;line-height:1.45}.transaction-technical{border-top:1px solid var(--dark-line)}.transaction-technical summary{padding:17px 18px;color:var(--accent);font-size:15px;font-weight:700;cursor:pointer}.technical-field strong{font:12px/1.55 Consolas,monospace;word-break:break-all}.drawer-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#091a278a}.account-drawer{position:absolute;top:0;right:0;bottom:0;z-index:11;width:338px;display:flex;flex-direction:column;border-left:1px solid var(--dark-line);background:var(--dark);color:#fff;transform:translate(100%);transition:transform .18s ease-out}.account-drawer.open{transform:translate(0)}.drawer-header{height:72px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:var(--blue);color:#fff}.drawer-header>div{display:grid;gap:2px}.drawer-header strong{font-size:19px}.drawer-header span{color:var(--muted-white);font-size:14px}.drawer-header button{width:34px;height:34px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:23px;line-height:1}.drawer-section{max-height:244px;overflow-y:auto;padding:17px 15px 14px;border-bottom:1px solid var(--dark-line)}.drawer-label{display:block;margin:0 3px 8px;color:var(--dark-muted);font-size:13px;font-weight:700;text-transform:uppercase}.known-address{width:100%;min-height:58px;display:grid;grid-template-columns:minmax(0,1fr) 30px;gap:4px;align-items:center;border:1px solid var(--dark-line);padding:4px;background:var(--surface);color:#fff}.known-address+.known-address{margin-top:7px}.known-address.active-address{border-color:var(--accent);background:var(--surface-active)}.address-select{min-width:0;min-height:48px;display:grid;grid-template-columns:36px minmax(0,1fr) 20px;gap:10px;align-items:center;border:0;padding:5px 6px;background:transparent;color:#fff;text-align:left}.rename-address{width:28px;height:34px;border:0;background:transparent;color:var(--accent);font-size:16px}.address-avatar{width:34px;height:34px;display:grid;place-items:center;background:#204f70}.address-avatar img{width:24px;filter:brightness(0) invert(1)}.address-details{min-width:0;display:grid;gap:2px}.address-details strong{font-size:15px}.address-details small{overflow:hidden;color:var(--dark-muted);font-size:13px;text-overflow:ellipsis;white-space:nowrap}.selected-mark{color:var(--success);font-size:14px;font-weight:700}.drawer-navigation{display:grid;padding:8px 0}.drawer-navigation button{min-height:72px;display:grid;grid-template-columns:34px minmax(0,1fr) 16px;gap:10px;align-items:center;border:0;border-bottom:1px solid var(--dark-line);padding:9px 18px;background:transparent;color:#fff;text-align:left}.drawer-navigation button>span:nth-child(2){display:grid;gap:2px}.drawer-navigation strong{font-size:16px}.drawer-navigation small{color:var(--dark-muted);font-size:13px}.menu-icon{width:32px;height:32px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:15px;font-weight:700}.menu-arrow{color:var(--dark-muted);font-size:20px;text-align:right}.drawer-lock{min-height:46px;display:flex;align-items:center;justify-content:center;gap:8px;margin:auto 15px 15px;border:1px solid var(--dark-line);background:var(--surface-raised);color:#dce8ee;font-size:14px;font-weight:700}.header-back{width:34px;height:34px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:18px}.settings-screen{background:var(--dark);color:#fff}.settings-screen .wallet-header{background:var(--blue);color:#fff}.settings-content{height:calc(100% - 72px);overflow-y:auto;padding-bottom:20px}.settings-group{padding:18px 20px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.settings-heading{display:grid;gap:3px;margin-bottom:15px}.settings-heading strong{font-size:13px}.settings-heading span{color:var(--dark-muted);font-size:12px;line-height:1.4}.settings-group label{color:#b7c6ce;font-size:12px;text-transform:uppercase}.settings-group input,.settings-group select{width:100%;height:44px;border:1px solid #8699a5;padding:0 11px;background:#fff;color:var(--ink);outline:0}.settings-group input:focus,.settings-group select:focus{border-color:var(--blue);outline:2px solid rgb(33 103 165 / 14%)}.settings-save{width:calc(100% - 40px);margin:16px 20px;border-color:var(--accent);background:var(--accent);color:#08202b}.backup-settings{padding-right:0;padding-left:0}.backup-settings .settings-heading{padding:0 20px}.settings-action{width:100%;min-height:61px;display:grid;grid-template-columns:34px minmax(0,1fr) 16px;gap:10px;align-items:center;border:0;border-top:1px solid var(--dark-line);padding:9px 20px;background:transparent;color:#fff;text-align:left}.settings-action:last-child{border-bottom:1px solid var(--dark-line)}.settings-action>span:nth-child(2){display:grid;gap:2px}.settings-action strong{font-size:14px}.settings-action small{color:var(--dark-muted);font-size:11px}.settings-action>span:last-child{color:var(--dark-muted);font-size:20px}.settings-action-icon{width:32px;height:32px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:15px;font-weight:700}.private-key-result{margin:16px 20px 0;border:1px solid var(--dark-line);padding:13px;background:var(--surface-raised)}.private-key-result>div{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px}.private-key-result>div>strong{font-size:11px;text-transform:uppercase}.private-key-result>div>button{width:28px;height:28px;border:0;background:transparent;color:var(--dark-muted);font-size:19px}.private-key-result textarea{height:95px;min-height:95px;margin:0 0 10px;border:1px solid #8699a5;padding:9px;background:#fff;color:var(--ink);font-size:11px}.private-key-result .secondary{border-color:var(--accent);color:var(--accent)}.modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:30;background:#091a27ad}.backup-key-modal{position:absolute;top:50%;right:20px;left:20px;z-index:31;border:1px solid white;background:var(--blue);color:#fff;transform:translateY(-50%)}.backup-key-modal>header{min-height:65px;display:flex;align-items:center;justify-content:space-between;padding:12px 15px;border-bottom:1px solid rgb(255 255 255 / 40%)}.backup-key-modal>header>div{display:grid;gap:2px}.backup-key-modal>header strong{font-size:13px}.backup-key-modal>header span{color:var(--muted-white);font-size:11px}.backup-key-modal>header button{width:30px;height:30px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:20px}.backup-key-modal>div{padding:17px 15px}.backup-key-modal .primary{margin-top:16px}#notice{position:absolute;right:18px;bottom:14px;left:18px;z-index:40;margin:0;padding:10px 12px;border:1px solid #8d4448;border-left:3px solid #ff7168;background:#321c20;color:#ffc0bb;font-size:11px}#notice:empty{display:none}.provider-approval-screen{display:flex;flex-direction:column}.provider-approval-screen .screen-content{min-height:0;flex:1;overflow-y:auto;padding-bottom:18px}.approval-context{display:flex;align-items:center;justify-content:space-between;gap:12px}.approval-context .eyebrow{min-width:0;overflow-wrap:anywhere}.risk-label{flex:0 0 auto;padding:5px 8px;border:1px solid currentColor;font-size:11px;font-weight:700;text-transform:uppercase}.risk-safe{background:#123b2d;color:#63e5a9}.risk-low{background:#403612;color:#ffe16c}.risk-high{background:#481f24;color:#ff8b8b}.approval-details{display:grid;grid-template-columns:minmax(100px,.8fr) minmax(0,1.4fr);margin:20px 0;border:1px solid #315466;background:#091923}.approval-details dt,.approval-details dd{min-width:0;margin:0;padding:11px 12px;border-bottom:1px solid #203d4c;overflow-wrap:anywhere}.approval-details dt{color:#85dce8;font-size:12px;font-weight:700;text-transform:capitalize}.approval-details dd{color:#f5f9fb;font:13px/1.45 Arial,Helvetica,sans-serif;white-space:pre-wrap}.approval-details dt:last-of-type,.approval-details dd:last-of-type{border-bottom:0}.approval-actions{display:grid;flex:0 0 auto;grid-template-columns:1fr 1fr;gap:10px;padding:14px 24px 18px;border-top:1px solid rgb(255 255 255 / 35%);background:var(--blue-deep)}.approval-actions button{min-height:48px;margin:0}.approval-actions button.is-processing:before{width:15px;height:15px;content:"";border:2px solid currentcolor;border-right-color:transparent;border-radius:50%;animation:approval-spin .7s linear infinite}.approval-actions button:disabled{opacity:.72}@keyframes approval-spin{to{transform:rotate(360deg)}} diff --git a/extension/dist/assets/print-DTb91-bp.css b/extension/dist/assets/print-DTb91-bp.css new file mode 100644 index 0000000..b6c42aa --- /dev/null +++ b/extension/dist/assets/print-DTb91-bp.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{max-width:760px;margin:0 auto;padding:34px;color:#111820;font:14px/1.45 Arial,Helvetica,sans-serif}header{display:flex;align-items:center;gap:15px;padding-bottom:18px;border-bottom:3px solid #2167a5}header img{width:58px}h1{margin:0;font-size:24px}header p{margin:2px 0 0;color:#5c6972}.warning{margin:22px 0;border:2px solid #9b1c1c;padding:12px;color:#7a1111;font-weight:700}section{margin-top:18px}h2{margin:0 0 7px;color:#42515b;font-size:12px;text-transform:uppercase}pre{margin:0;overflow-wrap:anywhere;white-space:pre-wrap;border:1px solid #aebac2;padding:11px;font:10px/1.35 Consolas,monospace}.print-error{color:#9b1c1c;font-weight:700}@media print{body{max-width:none;padding:0}} diff --git a/extension/dist/background.js b/extension/dist/background.js new file mode 100644 index 0000000..d670490 --- /dev/null +++ b/extension/dist/background.js @@ -0,0 +1,3 @@ +import{c as yt}from"./chunks/core-CvwXoH-U.js";const F="contractlessWallet",M="contractlessWallets",V="contractlessSettings",L="contractlessAutoLock",K="contractlessUnlockedWallet",j="contractlessApplicationSessions",ct=12e4,vt=10,bt={apiUrl:"https://api.contractless.dev",autoLockMinutes:60};let s;const J="pending-provider-approvals";async function k(){const e=await chrome.storage.local.get(V);return{...bt,...e[V]??{}}}function lt(e){const t=JSON.parse(e);if(typeof t.short_address!="string"||typeof t.public_key!="string")throw new Error("The encrypted wallet metadata is invalid.");return{address:t.short_address,publicKey:t.public_key}}async function $(){const e=await chrome.storage.local.get([M,F]),t=e[M];if((t==null?void 0:t.version)===1&&Array.isArray(t.wallets))return t;const a=e[F];if(typeof a!="string")return{version:1,wallets:[]};const i=lt(a),d={id:crypto.randomUUID(),label:"Contractless Account 1",address:i.address,publicKey:i.publicKey,walletJson:a},l={version:1,activeWalletId:d.id,wallets:[d]};return await chrome.storage.local.set({[M]:l}),await chrome.storage.local.remove(F),l}async function H(e){await chrome.storage.local.set({[M]:e})}function P(e){return e.wallets.find(t=>t.id===e.activeWalletId)??e.wallets[0]}async function X(e){const t=await $(),a=lt(e),i=t.wallets.find(l=>l.address===a.address);if(i)return i.walletJson=e,i.publicKey=a.publicKey,t.activeWalletId=i.id,await H(t),i;const d={id:crypto.randomUUID(),label:`Contractless Account ${t.wallets.length+1}`,address:a.address,publicKey:a.publicKey,walletJson:e};return t.wallets.push(d),t.activeWalletId=d.id,await H(t),d}async function E(){await dt();const e=await $(),t=P(e),a=await k();return{exists:e.wallets.length>0,unlocked:!!(s&&s.walletId===(t==null?void 0:t.id)),activeWalletId:t==null?void 0:t.id,activeWalletLabel:t==null?void 0:t.label,address:s==null?void 0:s.short_address,publicKey:s==null?void 0:s.public_key,wallets:e.wallets.map(i=>({id:i.id,label:i.label,address:i.address,publicKey:i.publicKey,active:i.id===(t==null?void 0:t.id)})),apiUrl:a.apiUrl,autoLockMinutes:a.autoLockMinutes}}async function b(){if(!s)return;const e=await k();s.expiresAt=Date.now()+e.autoLockMinutes*6e4,await chrome.storage.session.set({[K]:s}),await chrome.alarms.clear(L),await chrome.alarms.create(L,{when:s.expiresAt})}function kt(e,t){const a=new Uint8Array(e);let i="";const d=32768;for(let l=0;l{});if(!l.ok||(f==null?void 0:f.success)!==!0)throw new Error((f==null?void 0:f.error)??`Wallet registration failed with HTTP ${l.status}.`);return f.data}async function St(e,t){var f;const a=await k(),i=e.create_handshake_proof(t.public_key,t.private_key),d=await fetch(`${a.apiUrl}/api/v1/addresses/registration?address=${encodeURIComponent(t.short_address)}`,{headers:{"X-Contractless-Address":t.short_address,"X-Contractless-Public-Key":t.public_key,"X-Contractless-Signature":i}}),l=await d.json().catch(()=>{});if(!d.ok||(l==null?void 0:l.success)!==!0)throw new Error((l==null?void 0:l.error)??`Wallet registration lookup failed with HTTP ${d.status}.`);((f=l.data)==null?void 0:f.registered)!==!0&&await Y(e,t)}async function dt(){if(s){if(s.expiresAt>Date.now())return;await R();return}const t=(await chrome.storage.session.get(K))[K];if(!t||t.expiresAt<=Date.now()){await chrome.storage.session.remove(K);return}t.sessionId||(t.sessionId=crypto.randomUUID(),await wt(),await chrome.storage.session.set({[K]:t})),s=t}async function R(){s&&(s.private_key="",s=void 0),await chrome.storage.session.remove([K,j]),await chrome.alarms.clear(L)}async function Q(){const t=(await chrome.storage.session.get(j))[j];return Array.isArray(t)?t:[]}async function pt(e){await chrome.storage.session.set({[j]:e})}async function wt(){await chrome.storage.session.remove(j)}function Tt(e){const t=crypto.getRandomValues(new Uint8Array(e));return Array.from(t,a=>a.toString(16).padStart(2,"0")).join("")}async function Et(e){const t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return Array.from(new Uint8Array(t),a=>a.toString(16).padStart(2,"0")).join("")}function O(e){return!e.params||typeof e.params!="object"?{}:e.params}function C(e){const t=String(e??"").trim();if(!/^[A-Za-z0-9_-]{5,32}$/.test(t))throw new Error("The application ID must contain 5 to 32 URL-safe characters.");return t}function Z(e,t){if(String(e.origin??"")!==t)throw new Error("The application origin does not match the requesting website.")}function ut(e){const t=Number(e),a=Math.floor(Date.now()/1e3);if(!Number.isInteger(t)||Math.abs(a-t)>300)throw new Error("The application connection timestamp must be within five minutes.");return t}function ht(e){const t=String(e??"").trim().toLowerCase();if(!/^[0-9a-f]{32,128}$/.test(t)||t.length%2!==0)throw new Error("The application nonce must contain 16 to 64 random hexadecimal bytes.");return t}function gt(e){const t=String(e??"").trim();if(!t||new TextEncoder().encode(t).length>75)throw new Error("The application connection message must contain 1 to 75 bytes.");return t}function ft(e){const t=String(e??"").trim();if(!t||new TextEncoder().encode(t).length>75)throw new Error("A signed website message must contain 1 to 75 bytes.");return t}function st(e,t){if(e==="contractless_connect"||e==="contractless_signMessage")return"safe";const a=t.transaction;if(!a||typeof a!="object")return"high";const i=Number(a.txtype);return Number.isInteger(i)&&i>=100&&i<=113?"low":"high"}function _t(e){if(!e||typeof e!="object")throw new Error("A complete transaction object is required.");const t=e,a=Number(t.txtype);if(!Number.isInteger(a))throw new Error("The transaction type is invalid.");if(a===6||a===7)throw new Error("Swaps and loan creation require a staged signing workflow.");return t}function z(e){if(!e||typeof e!="object")throw new Error("A complete or partially signed transaction object is required.");const t=e,a=t.unsigned_swap??t.unsigned_loan_contract??t;if(!a||typeof a!="object")throw new Error("The unsigned transaction fields are missing.");const i=Number(a.txtype);if(i!==6&&i!==7)throw new Error("Staged signing is only available for swaps and loan creation.");return t}async function rt(e,t){var S;if(!s)throw new Error("Wallet is locked.");const a=t.unsigned_swap??t.unsigned_loan_contract??t;if(!a||typeof a!="object")return;const i=a,d=Number(i.txtype),l=d===2?[["Receiving",String(i.receiver??"")]]:d===6?[["First swap participant",String(i.sender1??"")],["Second swap participant",String(i.sender2??"")]]:d===7?[["Lender",String(i.lender??"")],["Borrower",String(i.borrower??"")]]:[];if(l.length===0)return;e.inspect_transaction_request(JSON.stringify(t));const f=await k(),v=e.create_handshake_proof(s.public_key,s.private_key),y={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":v},T=new Map;for(const[x,U]of l){const A=U.trim().toLowerCase();let I=T.get(A);if(I===void 0){const r=await fetch(`${f.apiUrl}/api/v1/addresses/registration?address=${encodeURIComponent(A)}`,{headers:y}),o=await r.json().catch(()=>{});if(!r.ok||(o==null?void 0:o.success)!==!0)throw new Error((o==null?void 0:o.error)??`Address registration lookup failed with HTTP ${r.status}.`);I=((S=o.data)==null?void 0:S.registered)===!0,T.set(A,I)}if(!I)throw new Error(`${x} wallet is not registered.`)}}async function mt(e,t){if(t.complete!==!0||typeof t.bytes_hex!="string"||typeof t.hash!="string")throw new Error("The transaction could not be completed.");if(!s)throw new Error("Wallet is locked.");const a=await k(),i=e.create_handshake_proof(s.public_key,s.private_key),d=await fetch(`${a.apiUrl}/api/v1/transactions/broadcast`,{method:"POST",headers:{"Content-Type":"application/json","X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},body:JSON.stringify({transaction_hex:t.bytes_hex})}),l=await d.json().catch(()=>{});if(!d.ok||(l==null?void 0:l.success)!==!0)throw new Error((l==null?void 0:l.error)??`Transaction broadcast failed with HTTP ${d.status}.`);return await b(),{txid:t.hash,bytes:t.bytes_hex,broadcast:l.data}}async function N(e,t){if(!s)throw new Error("Wallet is locked.");Z(e,t);const a=C(e.appId),i=String(e.accessKey??""),d=(await Q()).find(l=>l.origin===t&&l.appId===a&&l.accessKey===i&&l.walletId===(s==null?void 0:s.walletId)&&l.unlockSessionId===(s==null?void 0:s.sessionId));if(!d)throw new Error("The application session is invalid or has expired. Reconnect the wallet.");return d}async function B(){const t=(await chrome.storage.session.get(J))[J],a=Array.isArray(t)?t.filter(y=>typeof y=="string"&&y.length>0):typeof t=="string"&&t.length>0?[t]:[];if(a.length===0)return[];const i=a.map(y=>`approval:${y}`),d=await chrome.storage.session.get(i),l=Date.now(),f=[],v=[];for(const y of a){const T=`approval:${y}`,S=d[T];!S||l-S.createdAt>=ct?v.push(T):f.push(y)}return(v.length>0||f.length!==a.length)&&(await chrome.storage.session.set({[J]:f}),v.length>0&&await chrome.storage.session.remove(v)),f}async function G(e){const t=(await B()).filter(a=>a!==e);return await chrome.storage.session.set({[J]:t}),t}async function ot(e){const t=await B(),a=await chrome.storage.session.get(t.map(d=>`approval:${d}`));if(e.method==="contractless_connect"&&t.some(d=>{const l=a[`approval:${d}`];if(!l||l.method!=="contractless_connect")return!1;const f=O(l),v=O(e);return l.origin===e.origin&&f.appId===v.appId}))throw new Error("This application already has a wallet connection request pending.");if(t.length>=vt)throw new Error("The wallet approval queue is full. Resolve a pending request first.");await chrome.storage.session.set({[J]:[...t,e.id],[`approval:${e.id}`]:e});try{await chrome.action.openPopup()}catch{}}async function nt(e){const t=`approval-result:${e}`,a=Date.now()+ct;for(;Date.now()setTimeout(d,150))}throw await chrome.storage.session.remove([`approval:${e}`,t]),await G(e),new Error("Wallet approval request expired.")}function At(e){const t=e.url??e.origin;if(!t)throw new Error("The requesting website origin is unavailable.");const a=new URL(t).origin;if(a==="null")throw new Error("Requests from opaque origins are not supported.");return a}async function it(e,t){if(!s)throw new Error("Wallet is locked.");switch(await b(),t.method){case"contractless_connect":{const a=O(t);Z(a,t.origin);const i=String(a.walletSessionId??"");if(i&&i!==s.sessionId)throw new Error("The active wallet changed while the connection was being approved.");const d=C(a.appId),l=ut(a.timestamp),f=ht(a.nonce),v=gt(a.message),y=Tt(32),T=await Et(y),S=Math.floor(Date.now()/1e3),x=Math.min(S+900,Math.floor(s.expiresAt/1e3)),U=["Contractless application connection",`Application ID: ${d}`,`Origin: ${t.origin}`,`Requested timestamp: ${l}`,`Message: ${v}`,`Session key hash: ${T}`].join(` +`),A=JSON.parse(e.sign_message_request(JSON.stringify({origin:t.origin,message:U,nonce:f,issued_at:S,expires_at:x}),s.private_key,s.public_key)),I={origin:t.origin,appId:d,accessKey:y,walletId:s.walletId,unlockSessionId:s.sessionId,createdAt:Date.now()},r=await Q();return r.push(I),await pt(r),{address:s.short_address,publicKey:s.public_key,signature:A.signature,proof:A,accessKey:y,appId:d,origin:t.origin,sessionExpiresAt:s.expiresAt}}case"contractless_signMessage":{const a=O(t);await N(a,t.origin);const i=Math.floor(Date.now()/1e3),d=Number(a.expires_at??i+300),l=C(a.appId),f={origin:t.origin,message:["Contractless application message",`Application ID: ${l}`,`Message: ${ft(a.message)}`].join(` +`),nonce:String(a.nonce??""),issued_at:i,expires_at:d};return JSON.parse(e.sign_message_request(JSON.stringify(f),s.private_key,s.public_key))}case"contractless_sendTransaction":{const a=O(t);await N(a,t.origin);const i=_t(a.transaction),d=JSON.parse(e.sign_transaction_request(JSON.stringify(i),s.private_key,s.public_key,1));return mt(e,d)}case"contractless_signDualTransaction":{const a=O(t);await N(a,t.origin);const i=z(a.transaction),d=Number(a.signerSlot);if(d!==1&&d!==2)throw new Error("The signer slot must be 1 or 2.");const l=JSON.parse(e.sign_transaction_request(JSON.stringify(i),s.private_key,s.public_key,d));return{complete:!1,txid:l.hash,unsignedBytes:l.unsigned_bytes_hex,canonicalUnsignedJson:JSON.parse(e.inspect_transaction_request(JSON.stringify(i))).canonical_unsigned_json,signedTransaction:l.signed_transaction,signature:d===1?l.signature1:l.signature2,signature1:l.signature1,signature2:l.signature2,signerSlot:d,signerPublicKey:s.public_key}}default:throw new Error("The website requested an unsupported wallet method.")}}async function It(e,t){var i,d,l,f,v,y,T,S,x,U,A,I;const a=await yt();switch(await dt(),e.type){case"status":return E();case"create-wallet":{const r=String(e.password),o=a.generate_browser_wallet(r),n=JSON.parse(a.unlock_browser_wallet(o,r));await Y(a,n);const u=await X(o);return await W({walletId:u.id,...n}),E()}case"import-wallet":{const r=a.import_legacy_wallet(String(e.walletJson),String(e.legacyPassword),String(e.password)),o=await X(r),n=JSON.parse(a.unlock_browser_wallet(r,String(e.password)));return await W({walletId:o.id,...n}),E()}case"import-wallet-image":{const r=String(e.password),o=a.import_private_key_image(String(e.imageBase64),String(e.imagePassword),r),n=await X(o),u=JSON.parse(a.unlock_browser_wallet(o,r));return await W({walletId:n.id,...u}),E()}case"import-private-key":{const r=String(e.password),o=a.import_private_key(String(e.privateKey),r),n=await X(o),u=JSON.parse(a.unlock_browser_wallet(o,r));return await W({walletId:n.id,...u}),E()}case"unlock":{const r=await $(),o=P(r);if(!o)throw new Error("No wallet exists.");const n=JSON.parse(a.unlock_browser_wallet(o.walletJson,String(e.password)));await W({walletId:o.id,...n});try{await St(a,n)}catch(u){console.warn("Automatic wallet registration failed during unlock.",u)}return E()}case"switch-wallet":{const r=await $(),o=String(e.walletId);if(!r.wallets.some(n=>n.id===o))throw new Error("The selected wallet does not exist.");return await R(),r.activeWalletId=o,await H(r),E()}case"rename-wallet":{const r=await $(),o=r.wallets.find(u=>u.id===String(e.walletId));if(!o)throw new Error("The selected wallet does not exist.");const n=String(e.label).trim();if(n.length<1||n.length>40)throw new Error("Wallet labels must contain between 1 and 40 characters.");return o.label=n,await H(r),E()}case"lock":return await R(),E();case"save-settings":{const r={apiUrl:String(e.apiUrl).replace(/\/+$/,""),autoLockMinutes:Math.max(1,Number(e.autoLockMinutes))};return await chrome.storage.local.set({[V]:r}),await b(),E()}case"register-wallet":{if(!s)throw new Error("Wallet is locked.");const r=await Y(a,s);return await b(),r}case"wallet-balances":{if(!s)throw new Error("Wallet is locked.");const r=await k(),o=a.create_handshake_proof(s.public_key,s.private_key),n={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":o},u=encodeURIComponent(s.short_address),[c,h,w]=await Promise.all([fetch(`${r.apiUrl}/api/v1/balances?address=${u}`,{headers:n}),fetch(`${r.apiUrl}/api/v1/tokens`,{headers:n}),fetch(`${r.apiUrl}/api/v1/nfts`,{headers:n})]),[p,_,g]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{}),w.json().catch(()=>{})]);if(!c.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Balance lookup failed with HTTP ${c.status}.`);return await b(),{address:s.short_address,balances:((i=p.data)==null?void 0:i.balances)??[],tokens:h.ok&&(_==null?void 0:_.success)===!0?((d=_.data)==null?void 0:d.tokens)??[]:[],nfts:w.ok&&(g==null?void 0:g.success)===!0?((l=g.data)==null?void 0:l.nfts)??[]:[],tokenCatalogError:h.ok&&(_==null?void 0:_.success)===!0?void 0:(_==null?void 0:_.error)??`Token lookup failed with HTTP ${h.status}.`,nftCatalogError:w.ok&&(g==null?void 0:g.success)===!0?void 0:(g==null?void 0:g.error)??`NFT lookup failed with HTTP ${w.status}.`}}case"wallet-nft-details":{if(!s)throw new Error("Wallet is locked.");const r=String(e.name??"").trim(),o=Number(e.series??0);if(!r||r.length>15||!Number.isInteger(o)||o<0||o>4294967295)throw new Error("A valid NFT or RWA identifier is required.");const n=await k(),u=a.create_handshake_proof(s.public_key,s.private_key),c={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":u},h=`name=${encodeURIComponent(r)}&series=${o}`,[w,p]=await Promise.all([fetch(`${n.apiUrl}/api/v1/nfts/details?${h}`,{headers:c}),fetch(`${n.apiUrl}/api/v1/nfts/media?${h}`,{headers:c})]),[_,g]=await Promise.all([w.json().catch(()=>{}),p.json().catch(()=>{})]);if(!w.ok||(_==null?void 0:_.success)!==!0)throw new Error((_==null?void 0:_.error)??`NFT details lookup failed with HTTP ${w.status}.`);let m="",tt=!1,et={},D;if(p.ok&&(g==null?void 0:g.success)===!0){et=(f=g.data)!=null&&f.metadata&&typeof g.data.metadata=="object"?g.data.metadata:{},tt=((v=g.data)==null?void 0:v.image_available)===!0;const at=String(((y=g.data)==null?void 0:y.image_url)??"");if(at)try{const q=await fetch(new URL(at,`${n.apiUrl}/`).toString(),{headers:c});q.ok?m=kt(await q.arrayBuffer(),q.headers.get("Content-Type")??"image/png"):D=`NFT image returned HTTP ${q.status}.`}catch{D="NFT image could not be retrieved."}}else D=(g==null?void 0:g.error)??`NFT media lookup failed with HTTP ${p.status}.`;return await b(),{..._.data,metadata:et,image_data_url:m,image_available:tt,media_error:D}}case"wallet-receive-address":{if(!s)throw new Error("Wallet is locked.");const r=await k(),o=a.create_handshake_proof(s.public_key,s.private_key),n=encodeURIComponent(s.short_address),u=await fetch(`${r.apiUrl}/api/v1/addresses/vanity?address=${n}`,{headers:{"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":o}}),c=await u.json().catch(()=>{});if(!u.ok||(c==null?void 0:c.success)!==!0)throw new Error((c==null?void 0:c.error)??`Vanity address lookup failed with HTTP ${u.status}.`);await b();const h=typeof((T=c.data)==null?void 0:T.vanity_address)=="string"&&c.data.vanity_address.trim()!==""?c.data.vanity_address:void 0;return{shortAddress:s.short_address,receiveAddress:h??s.short_address,vanity:!!h}}case"wallet-prepare-transfer":{if(!s)throw new Error("Wallet is locked.");const r=await k(),o=a.create_handshake_proof(s.public_key,s.private_key),n={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":o},u=String(e.receiver??"").trim().toLowerCase(),c=s.short_address.endsWith(".cltc")?"cltc":"clc";let h=u;if(new RegExp(`^[0-9a-f]{40}\\.${c}$`).test(u)){const g=await fetch(`${r.apiUrl}/api/v1/addresses/registration?address=${encodeURIComponent(h)}`,{headers:n}),m=await g.json().catch(()=>{});if(!g.ok||(m==null?void 0:m.success)!==!0)throw new Error((m==null?void 0:m.error)??`Address lookup failed with HTTP ${g.status}.`);if(((x=m.data)==null?void 0:x.registered)!==!0)throw new Error("The receiving wallet is not registered.")}else{if(!new RegExp(`^[a-z]{1,20}\\.${c}$`).test(u))throw new Error(`Enter a valid Contractless ${c.toUpperCase()} address.`);const g=await fetch(`${r.apiUrl}/api/v1/addresses/vanity/resolve?address=${encodeURIComponent(u)}`,{headers:n}),m=await g.json().catch(()=>{});if(!g.ok||(m==null?void 0:m.success)!==!0)throw new Error((m==null?void 0:m.error)??`Vanity address lookup failed with HTTP ${g.status}.`);if(h=String(((S=m.data)==null?void 0:S.owner_address)??""),!h)throw new Error("That vanity address is not registered.")}const w=Number(e.nftSeries??0);if(!Number.isInteger(w)||w<0||w>4294967295)throw new Error("The NFT/RWA series number is invalid.");const p={txtype:2,time:Math.floor(Date.now()/1e3),value:String(e.valueAtomic??""),coin:String(e.asset??"").trim().padEnd(15," "),nft_series:w,sender:s.short_address,receiver:h,txfee:String(e.feeAtomic??"")},_=JSON.parse(a.inspect_transaction_request(JSON.stringify(p)));return await b(),{transaction:p,review:_.review,hash:_.hash,receiverInput:u}}case"wallet-broadcast-transfer":{if(!s)throw new Error("Wallet is locked.");const r=e.transaction;if(!r||typeof r!="object")throw new Error("The reviewed transfer is unavailable.");const o=JSON.parse(a.sign_transaction_request(JSON.stringify(r),s.private_key,s.public_key,1));return mt(a,o)}case"wallet-activity":{if(!s)throw new Error("Wallet is locked.");const r=await k(),o=a.create_handshake_proof(s.public_key,s.private_key),n=encodeURIComponent(s.short_address),u={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":o},[c,h]=await Promise.all([fetch(`${r.apiUrl}/api/v1/addresses/latest?address=${n}&limit=25`,{headers:u}),fetch(`${r.apiUrl}/api/v1/height`,{headers:u})]),[w,p]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{})]);if(!c.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Activity lookup failed with HTTP ${c.status}.`);if(!h.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Block height lookup failed with HTTP ${h.status}.`);return await b(),{address:s.short_address,current_height:Number(((U=p.data)==null?void 0:U.height)??0),transactions:Array.isArray((A=w.data)==null?void 0:A.transactions)?w.data.transactions:[]}}case"wallet-transaction":{if(!s)throw new Error("Wallet is locked.");const r=String(e.txid);if(!/^[0-9a-f]{64}$/i.test(r))throw new Error("A valid transaction ID is required.");const o=await k(),n=a.create_handshake_proof(s.public_key,s.private_key),u={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":n},[c,h]=await Promise.all([fetch(`${o.apiUrl}/api/v1/transactions/lookup?txid=${encodeURIComponent(r)}`,{headers:u}),fetch(`${o.apiUrl}/api/v1/height`,{headers:u})]),[w,p]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{})]);if(!c.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Transaction lookup failed with HTTP ${c.status}.`);if(!h.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Block height lookup failed with HTTP ${h.status}.`);return await b(),{...w.data,current_height:Number(((I=p.data)==null?void 0:I.height)??0)}}case"export-wallet":{const r=P(await $());if(!r)throw new Error("No wallet exists.");return r.walletJson}case"export-legacy-wallet":{const r=P(await $());if(!r)throw new Error("No wallet exists.");return a.export_legacy_wallet(r.walletJson,String(e.password),String(e.legacyPassword))}case"export-private-key-image":{const r=P(await $());if(!r)throw new Error("No wallet exists.");const o=JSON.parse(a.export_legacy_wallet(r.walletJson,String(e.password),String(e.imagePassword)));if(typeof o.private_key!="string")throw new Error("The wallet image could not be created.");return o.private_key}case"reveal-private-key":{const r=P(await $());if(!r)throw new Error("No wallet exists.");const o=JSON.parse(a.unlock_browser_wallet(r.walletJson,String(e.password)));if(typeof o.private_key!="string")throw new Error("The private key could not be decrypted.");return await b(),{label:r.label,address:r.address,privateKey:o.private_key}}case"provider-request":{const r=At(t),o=String(e.method),n=e.params&&typeof e.params=="object"?e.params:{};if(o==="contractless_detect")return{installed:!0,providerVersion:"1.0.0"};if(o==="contractless_connect"){Z(n,r);const c=C(n.appId),h=ut(n.timestamp);ht(n.nonce);const w=gt(n.message),p={id:crypto.randomUUID(),method:o,params:{...n,walletSessionId:s==null?void 0:s.sessionId},origin:r,createdAt:Date.now(),risk:st(o,n),display:{request:"Connect application",application_id:c,website:r,wallet_address:(s==null?void 0:s.short_address)??"Unlock wallet to continue",message:w,requested_timestamp:h,access:"Address access and future wallet requests until this wallet locks"}};if(await ot(p),!await nt(p.id))throw new Error("The wallet connection request was rejected.");return it(a,p)}if(o==="contractless_accounts")try{return await N(n,r),s?[s.short_address]:[]}catch{return[]}if(o==="contractless_balances"){if(await N(n,r),!s)throw new Error("Wallet is locked.");const c=await k(),h=a.create_handshake_proof(s.public_key,s.private_key),w=await fetch(`${c.apiUrl}/api/v1/balances?address=${encodeURIComponent(s.short_address)}`,{headers:{"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":h}}),p=await w.json().catch(()=>{});if(!w.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Balance lookup failed with HTTP ${w.status}.`);return await b(),p.data}if(o==="contractless_disconnect"){const c=await N(n,r),h=(await Q()).filter(w=>!(w.origin===c.origin&&w.appId===c.appId&&w.accessKey===c.accessKey));return await pt(h),{disconnected:!0}}if(o==="contractless_completeDualTransaction"){await N(n,r);const c=z(n.transaction);return JSON.parse(a.complete_dual_transaction_request(JSON.stringify(c),String(n.signature1??""),String(n.publicKey1??""),String(n.signature2??""),String(n.publicKey2??"")))}if(!["contractless_signMessage","contractless_sendTransaction","contractless_signDualTransaction"].includes(o))throw new Error("The website requested an unsupported wallet method.");if(await N(n,r),o==="contractless_sendTransaction"){const c=_t(n.transaction);await rt(a,c)}else if(o==="contractless_signDualTransaction"){const c=z(n.transaction);await rt(a,c)}const u={id:crypto.randomUUID(),method:o,params:n,origin:r,createdAt:Date.now(),risk:st(o,n),display:o==="contractless_signMessage"?{request:"Sign a text message",application_id:C(n.appId),website:r,message:ft(n.message),warning:"This is a text signature. It cannot move wallet assets."}:o==="contractless_signDualTransaction"?(()=>{const c=JSON.parse(a.inspect_transaction_request(JSON.stringify(n.transaction)));return{application_id:C(n.appId),website:r,transaction_type:c.transaction_name,signer_slot:n.signerSlot,transaction_fields:c.review,action:"Sign and return this signature to the application without broadcasting."}})():(()=>{const c=JSON.parse(a.inspect_transaction_request(JSON.stringify(n.transaction)));return{application_id:C(n.appId),website:r,transaction_type:c.transaction_name,transaction_fields:c.review,action:"Sign and broadcast this transaction."}})()};if(await ot(u),!await nt(u.id))throw new Error("The wallet request was rejected.");return it(a,u)}case"pending-provider-approval":{const r=await B(),o=r[0]??"";if(!o)return null;const n=`approval:${o}`,c=(await chrome.storage.session.get(n))[n];return c?s?(c.method==="contractless_connect"&&(c.params={...c.params,walletSessionId:s.sessionId},c.display={...c.display??{},wallet_address:s.short_address},await chrome.storage.session.set({[n]:c})),{locked:!1,request:c,position:1,total:r.length}):{locked:!0}:(await G(o),null)}case"resolve-provider-approval":{if(!s)throw new Error("Unlock the wallet before approving this request.");const r=String(e.id??""),o=await B();if(!r||o[0]!==r)throw new Error("The wallet approval request has expired.");const n=`approval:${r}`,c=(await chrome.storage.session.get(n))[n];if(!c)throw new Error("The wallet approval request has expired.");return c.method==="contractless_connect"&&(c.params={...c.params,walletSessionId:s.sessionId},await chrome.storage.session.set({[n]:c})),await chrome.storage.session.set({[`approval-result:${r}`]:{approved:e.approved===!0}}),await chrome.storage.session.remove(n),{resolved:!0,remaining:(await G(r)).length}}default:throw new Error("Unsupported wallet request.")}}chrome.runtime.onMessage.addListener((e,t,a)=>(It(e,t).then(i=>a({ok:!0,result:i})).catch(i=>a({ok:!1,error:i instanceof Error?i.message:String(i)})),!0));chrome.alarms.onAlarm.addListener(e=>{e.name===L&&R()}); diff --git a/extension/dist/chrome/assets/contractless-logo-BgfMS-Kf.png b/extension/dist/chrome/assets/contractless-logo-BgfMS-Kf.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/dist/chrome/assets/contractless-logo-BgfMS-Kf.png differ diff --git a/extension/dist/chrome/assets/contractless_browser_core_bg-CM1Z7v2F.wasm b/extension/dist/chrome/assets/contractless_browser_core_bg-CM1Z7v2F.wasm new file mode 100644 index 0000000..99ed81f Binary files /dev/null and b/extension/dist/chrome/assets/contractless_browser_core_bg-CM1Z7v2F.wasm differ diff --git a/extension/dist/chrome/assets/contractless_browser_core_bg-Dl61GrlP.wasm b/extension/dist/chrome/assets/contractless_browser_core_bg-Dl61GrlP.wasm new file mode 100644 index 0000000..c1c41b8 Binary files /dev/null and b/extension/dist/chrome/assets/contractless_browser_core_bg-Dl61GrlP.wasm differ diff --git a/extension/dist/chrome/assets/import-wallet-D9ROrgVZ.css b/extension/dist/chrome/assets/import-wallet-D9ROrgVZ.css new file mode 100644 index 0000000..e51e6f5 --- /dev/null +++ b/extension/dist/chrome/assets/import-wallet-D9ROrgVZ.css @@ -0,0 +1 @@ +:root{font:16px/1.5 Arial,Helvetica,sans-serif;color:#fff;background:#08131c}*{box-sizing:border-box}body{min-width:320px;min-height:100vh;margin:0;background:#08131c}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:wait;opacity:.72}.site-header{display:flex;align-items:center;justify-content:space-between;min-height:76px;padding:0 32px;border-bottom:1px solid #2e4757;background:#0d1d28}.brand{display:flex;align-items:center;gap:12px}.brand img{width:39px;filter:brightness(0) invert(1)}.brand div{display:grid}.brand strong{font-size:18px}.brand span,.local-page{color:#b9cbd6;font-size:13px}main{display:grid;place-items:start center;padding:54px 20px}.import-panel,.success-panel{width:min(100%,500px);padding:34px;border:1px solid #385666;background:#10222e}.eyebrow{color:#58d5e7;font-size:12px;font-weight:700;text-transform:uppercase}h1{margin:7px 0 10px;font-size:31px;line-height:1.15;letter-spacing:0}.intro,.success-panel p{margin:0 0 26px;color:#c8d6de}form{display:grid}label{margin:21px 0 8px;font-size:13px;font-weight:700}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.file-picker{display:flex;align-items:center;gap:14px;min-height:78px;margin-top:8px;padding:13px;border:1px solid #5ac9de;background:#132d3b;cursor:pointer}.file-picker>span:last-child{display:grid;gap:3px}.file-picker small{color:#afc6d1;font-weight:400}.file-icon{display:grid;place-items:center;width:44px;height:44px;border:1px solid #62d7e9;color:#62d7e9;font-size:25px}.input-row{display:grid;grid-template-columns:1fr auto}.input-row input{min-width:0;height:50px;padding:0 14px;border:1px solid #5a7180;border-right:0;background:#08151e;color:#fff}.input-row button{width:68px;border:1px solid #5a7180;background:#173345;color:#fff}.security-note,.address-result{display:grid;gap:4px;margin-top:20px;padding:13px 14px;border-left:3px solid #57d0df;background:#0a1922}.security-note span,.address-result span{color:#b7cad4;font-size:13px}.address-result strong{overflow-wrap:anywhere}.primary{min-height:50px;margin-top:24px;border:1px solid #67d5e4;background:#236eaa;color:#fff;font-weight:700}#notice{margin:14px 0 0;padding:10px 12px;border-left:3px solid #ff7168;background:#321c20;color:#ffc0bb;font-size:13px}#notice:empty{display:none}.success-mark{display:grid;place-items:center;width:58px;height:58px;margin-bottom:25px;border:2px solid #62d7e9;border-radius:50%;color:#62d7e9;font-size:31px;font-weight:700}.completion-message{margin-top:24px!important}@media(max-width:560px){.site-header{padding:0 18px}.local-page{display:none}main{padding:24px 12px}.import-panel,.success-panel{padding:24px 20px}h1{font-size:27px}} diff --git a/extension/dist/chrome/assets/popup-B3vY8vDf.css b/extension/dist/chrome/assets/popup-B3vY8vDf.css new file mode 100644 index 0000000..aaa5acc --- /dev/null +++ b/extension/dist/chrome/assets/popup-B3vY8vDf.css @@ -0,0 +1 @@ +:root{--blue: #2167a5;--blue-deep: #174d7c;--ink: #13202a;--muted-white: rgb(255 255 255 / 90%);--danger: #b9382e;--danger-soft: #fff0ee;--success: #1d8054;--dark: #0c151d;--surface: #111e28;--surface-raised: #172936;--surface-active: #1b3547;--dark-line: #314957;--dark-muted: #c4d0d7;--accent: #55c5dc;font:14px/1.45 Arial,Helvetica,sans-serif}*{box-sizing:border-box}body{width:390px;min-width:390px;min-height:600px;margin:0;background:var(--blue);color:#fff}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}.wallet-shell{position:relative;width:390px;height:600px;overflow:hidden;background:var(--blue)}.screen{width:100%;height:100%;background:var(--blue)}.screen[hidden]{display:none}.wallet-header{height:72px;display:flex;align-items:center;gap:11px;padding:0 20px;border-bottom:1px solid rgb(255 255 255 / 70%)}.wallet-header img{width:37px;filter:brightness(0) invert(1)}.wallet-header div{display:grid}.wallet-header strong{font-size:18px}.wallet-header small{color:var(--muted-white);font-size:14px}.wallet-header>span{margin-left:auto;font-size:13px;font-weight:700}.screen-content{padding:28px 24px 22px}.screen-content.compact{padding-top:18px}.eyebrow{font-size:11px;font-weight:700;text-transform:uppercase}h1{margin:7px 0 9px;font-size:27px;line-height:1.12;letter-spacing:0}.intro{margin:0 0 24px;color:var(--muted-white);line-height:1.5}label{display:block;margin-bottom:8px;font-size:12px;font-weight:700}.input-row{height:49px;display:grid;grid-template-columns:1fr auto;border:1px solid white;background:#fff}.input-row:focus-within,textarea:focus{outline:2px solid rgb(255 255 255 / 28%);outline-offset:2px}.input-row input{min-width:0;border:0;padding:0 13px;color:var(--ink);outline:0}.reveal{border:0;border-left:1px solid #d9e0e5;padding:0 13px;background:#fff;color:var(--blue);font-size:11px;font-weight:700}textarea{width:100%;min-height:82px;margin-bottom:13px;resize:none;border:1px solid white;padding:11px 13px;color:var(--ink);outline:0}.note{display:grid;gap:2px;margin:18px 0 23px;border-left:3px solid var(--accent);padding:13px;background:var(--surface-raised);color:#fff;font-size:12px}.note span{color:var(--dark-muted)}.compact-note{margin:12px 0 14px;padding:10px 13px}.warning{margin:18px 0 25px;border-left:3px solid var(--danger);padding:13px;background:#321c20;color:#ffbbb5;font-size:12px;line-height:1.4}.warning strong{display:block}.primary,.secondary{width:100%;min-height:48px;display:flex;align-items:center;justify-content:center;gap:10px;border:1px solid var(--accent);background:var(--accent);color:#08202b;font-weight:700}.primary span{font-size:18px}.secondary{min-height:42px;margin-top:9px;background:transparent;color:#dcecf3;font-size:12px}.secondary:disabled{border-color:#ffffff59;color:#ffffff7f}.text-button{width:100%;margin-top:8px;border:0;padding:8px;background:transparent;color:var(--muted-white)}.divider{display:flex;align-items:center;gap:10px;margin:17px 0 12px;color:#ffffffad;font-size:12px;text-transform:uppercase}.divider:before,.divider:after{height:1px;flex:1;background:#ffffff4d;content:""}.file-heading{margin-top:20px}.file-picker{min-height:86px;display:flex;align-items:center;gap:13px;margin-bottom:24px;border:1px dashed rgb(255 255 255 / 72%);padding:14px;background:#ffffff1a;cursor:pointer}.file-picker>span:last-child{display:grid;gap:4px}.file-picker small{color:#ffffffad;font-weight:400}.file-icon{width:34px;height:34px;display:grid;flex:0 0 34px;place-items:center;border:1px solid white;font-size:22px;font-weight:400}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}.loading-screen{position:relative}.loading-brand{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-content:center;justify-items:center;gap:34px}.loading-brand img,.large-logo{width:148px;filter:brightness(0) invert(1)}.spinner{width:28px;aspect-ratio:1;border:3px solid rgb(255 255 255 / 28%);border-top-color:#fff;border-radius:50%;animation:spin .85s linear infinite}.loading-screen>p{position:absolute;bottom:35px;width:100%;margin:0;color:#ffffffb8;font-size:12px;text-align:center;text-transform:uppercase}@keyframes spin{to{transform:rotate(360deg)}}.success-mark{width:38px;height:38px;display:grid;place-items:center;border-radius:50%;border:1px solid #55c98a;background:#123727;color:#66dfa0;font-size:22px;font-weight:700}.image-preview{position:relative;height:142px;margin:18px 0;overflow:hidden;border:1px solid var(--dark-line);background:var(--surface)}.image-preview img{width:100%;height:100%;display:block;object-fit:contain}.confirmation{display:grid;grid-template-columns:18px 1fr;gap:9px;align-items:start;margin:14px 0;color:#ffffffd6;font-size:11px;line-height:1.35}.confirmation input{width:16px;height:16px;margin:0}.centered-content{height:100%;display:flex;flex-direction:column;justify-content:center;padding:36px 28px;text-align:center}.centered-content{align-items:stretch}.centered-content .large-logo{align-self:center;width:92px;margin-bottom:25px}.centered-content h1{text-align:center}.centered-content .primary{margin-top:22px}.dashboard-header{height:66px}.account-button{min-width:105px;max-width:132px;height:34px;display:flex;align-items:center;gap:7px;margin-left:auto;overflow:hidden;border:1px solid rgb(255 255 255 / 45%);padding:0 10px;background:transparent;color:#fff;font-size:14px}.account-button>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.network-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:#63d18d}.assets-dashboard{height:calc(100% - 66px);overflow:hidden}.portfolio-balance{padding:22px 22px 19px;border-bottom:1px solid rgb(255 255 255 / 24%);background:var(--blue-deep)}.portfolio-balance>span{color:var(--muted-white);font-size:14px;text-transform:uppercase}.portfolio-balance>strong{display:block;margin-top:4px;font-size:31px;line-height:1.2}.portfolio-balance>strong small{font-size:13px}.portfolio-balance p{max-width:100%;margin:7px 0 0;overflow:hidden;color:#ffffffd1;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.wallet-actions{height:88px;display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid rgb(255 255 255 / 24%)}.wallet-actions button{display:grid;place-items:center;align-content:center;gap:5px;border:0;border-right:1px solid rgb(255 255 255 / 18%);background:transparent;color:#fff;font-size:14px;font-weight:700}.wallet-actions button:last-child{border-right:0}.action-icon{width:29px;height:29px;display:grid;place-items:center;border:1px solid rgb(255 255 255 / 65%);font-size:16px;font-weight:400}.asset-ledger{height:calc(100% - 209px);background:var(--dark);color:#fff}.asset-filter{height:42px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--dark-line);padding:5px 19px;background:#0f1a22}.asset-filter button{border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dark-muted);font-size:14px;font-weight:700}.asset-filter button.active{border-bottom-color:var(--accent);color:#fff}.asset-ledger-heading{height:45px;display:flex;align-items:center;justify-content:space-between;padding:0 19px;border-bottom:1px solid var(--dark-line)}.asset-ledger-heading h1{margin:0;font-size:19px}.asset-ledger-heading button{width:30px;height:30px;border:0;background:transparent;color:var(--accent);font-size:17px}.asset-row{min-height:66px;display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:11px;align-items:center;padding:10px 19px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.asset-symbol{width:36px;height:36px;display:grid;place-items:center;background:var(--blue);color:#fff;font-size:12px;font-weight:700}.contractless-symbol img{width:25px;filter:brightness(0) invert(1)}.asset-name,.asset-value{min-width:0;display:grid;gap:2px}.asset-name strong,.asset-value strong{overflow:hidden;font-size:16px;text-overflow:ellipsis;white-space:nowrap}.asset-name span,.asset-value span{color:var(--dark-muted);font-size:13px}.asset-value{max-width:125px;text-align:right}.token-list{max-height:calc(100% - 148px);overflow-y:auto}.empty-assets{display:grid;gap:5px;padding:27px 20px;color:var(--dark-muted);text-align:center}.empty-assets strong{color:#dce8ee;font-size:16px}.empty-assets span{font-size:14px}.receive-screen{background:var(--dark)}.receive-screen .wallet-header{background:var(--blue)}.send-screen,.send-review-screen,.send-success-screen{background:var(--dark);color:#fff}.send-screen .wallet-header,.send-review-screen .wallet-header,.send-success-screen .wallet-header{background:var(--blue)}.send-content,.send-review-content,.send-success-content{height:calc(100% - 72px);padding:22px;overflow-y:auto}.send-content{display:grid;align-content:start;gap:17px}.send-content label{color:#fff;font-size:14px}.send-content input,.send-content select{width:100%;min-height:48px;border:1px solid var(--dark-line);padding:0 13px;background:var(--surface);color:#fff;outline:0}.send-content input:focus,.send-content select:focus{border-color:var(--accent)}.send-amount-row{display:grid;grid-template-columns:minmax(0,1fr) auto}.send-amount-row input{border-right:0}.send-amount-row>span{min-width:64px;display:grid;place-items:center;border:1px solid var(--dark-line);background:var(--surface-raised);color:var(--accent);font-weight:700}.field-note{display:block;margin-top:7px;color:var(--dark-muted);font-size:13px}.send-continue{margin-top:5px}.send-review-content{padding:0 22px 24px}.send-review-amount{margin:0 -22px 20px;padding:24px 22px;background:var(--blue-deep)}.send-review-amount>span{color:var(--muted-white);font-size:13px;text-transform:uppercase}.send-review-amount strong{display:block;margin-top:5px;font-size:29px}.send-review-amount small{color:var(--accent);font-size:15px}.send-review-details{margin:0 0 18px}.send-review-details>div{padding:12px 0;border-bottom:1px solid var(--dark-line)}.send-review-details dt{margin-bottom:4px;color:var(--dark-muted);font-size:13px}.send-review-details dd{margin:0;overflow-wrap:anywhere;color:#fff;font-weight:700}.signing-notice{margin:18px 0;padding-left:12px;border-left:3px solid var(--accent);color:var(--dark-muted)}.send-success-content{text-align:center}.send-success-content p{color:var(--dark-muted)}.send-txid{margin:24px 0 14px;padding:14px;border:1px solid var(--dark-line);background:var(--surface);text-align:left}.send-txid span{display:block;margin-bottom:5px;color:var(--dark-muted);font-size:13px}.send-txid strong{overflow-wrap:anywhere}.send-success-content .secondary{margin-bottom:10px}.receive-content{height:calc(100% - 72px);overflow-y:auto;padding:25px 24px 22px;text-align:center}.receive-label{color:var(--accent);font-size:12px;font-weight:700;text-transform:uppercase}.receive-content h1{margin-top:8px}.receive-content>p{margin:0 auto 20px;color:var(--dark-muted);font-size:11px;line-height:1.45}.qr-frame{width:238px;height:238px;display:grid;place-items:center;margin:0 auto 19px;border:1px solid #708693;padding:8px;background:#fff}.qr-frame canvas{width:220px;height:220px;display:block}.receive-address{display:grid;gap:6px;border:1px solid var(--dark-line);padding:13px;background:var(--surface);text-align:left}.receive-address span{color:var(--dark-muted);font-size:11px;font-weight:700;text-transform:uppercase}.receive-address strong{overflow-wrap:anywhere;color:#e5f0f5;font:12px/1.4 Consolas,monospace}.receive-copy{margin-top:13px}.nfts-screen,.nft-detail-screen{background:var(--dark);color:#fff}.nfts-screen .wallet-header,.nft-detail-screen .wallet-header{background:var(--blue)}.header-refresh{width:34px;height:34px;display:grid;place-items:center;margin-left:auto;border:1px solid rgb(255 255 255 / 70%);background:transparent;color:#fff;font-size:18px}.nfts-content,.nft-detail-content{height:calc(100% - 72px);overflow-y:auto}.nfts-summary{min-height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:#0f1a22;color:var(--dark-muted);font-size:13px;font-weight:700;text-transform:uppercase}.nfts-summary span:first-child{color:var(--accent)}.nfts-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;padding:1px;background:var(--dark-line)}.nft-card{min-width:0;display:grid;align-content:start;border:0;padding:0;background:var(--surface);color:#fff;text-align:left}.nft-card:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.nft-card-media{width:100%;aspect-ratio:1;display:grid;place-items:center;overflow:hidden;background:#17303e;color:var(--accent);font-size:28px;font-weight:700}.nft-card-media img{width:100%;height:100%;display:block;object-fit:cover}.nft-card-identity{min-height:116px;display:grid;align-content:start;gap:3px;padding:11px 12px 13px}.nft-card-identity>span{color:var(--accent);font-size:11px;font-weight:700}.nft-card-identity strong{overflow:hidden;font-size:15px;text-overflow:ellipsis;white-space:nowrap}.nft-card-identity small{overflow:hidden;color:var(--dark-muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.nft-card-identity b{margin-top:5px;color:#fff;font-size:12px}.nft-card-identity em{color:#e9c667;font-size:11px;font-style:normal}.nfts-empty{grid-column:1 / -1;min-height:300px;display:grid;place-content:center;gap:6px;padding:30px;background:var(--surface);color:var(--dark-muted);text-align:center}.nfts-empty strong{color:#fff;font-size:17px}.nfts-pagination{min-height:52px;display:grid;grid-template-columns:42px 1fr 42px;align-items:center;gap:10px;padding:7px 14px;border-top:1px solid var(--dark-line);background:#0f1a22;text-align:center}.nfts-pagination[hidden]{display:none}.nfts-pagination button{width:38px;height:38px;border:1px solid var(--dark-line);background:var(--surface);color:var(--accent);font-size:17px}.nfts-pagination button:disabled{color:#61727c}.nfts-pagination span{color:var(--dark-muted);font-size:13px}.nft-detail-media{width:100%;aspect-ratio:16 / 11;display:grid;place-items:center;overflow:hidden;border-bottom:1px solid var(--dark-line);background:#142934}.nft-detail-media img{width:100%;height:100%;display:block;object-fit:contain}.nft-detail-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:15px;align-items:end;padding:18px 20px 14px}.nft-detail-heading>div{min-width:0}.nft-detail-heading span{color:var(--accent);font-size:11px;font-weight:700}.nft-detail-heading h1{overflow-wrap:anywhere;margin:4px 0 0;font-size:23px}.nft-detail-heading>strong{color:#63d18d;font-size:13px;text-align:right}.nft-detail-description{margin:0;padding:0 20px 17px;color:var(--dark-muted);font-size:14px;line-height:1.5}.nft-detail-attributes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;padding:0 20px 18px}.nft-detail-attributes[hidden]{display:none}.nft-detail-attributes>div{min-width:0;display:grid;gap:2px;border:1px solid var(--dark-line);padding:9px;background:var(--surface-raised)}.nft-detail-attributes span{overflow:hidden;color:var(--dark-muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.nft-detail-attributes strong{overflow-wrap:anywhere;font-size:13px}.nft-detail-fields{margin:0;border-top:1px solid var(--dark-line)}.nft-detail-fields>div{display:grid;gap:4px;padding:12px 20px;border-bottom:1px solid var(--dark-line)}.nft-detail-fields dt{color:var(--dark-muted);font-size:12px}.nft-detail-fields dd{margin:0;overflow-wrap:anywhere;color:#fff;font:12px/1.45 Consolas,monospace}.nft-detail-actions{padding:18px 20px 0}.nft-provenance{padding:18px 20px 25px}.nft-provenance h2{margin:0 0 11px;font-size:16px}.nft-provenance p{color:var(--dark-muted)}.nft-history-row{width:100%;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;border:0;padding:10px 0;border-bottom:1px solid var(--dark-line);background:transparent;color:#fff;text-align:left}.nft-history-row:hover,.nft-history-row:focus-visible{background:var(--surface-raised)}.nft-history-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.nft-history-row strong{font-size:13px}.nft-history-row span{color:var(--dark-muted);font-size:12px;text-align:right}.nft-history-row small{grid-column:1 / -1;color:var(--accent);font:11px Consolas,monospace}.activity-screen{background:var(--dark);color:#fff}.activity-screen .wallet-header{background:var(--blue)}.activity-content{height:calc(100% - 72px);overflow-y:auto}.activity-summary{min-height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:#0f1a22;color:var(--dark-muted);font-size:13px;text-transform:uppercase}.activity-summary strong{color:var(--accent);font-size:13px}.activity-summary>div{display:flex;align-items:center;gap:8px}.activity-summary button{width:30px;height:30px;border:1px solid var(--dark-line);background:transparent;color:var(--accent);font-size:17px}.activity-list{background:var(--surface)}.activity-row{width:100%;min-height:98px;display:grid;grid-template-columns:31px minmax(0,1fr) auto;gap:10px;align-items:center;padding:11px 17px;border-bottom:1px solid var(--dark-line);border-top:0;border-right:0;border-left:0;background:transparent;color:#fff;text-align:left}.activity-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.activity-marker{width:29px;height:29px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:13px;font-weight:700}.activity-marker.incoming,.activity-status .incoming{color:#63d18d}.activity-marker.outgoing,.activity-status .outgoing{color:#fff}.activity-details,.activity-status{min-width:0;display:grid;gap:2px}.activity-details strong{overflow:hidden;font-size:16px;text-overflow:ellipsis;white-space:nowrap}.activity-details span{color:#c1d0d8;font:13px Consolas,monospace}.activity-details small,.activity-status span{color:var(--dark-muted);font-size:13px}.activity-status{max-width:132px;text-align:right}.activity-status strong{font-size:14px;white-space:nowrap}.activity-status .confirmed{color:#63d18d}.activity-status .pending{color:#e9c667}.activity-empty{display:grid;gap:6px;padding:42px 24px;color:var(--dark-muted);font-size:14px;text-align:center}.activity-empty strong{color:#dce8ee;font-size:16px}.activity-error{color:#ffaaa4}.transaction-screen{background:var(--dark);color:#fff}.transaction-screen .wallet-header{background:var(--blue)}.transaction-confirmed{color:#84e7aa}.transaction-pending{color:#f1d27b}.transaction-content{height:calc(100% - 72px);overflow-y:auto;padding-bottom:24px}.transaction-identity{display:grid;gap:13px;padding:18px;border-bottom:1px solid var(--dark-line);background:var(--surface-raised)}.transaction-identity>div{display:grid;gap:7px}.transaction-identity span,.transaction-field span{color:var(--dark-muted);font-size:12px;font-weight:700;text-transform:uppercase}.transaction-identity strong{overflow-wrap:anywhere;color:#fff;font:13px/1.55 Consolas,monospace}.transaction-identity button{min-height:40px;border:1px solid var(--accent);background:transparent;color:var(--accent);font-size:14px;font-weight:700}.transaction-section{border-top:1px solid var(--dark-line)}.transaction-section h2{margin:0;padding:16px 18px 10px;font-size:16px}.transaction-fields{display:grid}.transaction-field{display:grid;gap:6px;padding:13px 18px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.transaction-field strong{overflow-wrap:anywhere;color:#fff;font-size:15px;font-weight:600;line-height:1.45}.transaction-technical{border-top:1px solid var(--dark-line)}.transaction-technical summary{padding:17px 18px;color:var(--accent);font-size:15px;font-weight:700;cursor:pointer}.technical-field strong{font:12px/1.55 Consolas,monospace;word-break:break-all}.drawer-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#091a278a}.account-drawer{position:absolute;top:0;right:0;bottom:0;z-index:11;width:338px;display:flex;flex-direction:column;border-left:1px solid var(--dark-line);background:var(--dark);color:#fff;transform:translate(100%);transition:transform .18s ease-out}.account-drawer.open{transform:translate(0)}.drawer-header{height:72px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:var(--blue);color:#fff}.drawer-header>div{display:grid;gap:2px}.drawer-header strong{font-size:19px}.drawer-header span{color:var(--muted-white);font-size:14px}.drawer-header button{width:34px;height:34px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:23px;line-height:1}.drawer-section{max-height:244px;overflow-y:auto;padding:17px 15px 14px;border-bottom:1px solid var(--dark-line)}.drawer-label{display:block;margin:0 3px 8px;color:var(--dark-muted);font-size:13px;font-weight:700;text-transform:uppercase}.known-address{width:100%;min-height:58px;display:grid;grid-template-columns:minmax(0,1fr) 30px;gap:4px;align-items:center;border:1px solid var(--dark-line);padding:4px;background:var(--surface);color:#fff}.known-address+.known-address{margin-top:7px}.known-address.active-address{border-color:var(--accent);background:var(--surface-active)}.address-select{min-width:0;min-height:48px;display:grid;grid-template-columns:36px minmax(0,1fr) 20px;gap:10px;align-items:center;border:0;padding:5px 6px;background:transparent;color:#fff;text-align:left}.rename-address{width:28px;height:34px;border:0;background:transparent;color:var(--accent);font-size:16px}.address-avatar{width:34px;height:34px;display:grid;place-items:center;background:#204f70}.address-avatar img{width:24px;filter:brightness(0) invert(1)}.address-details{min-width:0;display:grid;gap:2px}.address-details strong{font-size:15px}.address-details small{overflow:hidden;color:var(--dark-muted);font-size:13px;text-overflow:ellipsis;white-space:nowrap}.selected-mark{color:var(--success);font-size:14px;font-weight:700}.drawer-navigation{display:grid;padding:8px 0}.drawer-navigation button{min-height:72px;display:grid;grid-template-columns:34px minmax(0,1fr) 16px;gap:10px;align-items:center;border:0;border-bottom:1px solid var(--dark-line);padding:9px 18px;background:transparent;color:#fff;text-align:left}.drawer-navigation button>span:nth-child(2){display:grid;gap:2px}.drawer-navigation strong{font-size:16px}.drawer-navigation small{color:var(--dark-muted);font-size:13px}.menu-icon{width:32px;height:32px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:15px;font-weight:700}.menu-arrow{color:var(--dark-muted);font-size:20px;text-align:right}.drawer-lock{min-height:46px;display:flex;align-items:center;justify-content:center;gap:8px;margin:auto 15px 15px;border:1px solid var(--dark-line);background:var(--surface-raised);color:#dce8ee;font-size:14px;font-weight:700}.header-back{width:34px;height:34px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:18px}.settings-screen{background:var(--dark);color:#fff}.settings-screen .wallet-header{background:var(--blue);color:#fff}.settings-content{height:calc(100% - 72px);overflow-y:auto;padding-bottom:20px}.settings-group{padding:18px 20px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.settings-heading{display:grid;gap:3px;margin-bottom:15px}.settings-heading strong{font-size:13px}.settings-heading span{color:var(--dark-muted);font-size:12px;line-height:1.4}.settings-group label{color:#b7c6ce;font-size:12px;text-transform:uppercase}.settings-group input,.settings-group select{width:100%;height:44px;border:1px solid #8699a5;padding:0 11px;background:#fff;color:var(--ink);outline:0}.settings-group input:focus,.settings-group select:focus{border-color:var(--blue);outline:2px solid rgb(33 103 165 / 14%)}.settings-save{width:calc(100% - 40px);margin:16px 20px;border-color:var(--accent);background:var(--accent);color:#08202b}.backup-settings{padding-right:0;padding-left:0}.backup-settings .settings-heading{padding:0 20px}.settings-action{width:100%;min-height:61px;display:grid;grid-template-columns:34px minmax(0,1fr) 16px;gap:10px;align-items:center;border:0;border-top:1px solid var(--dark-line);padding:9px 20px;background:transparent;color:#fff;text-align:left}.settings-action:last-child{border-bottom:1px solid var(--dark-line)}.settings-action>span:nth-child(2){display:grid;gap:2px}.settings-action strong{font-size:14px}.settings-action small{color:var(--dark-muted);font-size:11px}.settings-action>span:last-child{color:var(--dark-muted);font-size:20px}.settings-action-icon{width:32px;height:32px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:15px;font-weight:700}.private-key-result{margin:16px 20px 0;border:1px solid var(--dark-line);padding:13px;background:var(--surface-raised)}.private-key-result>div{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px}.private-key-result>div>strong{font-size:11px;text-transform:uppercase}.private-key-result>div>button{width:28px;height:28px;border:0;background:transparent;color:var(--dark-muted);font-size:19px}.private-key-result textarea{height:95px;min-height:95px;margin:0 0 10px;border:1px solid #8699a5;padding:9px;background:#fff;color:var(--ink);font-size:11px}.private-key-result .secondary{border-color:var(--accent);color:var(--accent)}.modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:30;background:#091a27ad}.backup-key-modal{position:absolute;top:50%;right:20px;left:20px;z-index:31;border:1px solid white;background:var(--blue);color:#fff;transform:translateY(-50%)}.backup-key-modal>header{min-height:65px;display:flex;align-items:center;justify-content:space-between;padding:12px 15px;border-bottom:1px solid rgb(255 255 255 / 40%)}.backup-key-modal>header>div{display:grid;gap:2px}.backup-key-modal>header strong{font-size:13px}.backup-key-modal>header span{color:var(--muted-white);font-size:11px}.backup-key-modal>header button{width:30px;height:30px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:20px}.backup-key-modal>div{padding:17px 15px}.backup-key-modal .primary{margin-top:16px}#notice{position:absolute;right:18px;bottom:14px;left:18px;z-index:40;margin:0;padding:10px 12px;border:1px solid #8d4448;border-left:3px solid #ff7168;background:#321c20;color:#ffc0bb;font-size:11px}#notice:empty{display:none}.provider-approval-screen{display:flex;flex-direction:column}.provider-approval-screen .screen-content{min-height:0;flex:1;overflow-y:auto;padding-bottom:18px}.approval-context{display:flex;align-items:center;justify-content:space-between;gap:12px}.approval-context .eyebrow{min-width:0;overflow-wrap:anywhere}.risk-label{flex:0 0 auto;padding:5px 8px;border:1px solid currentColor;font-size:11px;font-weight:700;text-transform:uppercase}.risk-safe{background:#123b2d;color:#63e5a9}.risk-low{background:#403612;color:#ffe16c}.risk-high{background:#481f24;color:#ff8b8b}.approval-details{display:grid;grid-template-columns:minmax(100px,.8fr) minmax(0,1.4fr);margin:20px 0;border:1px solid #315466;background:#091923}.approval-details dt,.approval-details dd{min-width:0;margin:0;padding:11px 12px;border-bottom:1px solid #203d4c;overflow-wrap:anywhere}.approval-details dt{color:#85dce8;font-size:12px;font-weight:700;text-transform:capitalize}.approval-details dd{color:#f5f9fb;font:13px/1.45 Arial,Helvetica,sans-serif;white-space:pre-wrap}.approval-details dt:last-of-type,.approval-details dd:last-of-type{border-bottom:0}.approval-actions{display:grid;flex:0 0 auto;grid-template-columns:1fr 1fr;gap:10px;padding:14px 24px 18px;border-top:1px solid rgb(255 255 255 / 35%);background:var(--blue-deep)}.approval-actions button{min-height:48px;margin:0}.approval-actions button.is-processing:before{width:15px;height:15px;content:"";border:2px solid currentcolor;border-right-color:transparent;border-radius:50%;animation:approval-spin .7s linear infinite}.approval-actions button:disabled{opacity:.72}@keyframes approval-spin{to{transform:rotate(360deg)}}.network-choice{display:grid;grid-template-columns:1fr 1fr;width:100%;margin:0 0 18px;border:1px solid #5b8caf;background:#0a1b27}.network-choice button{min-height:42px;border:0;border-right:1px solid #315466;background:transparent;color:#b9cfdb;font:700 13px/1 Arial,Helvetica,sans-serif}.network-choice button:last-child{border-right:0}.network-choice button.active{background:#43d6e8;color:#06151e}.drawer-network{margin:8px 0 0}.settings-help{display:block;margin-top:8px;color:#a9c2cf;font-size:12px;line-height:1.45} diff --git a/extension/dist/chrome/assets/print-DTb91-bp.css b/extension/dist/chrome/assets/print-DTb91-bp.css new file mode 100644 index 0000000..b6c42aa --- /dev/null +++ b/extension/dist/chrome/assets/print-DTb91-bp.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{max-width:760px;margin:0 auto;padding:34px;color:#111820;font:14px/1.45 Arial,Helvetica,sans-serif}header{display:flex;align-items:center;gap:15px;padding-bottom:18px;border-bottom:3px solid #2167a5}header img{width:58px}h1{margin:0;font-size:24px}header p{margin:2px 0 0;color:#5c6972}.warning{margin:22px 0;border:2px solid #9b1c1c;padding:12px;color:#7a1111;font-weight:700}section{margin-top:18px}h2{margin:0 0 7px;color:#42515b;font-size:12px;text-transform:uppercase}pre{margin:0;overflow-wrap:anywhere;white-space:pre-wrap;border:1px solid #aebac2;padding:11px;font:10px/1.35 Consolas,monospace}.print-error{color:#9b1c1c;font-weight:700}@media print{body{max-width:none;padding:0}} diff --git a/extension/dist/chrome/background.js b/extension/dist/chrome/background.js new file mode 100644 index 0000000..79b546a --- /dev/null +++ b/extension/dist/chrome/background.js @@ -0,0 +1,3 @@ +import{c as St}from"./chunks/core-D2IK1U91.js";const z="contractlessWallet",F="contractlessWallets",B="contractlessSettings",V="contractlessAutoLock",x="contractlessUnlockedWallet",M="contractlessApplicationSessions",pt=12e4,At=10,ot={testnetApiUrl:"https://api.contractless.dev",autoLockMinutes:60};let s;const q="pending-provider-approvals";async function S(){const t=(await chrome.storage.local.get(B))[B]??{},r=t.selectedNetwork==="mainnet"?"mainnet":"testnet",a=t.testnetApiUrl??t.apiUrl??ot.testnetApiUrl,l=t.mainnetApiUrl??"";return{selectedNetwork:r,testnetApiUrl:a,mainnetApiUrl:l,apiUrl:r==="mainnet"?l:a,autoLockMinutes:t.autoLockMinutes??ot.autoLockMinutes}}function y(e){if(!e.apiUrl)throw new Error(`No ${e.selectedNetwork} API is configured.`);return e.apiUrl}function ut(e){if(e.endsWith(".cltc"))return"testnet";if(e.endsWith(".clc"))return"mainnet";throw new Error("The wallet address does not identify a supported Contractless network.")}function ht(e){const t=JSON.parse(e);if(typeof t.short_address!="string"||typeof t.public_key!="string")throw new Error("The encrypted wallet metadata is invalid.");return{address:t.short_address,publicKey:t.public_key,network:ut(t.short_address)}}async function C(){const e=await chrome.storage.local.get([F,z]),t=e[F];if((t==null?void 0:t.version)===2&&Array.isArray(t.wallets))return t;if((t==null?void 0:t.version)===1&&Array.isArray(t.wallets)){const f=t,v=f.wallets.map(b=>({...b,network:ut(b.address)})),m=v.find(b=>b.id===f.activeWalletId),A={version:2,activeWalletIds:m?{[m.network]:m.id}:{},wallets:v};return await L(A),A}const r=e[z];if(typeof r!="string")return{version:2,activeWalletIds:{},wallets:[]};const a=ht(r),l={id:crypto.randomUUID(),label:"Contractless Account 1",address:a.address,publicKey:a.publicKey,walletJson:r,network:a.network},d={version:2,activeWalletIds:{[a.network]:l.id},wallets:[l]};return await chrome.storage.local.set({[F]:d}),await chrome.storage.local.remove(z),d}async function L(e){await chrome.storage.local.set({[F]:e})}function O(e,t){const r=e.wallets.filter(a=>a.network===t);return r.find(a=>a.id===e.activeWalletIds[t])??r[0]}async function R(e){const t=await C(),r=ht(e),a=t.wallets.find(d=>d.address===r.address);if(a)return a.walletJson=e,a.publicKey=r.publicKey,t.activeWalletIds[r.network]=a.id,await L(t),a;const l={id:crypto.randomUUID(),label:`Contractless Account ${t.wallets.filter(d=>d.network===r.network).length+1}`,address:r.address,publicKey:r.publicKey,walletJson:e,network:r.network};return t.wallets.push(l),t.activeWalletIds[r.network]=l.id,await L(t),l}async function E(){await gt();const e=await C(),t=await S(),r=O(e,t.selectedNetwork),a=e.wallets.filter(l=>l.network===t.selectedNetwork);return{exists:a.length>0,unlocked:!!(s&&s.walletId===(r==null?void 0:r.id)&&s.network===t.selectedNetwork),activeWalletId:r==null?void 0:r.id,activeWalletLabel:r==null?void 0:r.label,address:s==null?void 0:s.short_address,publicKey:s==null?void 0:s.public_key,wallets:a.map(l=>({id:l.id,label:l.label,address:l.address,publicKey:l.publicKey,network:l.network,active:l.id===(r==null?void 0:r.id)})),selectedNetwork:t.selectedNetwork,networkName:t.selectedNetwork==="testnet"?"Testnet":"Mainnet",networkSymbol:t.selectedNetwork==="testnet"?"CLTC":"CLC",testnetApiUrl:t.testnetApiUrl,mainnetApiUrl:t.mainnetApiUrl,apiUrl:t.apiUrl,autoLockMinutes:t.autoLockMinutes}}async function T(){if(!s)return;const e=await S();s.expiresAt=Date.now()+e.autoLockMinutes*6e4,await chrome.storage.session.set({[x]:s}),await chrome.alarms.clear(V),await chrome.alarms.create(V,{when:s.expiresAt})}function Tt(e,t){const r=new Uint8Array(e);let a="";const l=32768;for(let d=0;d{});if(!d.ok||(f==null?void 0:f.success)!==!0)throw new Error((f==null?void 0:f.error)??`Wallet registration failed with HTTP ${d.status}.`);return f.data}async function Et(e,t){var f;const r=await S();if(!r.apiUrl)return;const a=e.create_handshake_proof(t.public_key,t.private_key),l=await fetch(`${y(r)}/api/v1/addresses/registration?address=${encodeURIComponent(t.short_address)}`,{headers:{"X-Contractless-Address":t.short_address,"X-Contractless-Public-Key":t.public_key,"X-Contractless-Signature":a}}),d=await l.json().catch(()=>{});if(!l.ok||(d==null?void 0:d.success)!==!0)throw new Error((d==null?void 0:d.error)??`Wallet registration lookup failed with HTTP ${l.status}.`);((f=d.data)==null?void 0:f.registered)!==!0&&await G(e,t)}async function gt(){const e=await S();if(s){if(s.expiresAt>Date.now()&&s.network===e.selectedNetwork)return;await D();return}const r=(await chrome.storage.session.get(x))[x];if(!r||r.expiresAt<=Date.now()){await chrome.storage.session.remove(x);return}if(r.network!==e.selectedNetwork){await chrome.storage.session.remove(x),await Q();return}r.sessionId||(r.sessionId=crypto.randomUUID(),await Q(),await chrome.storage.session.set({[x]:r})),s=r}async function D(){s&&(s.private_key="",s=void 0),await chrome.storage.session.remove([x,M]),await chrome.alarms.clear(V)}async function et(){const t=(await chrome.storage.session.get(M))[M];return Array.isArray(t)?t:[]}async function ft(e){await chrome.storage.session.set({[M]:e})}async function Q(){await chrome.storage.session.remove(M)}function Nt(e){const t=crypto.getRandomValues(new Uint8Array(e));return Array.from(t,r=>r.toString(16).padStart(2,"0")).join("")}async function It(e){const t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return Array.from(new Uint8Array(t),r=>r.toString(16).padStart(2,"0")).join("")}function J(e){return!e.params||typeof e.params!="object"?{}:e.params}function P(e){const t=String(e??"").trim();if(!/^[A-Za-z0-9_-]{5,32}$/.test(t))throw new Error("The application ID must contain 5 to 32 URL-safe characters.");return t}function rt(e,t){if(String(e.origin??"")!==t)throw new Error("The application origin does not match the requesting website.")}function _t(e){const t=Number(e),r=Math.floor(Date.now()/1e3);if(!Number.isInteger(t)||Math.abs(r-t)>300)throw new Error("The application connection timestamp must be within five minutes.");return t}function mt(e){const t=String(e??"").trim().toLowerCase();if(!/^[0-9a-f]{32,128}$/.test(t)||t.length%2!==0)throw new Error("The application nonce must contain 16 to 64 random hexadecimal bytes.");return t}function kt(e){const t=String(e??"").trim();if(!t||new TextEncoder().encode(t).length>75)throw new Error("The application connection message must contain 1 to 75 bytes.");return t}function yt(e){const t=String(e??"").trim();if(!t||new TextEncoder().encode(t).length>75)throw new Error("A signed website message must contain 1 to 75 bytes.");return t}function it(e,t){if(e==="contractless_connect"||e==="contractless_signMessage")return"safe";const r=t.transaction;if(!r||typeof r!="object")return"high";const a=Number(r.txtype);return Number.isInteger(a)&&a>=100&&a<=113?"low":"high"}function vt(e){if(!e||typeof e!="object")throw new Error("A complete transaction object is required.");const t=e,r=Number(t.txtype);if(!Number.isInteger(r))throw new Error("The transaction type is invalid.");if(r===6||r===7)throw new Error("Swaps and loan creation require a staged signing workflow.");return t}function Z(e){if(!e||typeof e!="object")throw new Error("A complete or partially signed transaction object is required.");const t=e,r=t.unsigned_swap??t.unsigned_loan_contract??t;if(!r||typeof r!="object")throw new Error("The unsigned transaction fields are missing.");const a=Number(r.txtype);if(a!==6&&a!==7)throw new Error("Staged signing is only available for swaps and loan creation.");return t}async function ct(e,t){var b;if(!s)throw new Error("Wallet is locked.");const r=t.unsigned_swap??t.unsigned_loan_contract??t;if(!r||typeof r!="object")return;const a=r,l=Number(a.txtype),d=l===2?[["Receiving",String(a.receiver??"")]]:l===6?[["First swap participant",String(a.sender1??"")],["Second swap participant",String(a.sender2??"")]]:l===7?[["Lender",String(a.lender??"")],["Borrower",String(a.borrower??"")]]:[];if(d.length===0)return;e.inspect_transaction_request(JSON.stringify(t));const f=await S(),v=e.create_handshake_proof(s.public_key,s.private_key),m={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":v},A=new Map;for(const[W,K]of d){const N=K.trim().toLowerCase();let I=A.get(N);if(I===void 0){const $=await fetch(`${y(f)}/api/v1/addresses/registration?address=${encodeURIComponent(N)}`,{headers:m}),n=await $.json().catch(()=>{});if(!$.ok||(n==null?void 0:n.success)!==!0)throw new Error((n==null?void 0:n.error)??`Address registration lookup failed with HTTP ${$.status}.`);I=((b=n.data)==null?void 0:b.registered)===!0,A.set(N,I)}if(!I)throw new Error(`${W} wallet is not registered.`)}}async function bt(e,t){if(t.complete!==!0||typeof t.bytes_hex!="string"||typeof t.hash!="string")throw new Error("The transaction could not be completed.");if(!s)throw new Error("Wallet is locked.");const r=await S(),a=e.create_handshake_proof(s.public_key,s.private_key),l=await fetch(`${y(r)}/api/v1/transactions/broadcast`,{method:"POST",headers:{"Content-Type":"application/json","X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":a},body:JSON.stringify({transaction_hex:t.bytes_hex})}),d=await l.json().catch(()=>{});if(!l.ok||(d==null?void 0:d.success)!==!0)throw new Error((d==null?void 0:d.error)??`Transaction broadcast failed with HTTP ${l.status}.`);return await T(),{txid:t.hash,bytes:t.bytes_hex,broadcast:d.data}}async function U(e,t){if(!s)throw new Error("Wallet is locked.");rt(e,t);const r=P(e.appId),a=String(e.accessKey??""),l=(await et()).find(d=>d.origin===t&&d.appId===r&&d.accessKey===a&&d.walletId===(s==null?void 0:s.walletId)&&d.unlockSessionId===(s==null?void 0:s.sessionId));if(!l)throw new Error("The application session is invalid or has expired. Reconnect the wallet.");return l}async function Y(){const t=(await chrome.storage.session.get(q))[q],r=Array.isArray(t)?t.filter(m=>typeof m=="string"&&m.length>0):typeof t=="string"&&t.length>0?[t]:[];if(r.length===0)return[];const a=r.map(m=>`approval:${m}`),l=await chrome.storage.session.get(a),d=Date.now(),f=[],v=[];for(const m of r){const A=`approval:${m}`,b=l[A];!b||d-b.createdAt>=pt?v.push(A):f.push(m)}return(v.length>0||f.length!==r.length)&&(await chrome.storage.session.set({[q]:f}),v.length>0&&await chrome.storage.session.remove(v)),f}async function tt(e){const t=(await Y()).filter(r=>r!==e);return await chrome.storage.session.set({[q]:t}),t}async function lt(e){const t=await Y(),r=await chrome.storage.session.get(t.map(l=>`approval:${l}`));if(e.method==="contractless_connect"&&t.some(l=>{const d=r[`approval:${l}`];if(!d||d.method!=="contractless_connect")return!1;const f=J(d),v=J(e);return d.origin===e.origin&&f.appId===v.appId}))throw new Error("This application already has a wallet connection request pending.");if(t.length>=At)throw new Error("The wallet approval queue is full. Resolve a pending request first.");await chrome.storage.session.set({[q]:[...t,e.id],[`approval:${e.id}`]:e});try{await chrome.action.openPopup()}catch{}}async function dt(e){const t=`approval-result:${e}`,r=Date.now()+pt;for(;Date.now()setTimeout(l,150))}throw await chrome.storage.session.remove([`approval:${e}`,t]),await tt(e),new Error("Wallet approval request expired.")}function $t(e){const t=e.url??e.origin;if(!t)throw new Error("The requesting website origin is unavailable.");const r=new URL(t).origin;if(r==="null")throw new Error("Requests from opaque origins are not supported.");return r}async function wt(e,t){if(!s)throw new Error("Wallet is locked.");switch(await T(),t.method){case"contractless_connect":{const r=J(t);rt(r,t.origin);const a=String(r.walletSessionId??"");if(a&&a!==s.sessionId)throw new Error("The active wallet changed while the connection was being approved.");const l=P(r.appId),d=_t(r.timestamp),f=mt(r.nonce),v=kt(r.message),m=Nt(32),A=await It(m),b=Math.floor(Date.now()/1e3),W=Math.min(b+900,Math.floor(s.expiresAt/1e3)),K=["Contractless application connection",`Application ID: ${l}`,`Origin: ${t.origin}`,`Requested timestamp: ${d}`,`Message: ${v}`,`Session key hash: ${A}`].join(` +`),N=JSON.parse(e.sign_message_request(JSON.stringify({origin:t.origin,message:K,nonce:f,issued_at:b,expires_at:W}),s.private_key,s.public_key)),I={origin:t.origin,appId:l,accessKey:m,walletId:s.walletId,unlockSessionId:s.sessionId,createdAt:Date.now()},$=await et();return $.push(I),await ft($),{address:s.short_address,publicKey:s.public_key,signature:N.signature,proof:N,accessKey:m,appId:l,origin:t.origin,sessionExpiresAt:s.expiresAt}}case"contractless_signMessage":{const r=J(t);await U(r,t.origin);const a=Math.floor(Date.now()/1e3),l=Number(r.expires_at??a+300),d=P(r.appId),f={origin:t.origin,message:["Contractless application message",`Application ID: ${d}`,`Message: ${yt(r.message)}`].join(` +`),nonce:String(r.nonce??""),issued_at:a,expires_at:l};return JSON.parse(e.sign_message_request(JSON.stringify(f),s.private_key,s.public_key))}case"contractless_sendTransaction":{const r=J(t);await U(r,t.origin);const a=vt(r.transaction),l=JSON.parse(e.sign_transaction_request(JSON.stringify(a),s.private_key,s.public_key,1));return bt(e,l)}case"contractless_signDualTransaction":{const r=J(t);await U(r,t.origin);const a=Z(r.transaction),l=Number(r.signerSlot);if(l!==1&&l!==2)throw new Error("The signer slot must be 1 or 2.");const d=JSON.parse(e.sign_transaction_request(JSON.stringify(a),s.private_key,s.public_key,l));return{complete:!1,txid:d.hash,unsignedBytes:d.unsigned_bytes_hex,canonicalUnsignedJson:JSON.parse(e.inspect_transaction_request(JSON.stringify(a))).canonical_unsigned_json,signedTransaction:d.signed_transaction,signature:l===1?d.signature1:d.signature2,signature1:d.signature1,signature2:d.signature2,signerSlot:l,signerPublicKey:s.public_key}}default:throw new Error("The website requested an unsupported wallet method.")}}async function Ct(e,t){var l,d,f,v,m,A,b,W,K,N,I,$;const r=await S();if(e.type==="switch-network"){const n=e.network==="mainnet"?"mainnet":"testnet";await D();const i={...r,selectedNetwork:n,apiUrl:n==="mainnet"?r.mainnetApiUrl:r.testnetApiUrl};return await chrome.storage.local.set({[B]:i}),E()}const a=await St(r.selectedNetwork);switch(await gt(),e.type){case"status":return E();case"create-wallet":{const n=String(e.password),i=a.generate_browser_wallet(n),o=JSON.parse(a.unlock_browser_wallet(i,n)),u=await R(i);if(await j({walletId:u.id,network:r.selectedNetwork,...o}),r.apiUrl)try{await G(a,o)}catch(c){console.warn("Automatic wallet registration failed during creation.",c)}return E()}case"import-wallet":{const n=a.import_legacy_wallet(String(e.walletJson),String(e.legacyPassword),String(e.password)),i=await R(n),o=JSON.parse(a.unlock_browser_wallet(n,String(e.password)));return await j({walletId:i.id,network:r.selectedNetwork,...o}),E()}case"import-wallet-image":{const n=String(e.password),i=a.import_private_key_image(String(e.imageBase64),String(e.imagePassword),n),o=await R(i),u=JSON.parse(a.unlock_browser_wallet(i,n));return await j({walletId:o.id,network:r.selectedNetwork,...u}),E()}case"import-private-key":{const n=String(e.password),i=a.import_private_key(String(e.privateKey),n),o=await R(i),u=JSON.parse(a.unlock_browser_wallet(i,n));return await j({walletId:o.id,network:r.selectedNetwork,...u}),E()}case"unlock":{const n=await C(),i=O(n,r.selectedNetwork);if(!i)throw new Error("No wallet exists.");const o=JSON.parse(a.unlock_browser_wallet(i.walletJson,String(e.password)));await j({walletId:i.id,network:r.selectedNetwork,...o});try{await Et(a,o)}catch(u){console.warn("Automatic wallet registration failed during unlock.",u)}return E()}case"switch-wallet":{const n=await C(),i=String(e.walletId),o=n.wallets.find(u=>u.id===i);if(!o||o.network!==r.selectedNetwork)throw new Error("The selected wallet does not exist.");return await D(),n.activeWalletIds[r.selectedNetwork]=i,await L(n),E()}case"rename-wallet":{const n=await C(),i=n.wallets.find(u=>u.id===String(e.walletId));if(!i)throw new Error("The selected wallet does not exist.");const o=String(e.label).trim();if(o.length<1||o.length>40)throw new Error("Wallet labels must contain between 1 and 40 characters.");return i.label=o,await L(n),E()}case"lock":return await D(),E();case"save-settings":{const n=String(e.testnetApiUrl??r.testnetApiUrl).replace(/\/+$/,""),i=String(e.mainnetApiUrl??r.mainnetApiUrl).replace(/\/+$/,""),o={selectedNetwork:r.selectedNetwork,testnetApiUrl:n,mainnetApiUrl:i,apiUrl:r.selectedNetwork==="mainnet"?i:n,autoLockMinutes:Math.max(1,Number(e.autoLockMinutes))};return await chrome.storage.local.set({[B]:o}),await T(),E()}case"register-wallet":{if(!s)throw new Error("Wallet is locked.");const n=await G(a,s);return await T(),n}case"wallet-balances":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},u=encodeURIComponent(s.short_address),[c,h,p]=await Promise.all([fetch(`${y(n)}/api/v1/balances?address=${u}`,{headers:o}),fetch(`${y(n)}/api/v1/tokens`,{headers:o}),fetch(`${y(n)}/api/v1/nfts`,{headers:o})]),[w,_,g]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{}),p.json().catch(()=>{})]);if(!c.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Balance lookup failed with HTTP ${c.status}.`);return await T(),{address:s.short_address,balances:((l=w.data)==null?void 0:l.balances)??[],tokens:h.ok&&(_==null?void 0:_.success)===!0?((d=_.data)==null?void 0:d.tokens)??[]:[],nfts:p.ok&&(g==null?void 0:g.success)===!0?((f=g.data)==null?void 0:f.nfts)??[]:[],tokenCatalogError:h.ok&&(_==null?void 0:_.success)===!0?void 0:(_==null?void 0:_.error)??`Token lookup failed with HTTP ${h.status}.`,nftCatalogError:p.ok&&(g==null?void 0:g.success)===!0?void 0:(g==null?void 0:g.error)??`NFT lookup failed with HTTP ${p.status}.`}}case"wallet-nft-details":{if(!s)throw new Error("Wallet is locked.");const n=String(e.name??"").trim(),i=Number(e.series??0);if(!n||n.length>15||!Number.isInteger(i)||i<0||i>4294967295)throw new Error("A valid NFT or RWA identifier is required.");const o=await S(),u=a.create_handshake_proof(s.public_key,s.private_key),c={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":u},h=`name=${encodeURIComponent(n)}&series=${i}`,[p,w]=await Promise.all([fetch(`${y(o)}/api/v1/nfts/details?${h}`,{headers:c}),fetch(`${y(o)}/api/v1/nfts/media?${h}`,{headers:c})]),[_,g]=await Promise.all([p.json().catch(()=>{}),w.json().catch(()=>{})]);if(!p.ok||(_==null?void 0:_.success)!==!0)throw new Error((_==null?void 0:_.error)??`NFT details lookup failed with HTTP ${p.status}.`);let k="",st=!1,at={},X;if(w.ok&&(g==null?void 0:g.success)===!0){at=(v=g.data)!=null&&v.metadata&&typeof g.data.metadata=="object"?g.data.metadata:{},st=((m=g.data)==null?void 0:m.image_available)===!0;const nt=String(((A=g.data)==null?void 0:A.image_url)??"");if(nt)try{const H=await fetch(new URL(nt,`${y(o)}/`).toString(),{headers:c});H.ok?k=Tt(await H.arrayBuffer(),H.headers.get("Content-Type")??"image/png"):X=`NFT image returned HTTP ${H.status}.`}catch{X="NFT image could not be retrieved."}}else X=(g==null?void 0:g.error)??`NFT media lookup failed with HTTP ${w.status}.`;return await T(),{..._.data,metadata:at,image_data_url:k,image_available:st,media_error:X}}case"wallet-receive-address":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o=encodeURIComponent(s.short_address),u=await fetch(`${y(n)}/api/v1/addresses/vanity?address=${o}`,{headers:{"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i}}),c=await u.json().catch(()=>{});if(!u.ok||(c==null?void 0:c.success)!==!0)throw new Error((c==null?void 0:c.error)??`Vanity address lookup failed with HTTP ${u.status}.`);await T();const h=typeof((b=c.data)==null?void 0:b.vanity_address)=="string"&&c.data.vanity_address.trim()!==""?c.data.vanity_address:void 0;return{shortAddress:s.short_address,receiveAddress:h??s.short_address,vanity:!!h}}case"wallet-prepare-transfer":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},u=String(e.receiver??"").trim().toLowerCase(),c=s.short_address.endsWith(".cltc")?"cltc":"clc";let h=u;if(new RegExp(`^[0-9a-f]{40}\\.${c}$`).test(u)){const g=await fetch(`${y(n)}/api/v1/addresses/registration?address=${encodeURIComponent(h)}`,{headers:o}),k=await g.json().catch(()=>{});if(!g.ok||(k==null?void 0:k.success)!==!0)throw new Error((k==null?void 0:k.error)??`Address lookup failed with HTTP ${g.status}.`);if(((K=k.data)==null?void 0:K.registered)!==!0)throw new Error("The receiving wallet is not registered.")}else{if(!new RegExp(`^[a-z]{1,20}\\.${c}$`).test(u))throw new Error(`Enter a valid Contractless ${c.toUpperCase()} address.`);const g=await fetch(`${y(n)}/api/v1/addresses/vanity/resolve?address=${encodeURIComponent(u)}`,{headers:o}),k=await g.json().catch(()=>{});if(!g.ok||(k==null?void 0:k.success)!==!0)throw new Error((k==null?void 0:k.error)??`Vanity address lookup failed with HTTP ${g.status}.`);if(h=String(((W=k.data)==null?void 0:W.owner_address)??""),!h)throw new Error("That vanity address is not registered.")}const p=Number(e.nftSeries??0);if(!Number.isInteger(p)||p<0||p>4294967295)throw new Error("The NFT/RWA series number is invalid.");const w={txtype:2,time:Math.floor(Date.now()/1e3),value:String(e.valueAtomic??""),coin:String(e.asset??"").trim().padEnd(15," "),nft_series:p,sender:s.short_address,receiver:h,txfee:String(e.feeAtomic??"")},_=JSON.parse(a.inspect_transaction_request(JSON.stringify(w)));return await T(),{transaction:w,review:_.review,hash:_.hash,receiverInput:u}}case"wallet-broadcast-transfer":{if(!s)throw new Error("Wallet is locked.");const n=e.transaction;if(!n||typeof n!="object")throw new Error("The reviewed transfer is unavailable.");const i=JSON.parse(a.sign_transaction_request(JSON.stringify(n),s.private_key,s.public_key,1));return bt(a,i)}case"wallet-activity":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o=encodeURIComponent(s.short_address),u={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},[c,h]=await Promise.all([fetch(`${y(n)}/api/v1/addresses/latest?address=${o}&limit=25`,{headers:u}),fetch(`${y(n)}/api/v1/height`,{headers:u})]),[p,w]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{})]);if(!c.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Activity lookup failed with HTTP ${c.status}.`);if(!h.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Block height lookup failed with HTTP ${h.status}.`);return await T(),{address:s.short_address,current_height:Number(((N=w.data)==null?void 0:N.height)??0),transactions:Array.isArray((I=p.data)==null?void 0:I.transactions)?p.data.transactions:[]}}case"wallet-transaction":{if(!s)throw new Error("Wallet is locked.");const n=String(e.txid);if(!/^[0-9a-f]{64}$/i.test(n))throw new Error("A valid transaction ID is required.");const i=await S(),o=a.create_handshake_proof(s.public_key,s.private_key),u={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":o},[c,h]=await Promise.all([fetch(`${y(i)}/api/v1/transactions/lookup?txid=${encodeURIComponent(n)}`,{headers:u}),fetch(`${y(i)}/api/v1/height`,{headers:u})]),[p,w]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{})]);if(!c.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Transaction lookup failed with HTTP ${c.status}.`);if(!h.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Block height lookup failed with HTTP ${h.status}.`);return await T(),{...p.data,current_height:Number((($=w.data)==null?void 0:$.height)??0)}}case"export-wallet":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");return n.walletJson}case"export-legacy-wallet":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");return a.export_legacy_wallet(n.walletJson,String(e.password),String(e.legacyPassword))}case"export-private-key-image":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");const i=JSON.parse(a.export_legacy_wallet(n.walletJson,String(e.password),String(e.imagePassword)));if(typeof i.private_key!="string")throw new Error("The wallet image could not be created.");return i.private_key}case"reveal-private-key":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");const i=JSON.parse(a.unlock_browser_wallet(n.walletJson,String(e.password)));if(typeof i.private_key!="string")throw new Error("The private key could not be decrypted.");return await T(),{label:n.label,address:n.address,privateKey:i.private_key}}case"provider-request":{const n=$t(t),i=String(e.method),o=e.params&&typeof e.params=="object"?e.params:{};if(i==="contractless_detect")return{installed:!0,providerVersion:"1.0.0"};if(i==="contractless_connect"){rt(o,n);const c=P(o.appId),h=_t(o.timestamp);mt(o.nonce);const p=kt(o.message),w={id:crypto.randomUUID(),method:i,params:{...o,walletSessionId:s==null?void 0:s.sessionId},origin:n,createdAt:Date.now(),risk:it(i,o),display:{request:"Connect application",application_id:c,website:n,wallet_address:(s==null?void 0:s.short_address)??"Unlock wallet to continue",message:p,requested_timestamp:h,access:"Address access and future wallet requests until this wallet locks"}};if(await lt(w),!await dt(w.id))throw new Error("The wallet connection request was rejected.");return wt(a,w)}if(i==="contractless_accounts")try{return await U(o,n),s?[s.short_address]:[]}catch{return[]}if(i==="contractless_balances"){if(await U(o,n),!s)throw new Error("Wallet is locked.");const c=await S(),h=a.create_handshake_proof(s.public_key,s.private_key),p=await fetch(`${y(c)}/api/v1/balances?address=${encodeURIComponent(s.short_address)}`,{headers:{"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":h}}),w=await p.json().catch(()=>{});if(!p.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Balance lookup failed with HTTP ${p.status}.`);return await T(),w.data}if(i==="contractless_disconnect"){const c=await U(o,n),h=(await et()).filter(p=>!(p.origin===c.origin&&p.appId===c.appId&&p.accessKey===c.accessKey));return await ft(h),{disconnected:!0}}if(i==="contractless_completeDualTransaction"){await U(o,n);const c=Z(o.transaction);return JSON.parse(a.complete_dual_transaction_request(JSON.stringify(c),String(o.signature1??""),String(o.publicKey1??""),String(o.signature2??""),String(o.publicKey2??"")))}if(!["contractless_signMessage","contractless_sendTransaction","contractless_signDualTransaction"].includes(i))throw new Error("The website requested an unsupported wallet method.");if(await U(o,n),i==="contractless_sendTransaction"){const c=vt(o.transaction);await ct(a,c)}else if(i==="contractless_signDualTransaction"){const c=Z(o.transaction);await ct(a,c)}const u={id:crypto.randomUUID(),method:i,params:o,origin:n,createdAt:Date.now(),risk:it(i,o),display:i==="contractless_signMessage"?{request:"Sign a text message",application_id:P(o.appId),website:n,message:yt(o.message),warning:"This is a text signature. It cannot move wallet assets."}:i==="contractless_signDualTransaction"?(()=>{const c=JSON.parse(a.inspect_transaction_request(JSON.stringify(o.transaction)));return{application_id:P(o.appId),website:n,transaction_type:c.transaction_name,signer_slot:o.signerSlot,transaction_fields:c.review,action:"Sign and return this signature to the application without broadcasting."}})():(()=>{const c=JSON.parse(a.inspect_transaction_request(JSON.stringify(o.transaction)));return{application_id:P(o.appId),website:n,transaction_type:c.transaction_name,transaction_fields:c.review,action:"Sign and broadcast this transaction."}})()};if(await lt(u),!await dt(u.id))throw new Error("The wallet request was rejected.");return wt(a,u)}case"pending-provider-approval":{const n=await Y(),i=n[0]??"";if(!i)return null;const o=`approval:${i}`,c=(await chrome.storage.session.get(o))[o];return c?s?(c.method==="contractless_connect"&&(c.params={...c.params,walletSessionId:s.sessionId},c.display={...c.display??{},wallet_address:s.short_address},await chrome.storage.session.set({[o]:c})),{locked:!1,request:c,position:1,total:n.length}):{locked:!0}:(await tt(i),null)}case"resolve-provider-approval":{if(!s)throw new Error("Unlock the wallet before approving this request.");const n=String(e.id??""),i=await Y();if(!n||i[0]!==n)throw new Error("The wallet approval request has expired.");const o=`approval:${n}`,c=(await chrome.storage.session.get(o))[o];if(!c)throw new Error("The wallet approval request has expired.");return c.method==="contractless_connect"&&(c.params={...c.params,walletSessionId:s.sessionId},await chrome.storage.session.set({[o]:c})),await chrome.storage.session.set({[`approval-result:${n}`]:{approved:e.approved===!0}}),await chrome.storage.session.remove(o),{resolved:!0,remaining:(await tt(n)).length}}default:throw new Error("Unsupported wallet request.")}}chrome.runtime.onMessage.addListener((e,t,r)=>(Ct(e,t).then(a=>r({ok:!0,result:a})).catch(a=>r({ok:!1,error:a instanceof Error?a.message:String(a)})),!0));chrome.alarms.onAlarm.addListener(e=>{e.name===V&&D()}); diff --git a/extension/dist/chrome/chunks/core-D2IK1U91.js b/extension/dist/chrome/chunks/core-D2IK1U91.js new file mode 100644 index 0000000..ace83d1 --- /dev/null +++ b/extension/dist/chrome/chunks/core-D2IK1U91.js @@ -0,0 +1 @@ +function _e(n){let e,t;try{const i=F(n,o.__wbindgen_malloc),l=w,s=o.bytes_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function re(n,e,t,_,r){let i,l;try{const f=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),A=w,W=u(_,o.__wbindgen_malloc,o.__wbindgen_realloc),$=w,z=u(r,o.__wbindgen_malloc,o.__wbindgen_realloc),G=w,T=o.complete_dual_transaction_request(f,d,m,p,b,A,W,$,z,G);var s=T[0],a=T[1];if(T[3])throw s=0,a=0,x(T[2]);return i=s,l=a,h(s,a)}finally{o.__wbindgen_free(i,l,1)}}function oe(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.create_handshake_proof(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function ce(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.create_wallet_registration(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function ie(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.create_wallet_registration_from_keys(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function se(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.decode_transaction(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function le(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.decode_transfer(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function ae(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.export_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function de(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.generate_browser_wallet(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function fe(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.import_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function be(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.import_private_key(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function we(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.import_private_key_image(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function ge(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.inspect_transaction_request(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function ue(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.public_key_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function ye(n){const e=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),t=w,_=o.short_address_to_bytes(e,t);if(_[3])throw x(_[2]);var r=Y(_[0],_[1]).slice();return o.__wbindgen_free(_[0],_[1]*1,1),r}function me(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.sign_message_request(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function pe(n,e,t,_){let r,i;try{const a=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),m=w,p=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),b=w,A=o.sign_transaction_request(a,f,d,m,p,b,_);var l=A[0],s=A[1];if(A[3])throw l=0,s=0,x(A[2]);return r=l,i=s,h(l,s)}finally{o.__wbindgen_free(r,i,1)}}function he(n){let e,t;try{const _=F(n,o.__wbindgen_malloc),r=w,i=o.skein_128_hex(_,r);return e=i[0],t=i[1],h(i[0],i[1])}finally{o.__wbindgen_free(e,t,1)}}function ve(n){let e,t;try{const _=F(n,o.__wbindgen_malloc),r=w,i=o.skein_256_hex(_,r);return e=i[0],t=i[1],h(i[0],i[1])}finally{o.__wbindgen_free(e,t,1)}}function xe(n){let e,t;try{const _=F(n,o.__wbindgen_malloc),r=w,i=o.skein_512_hex(_,r);return e=i[0],t=i[1],h(i[0],i[1])}finally{o.__wbindgen_free(e,t,1)}}function ke(){let n,e;try{const r=o.transaction_type_registry();var t=r[0],_=r[1];if(r[3])throw t=0,_=0,x(r[2]);return n=t,e=_,h(t,_)}finally{o.__wbindgen_free(n,e,1)}}function Ae(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.unlock_browser_wallet(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function $e(n,e,t){const _=F(n,o.__wbindgen_malloc),r=w,i=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=o.verify_bytes_hex(_,r,i,l,s,a);if(f[2])throw x(f[1]);return f[0]!==0}function Te(n,e,t){const _=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=w,i=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=o.verify_hash_hex(_,r,i,l,s,a);if(f[2])throw x(f[1]);return f[0]!==0}function We(n,e,t,_){let r,i;try{const a=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),m=w,p=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),b=w,A=u(_,o.__wbindgen_malloc,o.__wbindgen_realloc),W=w,$=o.verify_message_proof(a,f,d,m,p,b,A,W);var l=$[0],s=$[1];if($[3])throw l=0,s=0,x($[2]);return r=l,i=s,h(l,s)}finally{o.__wbindgen_free(r,i,1)}}function J(){return{__proto__:null,"./contractless_browser_core_bg.js":{__proto__:null,__wbg___wbindgen_is_function_1ff95bcc5517c252:function(e){return typeof e=="function"},__wbg___wbindgen_is_object_a27215656b807791:function(e){const t=e;return typeof t=="object"&&t!==null},__wbg___wbindgen_is_string_ea5e6cc2e4141dfe:function(e){return typeof e=="string"},__wbg___wbindgen_is_undefined_c05833b95a3cf397:function(e){return e===void 0},__wbg___wbindgen_throw_344f42d3211c4765:function(e,t){throw new Error(h(e,t))},__wbg_call_a6e5c5dce5018821:function(){return U(function(e,t,_){return e.call(t,_)},arguments)},__wbg_crypto_38df2bab126b63dc:function(e){return e.crypto},__wbg_getRandomValues_c44a50d8cfdaebeb:function(){return U(function(e,t){e.getRandomValues(t)},arguments)},__wbg_length_1f0964f4a5e2c6d8:function(e){return e.length},__wbg_msCrypto_bd5a034af96bcba6:function(e){return e.msCrypto},__wbg_new_with_length_e6785c33c8e4cce8:function(e){return new Uint8Array(e>>>0)},__wbg_node_84ea875411254db1:function(e){return e.node},__wbg_process_44c7a14e11e9f69e:function(e){return e.process},__wbg_prototypesetcall_4770620bbe4688a0:function(e,t,_){Uint8Array.prototype.set.call(Y(e,t),_)},__wbg_randomFillSync_6c25eac9869eb53c:function(){return U(function(e,t){e.randomFillSync(t)},arguments)},__wbg_require_b4edbdcf3e2a1ef0:function(){return U(function(){return module.require},arguments)},__wbg_static_accessor_GLOBAL_4ef717fb391d88b7:function(){const e=typeof global>"u"?null:global;return D(e)?0:q(e)},__wbg_static_accessor_GLOBAL_THIS_8d1badc68b5a74f4:function(){const e=typeof globalThis>"u"?null:globalThis;return D(e)?0:q(e)},__wbg_static_accessor_SELF_146583524fe1469b:function(){const e=typeof self>"u"?null:self;return D(e)?0:q(e)},__wbg_static_accessor_WINDOW_f2829a2234d7819e:function(){const e=typeof window>"u"?null:window;return D(e)?0:q(e)},__wbg_subarray_3ed232c8a6baee09:function(e,t,_){return e.subarray(t>>>0,_>>>0)},__wbg_versions_276b2795b1c6a219:function(e){return e.versions},__wbindgen_cast_0000000000000001:function(e,t){return Y(e,t)},__wbindgen_cast_0000000000000002:function(e,t){return h(e,t)},__wbindgen_init_externref_table:function(){const e=o.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}}}}function q(n){const e=o.__externref_table_alloc();return o.__wbindgen_externrefs.set(e,n),e}function Y(n,e){return n=n>>>0,S().subarray(n/1,n/1+e)}function h(n,e){return Oe(n>>>0,e)}let R=null;function S(){return(R===null||R.byteLength===0)&&(R=new Uint8Array(o.memory.buffer)),R}function U(n,e){try{return n.apply(this,e)}catch(t){const _=q(t);o.__wbindgen_exn_store(_)}}function D(n){return n==null}function F(n,e){const t=e(n.length*1,1)>>>0;return S().set(n,t/1),w=n.length,t}function u(n,e,t){if(t===void 0){const s=j.encode(n),a=e(s.length,1)>>>0;return S().subarray(a,a+s.length).set(s),w=s.length,a}let _=n.length,r=e(_,1)>>>0;const i=S();let l=0;for(;l<_;l++){const s=n.charCodeAt(l);if(s>127)break;i[r+l]=s}if(l!==_){l!==0&&(n=n.slice(l)),r=t(r,_,_=l+n.length*3,1)>>>0;const s=S().subarray(r+l,r+_),a=j.encodeInto(n,s);l+=a.written,r=t(r,_,l,1)>>>0}return w=l,r}function x(n){const e=o.__wbindgen_externrefs.get(n);return o.__externref_table_dealloc(n),e}let N=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});N.decode();const Se=2146435072;let H=0;function Oe(n,e){return H+=e,H>=Se&&(N=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),N.decode(),H=e),N.decode(S().subarray(n,n+e))}const j=new TextEncoder;"encodeInto"in j||(j.encodeInto=function(n,e){const t=j.encode(n);return e.set(t),{read:n.length,written:t.length}});let w=0,o;function K(n,e){return o=n.exports,R=null,o.__wbindgen_start(),o}async function qe(n,e){if(typeof Response=="function"&&n instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(n,e)}catch(r){if(n.ok&&t(n.type)&&n.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}const _=await n.arrayBuffer();return await WebAssembly.instantiate(_,e)}else{const _=await WebAssembly.instantiate(n,e);return _ instanceof WebAssembly.Instance?{instance:_,module:n}:_}function t(_){switch(_){case"basic":case"cors":case"default":return!0}return!1}}function Re(n){if(o!==void 0)return o;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module:n}=n:console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const e=J();n instanceof WebAssembly.Module||(n=new WebAssembly.Module(n));const t=new WebAssembly.Instance(n,e);return K(t)}async function Q(n){if(o!==void 0)return o;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module_or_path:n}=n:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),n===void 0&&(n=new URL("/assets/contractless_browser_core_bg-Dl61GrlP.wasm",import.meta.url));const e=J();(typeof n=="string"||typeof Request=="function"&&n instanceof Request||typeof URL=="function"&&n instanceof URL)&&(n=fetch(n));const{instance:t,module:_}=await qe(await n,e);return K(t)}const Ee=Object.freeze(Object.defineProperty({__proto__:null,bytes_to_short_address:_e,complete_dual_transaction_request:re,create_handshake_proof:oe,create_wallet_registration:ce,create_wallet_registration_from_keys:ie,decode_transaction:se,decode_transfer:le,default:Q,export_legacy_wallet:ae,generate_browser_wallet:de,import_legacy_wallet:fe,import_private_key:be,import_private_key_image:we,initSync:Re,inspect_transaction_request:ge,public_key_to_short_address:ue,short_address_to_bytes:ye,sign_message_request:me,sign_transaction_request:pe,skein_128_hex:he,skein_256_hex:ve,skein_512_hex:xe,transaction_type_registry:ke,unlock_browser_wallet:Ae,verify_bytes_hex:$e,verify_hash_hex:Te,verify_message_proof:We},Symbol.toStringTag,{value:"Module"}));function Me(n){let e,t;try{const i=I(n,c.__wbindgen_malloc),l=g,s=c.bytes_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function je(n,e,t,_,r){let i,l;try{const f=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),A=g,W=y(_,c.__wbindgen_malloc,c.__wbindgen_realloc),$=g,z=y(r,c.__wbindgen_malloc,c.__wbindgen_realloc),G=g,T=c.complete_dual_transaction_request(f,d,m,p,b,A,W,$,z,G);var s=T[0],a=T[1];if(T[3])throw s=0,a=0,k(T[2]);return i=s,l=a,v(s,a)}finally{c.__wbindgen_free(i,l,1)}}function Le(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.create_handshake_proof(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function Fe(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.create_wallet_registration(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function Ie(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.create_wallet_registration_from_keys(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ue(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.decode_transaction(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function De(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.decode_transfer(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function Be(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.export_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ce(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.generate_browser_wallet(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function Pe(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.import_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ne(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.import_private_key(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function Ve(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.import_private_key_image(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function ze(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.inspect_transaction_request(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function Ge(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.public_key_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function He(n){const e=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),t=g,_=c.short_address_to_bytes(e,t);if(_[3])throw k(_[2]);var r=Z(_[0],_[1]).slice();return c.__wbindgen_free(_[0],_[1]*1,1),r}function Xe(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.sign_message_request(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ye(n,e,t,_){let r,i;try{const a=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),m=g,p=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),b=g,A=c.sign_transaction_request(a,f,d,m,p,b,_);var l=A[0],s=A[1];if(A[3])throw l=0,s=0,k(A[2]);return r=l,i=s,v(l,s)}finally{c.__wbindgen_free(r,i,1)}}function Ze(n){let e,t;try{const _=I(n,c.__wbindgen_malloc),r=g,i=c.skein_128_hex(_,r);return e=i[0],t=i[1],v(i[0],i[1])}finally{c.__wbindgen_free(e,t,1)}}function Je(n){let e,t;try{const _=I(n,c.__wbindgen_malloc),r=g,i=c.skein_256_hex(_,r);return e=i[0],t=i[1],v(i[0],i[1])}finally{c.__wbindgen_free(e,t,1)}}function Ke(n){let e,t;try{const _=I(n,c.__wbindgen_malloc),r=g,i=c.skein_512_hex(_,r);return e=i[0],t=i[1],v(i[0],i[1])}finally{c.__wbindgen_free(e,t,1)}}function Qe(){let n,e;try{const r=c.transaction_type_registry();var t=r[0],_=r[1];if(r[3])throw t=0,_=0,k(r[2]);return n=t,e=_,v(t,_)}finally{c.__wbindgen_free(n,e,1)}}function en(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.unlock_browser_wallet(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function nn(n,e,t){const _=I(n,c.__wbindgen_malloc),r=g,i=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=c.verify_bytes_hex(_,r,i,l,s,a);if(f[2])throw k(f[1]);return f[0]!==0}function tn(n,e,t){const _=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),r=g,i=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=c.verify_hash_hex(_,r,i,l,s,a);if(f[2])throw k(f[1]);return f[0]!==0}function _n(n,e,t,_){let r,i;try{const a=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),m=g,p=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),b=g,A=y(_,c.__wbindgen_malloc,c.__wbindgen_realloc),W=g,$=c.verify_message_proof(a,f,d,m,p,b,A,W);var l=$[0],s=$[1];if($[3])throw l=0,s=0,k($[2]);return r=l,i=s,v(l,s)}finally{c.__wbindgen_free(r,i,1)}}function ee(){return{__proto__:null,"./contractless_browser_core_bg.js":{__proto__:null,__wbg___wbindgen_is_function_1ff95bcc5517c252:function(e){return typeof e=="function"},__wbg___wbindgen_is_object_a27215656b807791:function(e){const t=e;return typeof t=="object"&&t!==null},__wbg___wbindgen_is_string_ea5e6cc2e4141dfe:function(e){return typeof e=="string"},__wbg___wbindgen_is_undefined_c05833b95a3cf397:function(e){return e===void 0},__wbg___wbindgen_throw_344f42d3211c4765:function(e,t){throw new Error(v(e,t))},__wbg_call_a6e5c5dce5018821:function(){return B(function(e,t,_){return e.call(t,_)},arguments)},__wbg_crypto_38df2bab126b63dc:function(e){return e.crypto},__wbg_getRandomValues_c44a50d8cfdaebeb:function(){return B(function(e,t){e.getRandomValues(t)},arguments)},__wbg_length_1f0964f4a5e2c6d8:function(e){return e.length},__wbg_msCrypto_bd5a034af96bcba6:function(e){return e.msCrypto},__wbg_new_with_length_e6785c33c8e4cce8:function(e){return new Uint8Array(e>>>0)},__wbg_node_84ea875411254db1:function(e){return e.node},__wbg_process_44c7a14e11e9f69e:function(e){return e.process},__wbg_prototypesetcall_4770620bbe4688a0:function(e,t,_){Uint8Array.prototype.set.call(Z(e,t),_)},__wbg_randomFillSync_6c25eac9869eb53c:function(){return B(function(e,t){e.randomFillSync(t)},arguments)},__wbg_require_b4edbdcf3e2a1ef0:function(){return B(function(){return module.require},arguments)},__wbg_static_accessor_GLOBAL_4ef717fb391d88b7:function(){const e=typeof global>"u"?null:global;return C(e)?0:E(e)},__wbg_static_accessor_GLOBAL_THIS_8d1badc68b5a74f4:function(){const e=typeof globalThis>"u"?null:globalThis;return C(e)?0:E(e)},__wbg_static_accessor_SELF_146583524fe1469b:function(){const e=typeof self>"u"?null:self;return C(e)?0:E(e)},__wbg_static_accessor_WINDOW_f2829a2234d7819e:function(){const e=typeof window>"u"?null:window;return C(e)?0:E(e)},__wbg_subarray_3ed232c8a6baee09:function(e,t,_){return e.subarray(t>>>0,_>>>0)},__wbg_versions_276b2795b1c6a219:function(e){return e.versions},__wbindgen_cast_0000000000000001:function(e,t){return Z(e,t)},__wbindgen_cast_0000000000000002:function(e,t){return v(e,t)},__wbindgen_init_externref_table:function(){const e=c.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}}}}function E(n){const e=c.__externref_table_alloc();return c.__wbindgen_externrefs.set(e,n),e}function Z(n,e){return n=n>>>0,O().subarray(n/1,n/1+e)}function v(n,e){return on(n>>>0,e)}let M=null;function O(){return(M===null||M.byteLength===0)&&(M=new Uint8Array(c.memory.buffer)),M}function B(n,e){try{return n.apply(this,e)}catch(t){const _=E(t);c.__wbindgen_exn_store(_)}}function C(n){return n==null}function I(n,e){const t=e(n.length*1,1)>>>0;return O().set(n,t/1),g=n.length,t}function y(n,e,t){if(t===void 0){const s=L.encode(n),a=e(s.length,1)>>>0;return O().subarray(a,a+s.length).set(s),g=s.length,a}let _=n.length,r=e(_,1)>>>0;const i=O();let l=0;for(;l<_;l++){const s=n.charCodeAt(l);if(s>127)break;i[r+l]=s}if(l!==_){l!==0&&(n=n.slice(l)),r=t(r,_,_=l+n.length*3,1)>>>0;const s=O().subarray(r+l,r+_),a=L.encodeInto(n,s);l+=a.written,r=t(r,_,l,1)>>>0}return g=l,r}function k(n){const e=c.__wbindgen_externrefs.get(n);return c.__externref_table_dealloc(n),e}let V=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});V.decode();const rn=2146435072;let X=0;function on(n,e){return X+=e,X>=rn&&(V=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),V.decode(),X=e),V.decode(O().subarray(n,n+e))}const L=new TextEncoder;"encodeInto"in L||(L.encodeInto=function(n,e){const t=L.encode(n);return e.set(t),{read:n.length,written:t.length}});let g=0,c;function ne(n,e){return c=n.exports,M=null,c.__wbindgen_start(),c}async function cn(n,e){if(typeof Response=="function"&&n instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(n,e)}catch(r){if(n.ok&&t(n.type)&&n.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}const _=await n.arrayBuffer();return await WebAssembly.instantiate(_,e)}else{const _=await WebAssembly.instantiate(n,e);return _ instanceof WebAssembly.Instance?{instance:_,module:n}:_}function t(_){switch(_){case"basic":case"cors":case"default":return!0}return!1}}function sn(n){if(c!==void 0)return c;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module:n}=n:console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const e=ee();n instanceof WebAssembly.Module||(n=new WebAssembly.Module(n));const t=new WebAssembly.Instance(n,e);return ne(t)}async function te(n){if(c!==void 0)return c;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module_or_path:n}=n:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),n===void 0&&(n=new URL("/assets/contractless_browser_core_bg-CM1Z7v2F.wasm",import.meta.url));const e=ee();(typeof n=="string"||typeof Request=="function"&&n instanceof Request||typeof URL=="function"&&n instanceof URL)&&(n=fetch(n));const{instance:t,module:_}=await cn(await n,e);return ne(t)}const ln=Object.freeze(Object.defineProperty({__proto__:null,bytes_to_short_address:Me,complete_dual_transaction_request:je,create_handshake_proof:Le,create_wallet_registration:Fe,create_wallet_registration_from_keys:Ie,decode_transaction:Ue,decode_transfer:De,default:te,export_legacy_wallet:Be,generate_browser_wallet:Ce,import_legacy_wallet:Pe,import_private_key:Ne,import_private_key_image:Ve,initSync:sn,inspect_transaction_request:ze,public_key_to_short_address:Ge,short_address_to_bytes:He,sign_message_request:Xe,sign_transaction_request:Ye,skein_128_hex:Ze,skein_256_hex:Je,skein_512_hex:Ke,transaction_type_registry:Qe,unlock_browser_wallet:en,verify_bytes_hex:nn,verify_hash_hex:tn,verify_message_proof:_n},Symbol.toStringTag,{value:"Module"})),P={};async function an(n="testnet"){const e=n==="mainnet"?ln:Ee;return P[n]??(P[n]=n==="mainnet"?te():Q()),await P[n],e}export{an as c}; diff --git a/extension/dist/chrome/chunks/modulepreload-polyfill-B5Qt9EMX.js b/extension/dist/chrome/chunks/modulepreload-polyfill-B5Qt9EMX.js new file mode 100644 index 0000000..3e0a78a --- /dev/null +++ b/extension/dist/chrome/chunks/modulepreload-polyfill-B5Qt9EMX.js @@ -0,0 +1 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})(); diff --git a/extension/dist/chrome/content.js b/extension/dist/chrome/content.js new file mode 100644 index 0000000..0ce4e56 --- /dev/null +++ b/extension/dist/chrome/content.js @@ -0,0 +1 @@ +const o=document.createElement("script");o.src=chrome.runtime.getURL("provider.js");o.onload=()=>o.remove();(document.head||document.documentElement).appendChild(o);window.addEventListener("message",e=>{var t;e.source!==window||((t=e.data)==null?void 0:t.source)!=="contractless-page"||chrome.runtime.sendMessage({type:"provider-request",method:e.data.method,params:e.data.params},a=>{window.postMessage({source:"contractless-wallet",id:e.data.id,response:a},window.location.origin)})}); diff --git a/extension/dist/chrome/contractless-logo-128.png b/extension/dist/chrome/contractless-logo-128.png new file mode 100644 index 0000000..86681b0 Binary files /dev/null and b/extension/dist/chrome/contractless-logo-128.png differ diff --git a/extension/dist/chrome/contractless-logo-16.png b/extension/dist/chrome/contractless-logo-16.png new file mode 100644 index 0000000..6d69407 Binary files /dev/null and b/extension/dist/chrome/contractless-logo-16.png differ diff --git a/extension/dist/chrome/contractless-logo-32.png b/extension/dist/chrome/contractless-logo-32.png new file mode 100644 index 0000000..9d949e9 Binary files /dev/null and b/extension/dist/chrome/contractless-logo-32.png differ diff --git a/extension/dist/chrome/contractless-logo-48.png b/extension/dist/chrome/contractless-logo-48.png new file mode 100644 index 0000000..3697789 Binary files /dev/null and b/extension/dist/chrome/contractless-logo-48.png differ diff --git a/extension/dist/chrome/contractless-logo.png b/extension/dist/chrome/contractless-logo.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/dist/chrome/contractless-logo.png differ diff --git a/extension/dist/chrome/import-wallet.html b/extension/dist/chrome/import-wallet.html new file mode 100644 index 0000000..941281a --- /dev/null +++ b/extension/dist/chrome/import-wallet.html @@ -0,0 +1,70 @@ + + + + + + Import Contractless Wallet + + + + + + + +
+
+ Restore Contractless wallet +

Import wallet image

+

+ Select your encrypted Contractless private-key image and enter the + encryption key originally used to create it. +

+ +
+ + + + +
+ + +
+ +
+ Processed locally by Contractless Wallet + The wallet image and encryption key are not sent to the website you were visiting. +
+ + + +
+
+ + +
+ + + diff --git a/extension/dist/chrome/import-wallet.js b/extension/dist/chrome/import-wallet.js new file mode 100644 index 0000000..7b1b204 --- /dev/null +++ b/extension/dist/chrome/import-wallet.js @@ -0,0 +1 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";const a=t=>document.getElementById(t),u=a("import-form"),l=a("wallet-image"),r=a("image-password"),s=a("import-button"),i=a("notice");m("status").then(t=>{const e=t;a("selected-network").textContent=e.networkName}).catch(t=>{i.textContent=d(t)});function w(t){let e="";for(let o=0;o{var t,e;a("image-file-name").textContent=((e=(t=l.files)==null?void 0:t[0])==null?void 0:e.name)??"Choose wallet image",i.textContent=""});a("reveal-password").addEventListener("click",t=>{const e=t.currentTarget,n=r.type==="text";r.type=n?"password":"text",e.textContent=n?"Show":"Hide"});u.addEventListener("submit",t=>{t.preventDefault(),(async()=>{var c;const e=(c=l.files)==null?void 0:c[0];if(!e)throw new Error("Choose a wallet image.");if(!r.value.trim())throw new Error("Enter the wallet image encryption key.");i.textContent="",s.disabled=!0,s.textContent="Importing wallet...";const n=w(new Uint8Array(await e.arrayBuffer())),o=await m("import-wallet-image",{imageBase64:n,imagePassword:r.value,password:r.value});r.value="",a("imported-address").textContent=o.address??"Imported wallet",a("import-panel").hidden=!0,a("success-panel").hidden=!1})().catch(e=>{i.textContent=d(e),s.disabled=!1,s.textContent="Import wallet"})});a("close-page").addEventListener("click",()=>window.close()); diff --git a/extension/dist/chrome/manifest.json b/extension/dist/chrome/manifest.json new file mode 100644 index 0000000..c42233e --- /dev/null +++ b/extension/dist/chrome/manifest.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 3, + "name": "Contractless Wallet", + "version": "0.1.1", + "description": "A local-signing Web3 wallet for Contractless.", + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "permissions": ["storage", "alarms", "activeTab", "clipboardWrite"], + "host_permissions": ["https://api.contractless.dev/*"], + "optional_host_permissions": ["https://*/"], + "action": { + "default_popup": "popup.html", + "default_title": "Contractless Wallet", + "default_icon": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + } + }, + "icons": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"], + "js": ["content.js"], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": ["provider.js"], + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"] + } + ] +} diff --git a/extension/dist/chrome/popup.html b/extension/dist/chrome/popup.html new file mode 100644 index 0000000..1aaf6fa --- /dev/null +++ b/extension/dist/chrome/popup.html @@ -0,0 +1,654 @@ + + + + + + Contractless Wallet + + + + + + +
+
+
+ Contractless +
+
+

Opening wallet

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/extension/dist/chrome/popup.js b/extension/dist/chrome/popup.js new file mode 100644 index 0000000..f57b0b4 --- /dev/null +++ b/extension/dist/chrome/popup.js @@ -0,0 +1,8 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";import{c as Yt}from"./chunks/core-D2IK1U91.js";function Cn(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var de={},De,Ct;function En(){return Ct||(Ct=1,De=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}),De}var Ue={},ee={},Et;function se(){if(Et)return ee;Et=1;let e;const t=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];return ee.getSymbolSize=function(o){if(!o)throw new Error('"version" cannot be null or undefined');if(o<1||o>40)throw new Error('"version" should be in range from 1 to 40');return o*4+17},ee.getSymbolTotalCodewords=function(o){return t[o]},ee.getBCHDigit=function(n){let o=0;for(;n!==0;)o++,n>>>=1;return o},ee.setToSJISFunction=function(o){if(typeof o!="function")throw new Error('"toSJISFunc" is not a valid function.');e=o},ee.isKanjiModeEnabled=function(){return typeof e<"u"},ee.toSJIS=function(o){return e(o)},ee}var qe={},bt;function lt(){return bt||(bt=1,(function(e){e.L={bit:1},e.M={bit:0},e.Q={bit:3},e.H={bit:2};function t(n){if(typeof n!="string")throw new Error("Param is not a string");switch(n.toLowerCase()){case"l":case"low":return e.L;case"m":case"medium":return e.M;case"q":case"quartile":return e.Q;case"h":case"high":return e.H;default:throw new Error("Unknown EC Level: "+n)}}e.isValid=function(o){return o&&typeof o.bit<"u"&&o.bit>=0&&o.bit<4},e.from=function(o,r){if(e.isValid(o))return o;try{return t(o)}catch{return r}}})(qe)),qe}var Fe,kt;function bn(){if(kt)return Fe;kt=1;function e(){this.buffer=[],this.length=0}return e.prototype={get:function(t){const n=Math.floor(t/8);return(this.buffer[n]>>>7-t%8&1)===1},put:function(t,n){for(let o=0;o>>n-o-1&1)===1)},getLengthInBits:function(){return this.length},putBit:function(t){const n=Math.floor(this.length/8);this.buffer.length<=n&&this.buffer.push(0),t&&(this.buffer[n]|=128>>>this.length%8),this.length++}},Fe=e,Fe}var Ve,At;function kn(){if(At)return Ve;At=1;function e(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}return e.prototype.set=function(t,n,o,r){const i=t*this.size+n;this.data[i]=o,r&&(this.reservedBit[i]=!0)},e.prototype.get=function(t,n){return this.data[t*this.size+n]},e.prototype.xor=function(t,n,o){this.data[t*this.size+n]^=o},e.prototype.isReserved=function(t,n){return this.reservedBit[t*this.size+n]},Ve=e,Ve}var Ke={},Nt;function An(){return Nt||(Nt=1,(function(e){const t=se().getSymbolSize;e.getRowColCoords=function(o){if(o===1)return[];const r=Math.floor(o/7)+2,i=t(o),a=i===145?26:Math.ceil((i-13)/(2*r-2))*2,l=[i-7];for(let s=1;s=0&&r<=7},e.from=function(r){return e.isValid(r)?parseInt(r,10):void 0},e.getPenaltyN1=function(r){const i=r.size;let a=0,l=0,s=0,d=null,u=null;for(let m=0;m=5&&(a+=t.N1+(l-5)),d=f,l=1),f=r.get(p,m),f===u?s++:(s>=5&&(a+=t.N1+(s-5)),u=f,s=1)}l>=5&&(a+=t.N1+(l-5)),s>=5&&(a+=t.N1+(s-5))}return a},e.getPenaltyN2=function(r){const i=r.size;let a=0;for(let l=0;l=10&&(l===1488||l===93)&&a++,s=s<<1&2047|r.get(u,d),u>=10&&(s===1488||s===93)&&a++}return a*t.N3},e.getPenaltyN4=function(r){let i=0;const a=r.data.length;for(let s=0;s=0;){const a=i[0];for(let s=0;s0){const l=new Uint8Array(this.degree);return l.set(i,a),l}return i},Je=t,Je}var We={},He={},Ye={},Rt;function Qt(){return Rt||(Rt=1,Ye.isValid=function(t){return!isNaN(t)&&t>=1&&t<=40}),Ye}var O={},It;function Zt(){if(It)return O;It=1;const e="[0-9]+",t="[A-Z $%*+\\-./:]+";let n="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";n=n.replace(/u/g,"\\u");const o="(?:(?![A-Z0-9 $%*+\\-./:]|"+n+`)(?:.|[\r +]))+`;O.KANJI=new RegExp(n,"g"),O.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),O.BYTE=new RegExp(o,"g"),O.NUMERIC=new RegExp(e,"g"),O.ALPHANUMERIC=new RegExp(t,"g");const r=new RegExp("^"+n+"$"),i=new RegExp("^"+e+"$"),a=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");return O.testKanji=function(s){return r.test(s)},O.testNumeric=function(s){return i.test(s)},O.testAlphanumeric=function(s){return a.test(s)},O}var Pt;function ce(){return Pt||(Pt=1,(function(e){const t=Qt(),n=Zt();e.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},e.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},e.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},e.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},e.MIXED={bit:-1},e.getCharCountIndicator=function(i,a){if(!i.ccBits)throw new Error("Invalid mode: "+i);if(!t.isValid(a))throw new Error("Invalid version: "+a);return a>=1&&a<10?i.ccBits[0]:a<27?i.ccBits[1]:i.ccBits[2]},e.getBestModeForData=function(i){return n.testNumeric(i)?e.NUMERIC:n.testAlphanumeric(i)?e.ALPHANUMERIC:n.testKanji(i)?e.KANJI:e.BYTE},e.toString=function(i){if(i&&i.id)return i.id;throw new Error("Invalid mode")},e.isValid=function(i){return i&&i.bit&&i.ccBits};function o(r){if(typeof r!="string")throw new Error("Param is not a string");switch(r.toLowerCase()){case"numeric":return e.NUMERIC;case"alphanumeric":return e.ALPHANUMERIC;case"kanji":return e.KANJI;case"byte":return e.BYTE;default:throw new Error("Unknown mode: "+r)}}e.from=function(i,a){if(e.isValid(i))return i;try{return o(i)}catch{return a}}})(He)),He}var Mt;function Bn(){return Mt||(Mt=1,(function(e){const t=se(),n=Gt(),o=lt(),r=ce(),i=Qt(),a=7973,l=t.getBCHDigit(a);function s(p,f,h){for(let w=1;w<=40;w++)if(f<=e.getCapacity(w,h,p))return w}function d(p,f){return r.getCharCountIndicator(p,f)+4}function u(p,f){let h=0;return p.forEach(function(w){const R=d(w.mode,f);h+=R+w.getBitsLength()}),h}function m(p,f){for(let h=1;h<=40;h++)if(u(p,h)<=e.getCapacity(h,f,r.MIXED))return h}e.from=function(f,h){return i.isValid(f)?parseInt(f,10):h},e.getCapacity=function(f,h,w){if(!i.isValid(f))throw new Error("Invalid QR Code version");typeof w>"u"&&(w=r.BYTE);const R=t.getSymbolTotalCodewords(f),C=n.getTotalCodewordsCount(f,h),B=(R-C)*8;if(w===r.MIXED)return B;const E=B-d(w,f);switch(w){case r.NUMERIC:return Math.floor(E/10*3);case r.ALPHANUMERIC:return Math.floor(E/11*2);case r.KANJI:return Math.floor(E/13);case r.BYTE:default:return Math.floor(E/8)}},e.getBestVersionForData=function(f,h){let w;const R=o.from(h,o.M);if(Array.isArray(f)){if(f.length>1)return m(f,R);if(f.length===0)return 1;w=f[0]}else w=f;return s(w.mode,w.getLength(),R)},e.getEncodedBits=function(f){if(!i.isValid(f)||f<7)throw new Error("Invalid QR Code version");let h=f<<12;for(;t.getBCHDigit(h)-l>=0;)h^=a<=0;)s^=t<0&&(i=this.data.substr(r),a=parseInt(i,10),o.put(a,l*3+1))},Ze=t,Ze}var Xe,Ut;function In(){if(Ut)return Xe;Ut=1;const e=ce(),t=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function n(o){this.mode=e.ALPHANUMERIC,this.data=o}return n.getBitsLength=function(r){return 11*Math.floor(r/2)+6*(r%2)},n.prototype.getLength=function(){return this.data.length},n.prototype.getBitsLength=function(){return n.getBitsLength(this.data.length)},n.prototype.write=function(r){let i;for(i=0;i+2<=this.data.length;i+=2){let a=t.indexOf(this.data[i])*45;a+=t.indexOf(this.data[i+1]),r.put(a,11)}this.data.length%2&&r.put(t.indexOf(this.data[i]),6)},Xe=n,Xe}var et,qt;function Pn(){if(qt)return et;qt=1;const e=ce();function t(n){this.mode=e.BYTE,typeof n=="string"?this.data=new TextEncoder().encode(n):this.data=new Uint8Array(n)}return t.getBitsLength=function(o){return o*8},t.prototype.getLength=function(){return this.data.length},t.prototype.getBitsLength=function(){return t.getBitsLength(this.data.length)},t.prototype.write=function(n){for(let o=0,r=this.data.length;o=33088&&i<=40956)i-=33088;else if(i>=57408&&i<=60351)i-=49472;else throw new Error("Invalid SJIS character: "+this.data[r]+` +Make sure your charset is UTF-8`);i=(i>>>8&255)*192+(i&255),o.put(i,13)}},tt=n,tt}var nt={exports:{}},Vt;function $n(){return Vt||(Vt=1,(function(e){var t={single_source_shortest_paths:function(n,o,r){var i={},a={};a[o]=0;var l=t.PriorityQueue.make();l.push(o,0);for(var s,d,u,m,p,f,h,w,R;!l.empty();){s=l.pop(),d=s.value,m=s.cost,p=n[d]||{};for(u in p)p.hasOwnProperty(u)&&(f=p[u],h=m+f,w=a[u],R=typeof a[u]>"u",(R||w>h)&&(a[u]=h,l.push(u,h),i[u]=d))}if(typeof r<"u"&&typeof a[r]>"u"){var C=["Could not find a path from ",o," to ",r,"."].join("");throw new Error(C)}return i},extract_shortest_path_from_predecessor_list:function(n,o){for(var r=[],i=o;i;)r.push(i),n[i],i=n[i];return r.reverse(),r},find_path:function(n,o,r){var i=t.single_source_shortest_paths(n,o,r);return t.extract_shortest_path_from_predecessor_list(i,r)},PriorityQueue:{make:function(n){var o=t.PriorityQueue,r={},i;n=n||{};for(i in o)o.hasOwnProperty(i)&&(r[i]=o[i]);return r.queue=[],r.sorter=n.sorter||o.default_sorter,r},default_sorter:function(n,o){return n.cost-o.cost},push:function(n,o){var r={value:n,cost:o};this.queue.push(r),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};e.exports=t})(nt)),nt.exports}var Kt;function Dn(){return Kt||(Kt=1,(function(e){const t=ce(),n=Rn(),o=In(),r=Pn(),i=Mn(),a=Zt(),l=se(),s=$n();function d(C){return unescape(encodeURIComponent(C)).length}function u(C,B,E){const L=[];let D;for(;(D=C.exec(E))!==null;)L.push({data:D[0],index:D.index,mode:B,length:D[0].length});return L}function m(C){const B=u(a.NUMERIC,t.NUMERIC,C),E=u(a.ALPHANUMERIC,t.ALPHANUMERIC,C);let L,D;return l.isKanjiModeEnabled()?(L=u(a.BYTE,t.BYTE,C),D=u(a.KANJI,t.KANJI,C)):(L=u(a.BYTE_KANJI,t.BYTE,C),D=[]),B.concat(E,L,D).sort(function(N,A){return N.index-A.index}).map(function(N){return{data:N.data,mode:N.mode,length:N.length}})}function p(C,B){switch(B){case t.NUMERIC:return n.getBitsLength(C);case t.ALPHANUMERIC:return o.getBitsLength(C);case t.KANJI:return i.getBitsLength(C);case t.BYTE:return r.getBitsLength(C)}}function f(C){return C.reduce(function(B,E){const L=B.length-1>=0?B[B.length-1]:null;return L&&L.mode===E.mode?(B[B.length-1].data+=E.data,B):(B.push(E),B)},[])}function h(C){const B=[];for(let E=0;E=0&&k<=6&&(_===0||_===6)||_>=0&&_<=6&&(k===0||k===6)||k>=2&&k<=4&&_>=2&&_<=4?y.set(b+k,S+_,!0,!0):y.set(b+k,S+_,!1,!0))}}function h(y){const N=y.size;for(let A=8;A>k&1)===1,y.set(T,b,S,!0),y.set(b,T,S,!0)}function C(y,N,A){const v=y.size,T=u.getEncodedBits(N,A);let b,S;for(b=0;b<15;b++)S=(T>>b&1)===1,b<6?y.set(b,8,S,!0):b<8?y.set(b+1,8,S,!0):y.set(v-15+b,8,S,!0),b<8?y.set(8,v-b-1,S,!0):b<9?y.set(8,15-b-1+1,S,!0):y.set(8,15-b-1,S,!0);y.set(v-8,8,1,!0)}function B(y,N){const A=y.size;let v=-1,T=A-1,b=7,S=0;for(let k=A-1;k>0;k-=2)for(k===6&&k--;;){for(let _=0;_<2;_++)if(!y.isReserved(T,k-_)){let X=!1;S>>b&1)===1),y.set(T,k-_,X),b--,b===-1&&(S++,b=7)}if(T+=v,T<0||A<=T){T-=v,v=-v;break}}}function E(y,N,A){const v=new n;A.forEach(function(_){v.put(_.mode.bit,4),v.put(_.getLength(),m.getCharCountIndicator(_.mode,y)),_.write(v)});const T=e.getSymbolTotalCodewords(y),b=l.getTotalCodewordsCount(y,N),S=(T-b)*8;for(v.getLengthInBits()+4<=S&&v.put(0,4);v.getLengthInBits()%8!==0;)v.putBit(0);const k=(S-v.getLengthInBits())/8;for(let _=0;_=7&&R(_,N),B(_,S),isNaN(v)&&(v=a.getBestMask(_,C.bind(null,_,A))),a.applyMask(v,_),C(_,A,v),{modules:_,version:N,errorCorrectionLevel:A,maskPattern:v,segments:T}}return Ue.create=function(N,A){if(typeof N>"u"||N==="")throw new Error("No input text");let v=t.M,T,b;return typeof A<"u"&&(v=t.from(A.errorCorrectionLevel,t.M),T=d.from(A.version),b=a.from(A.maskPattern),A.toSJISFunc&&e.setToSJISFunction(A.toSJISFunc)),D(N,T,v,b)},Ue}var rt={},ot={},zt;function Xt(){return zt||(zt=1,(function(e){function t(n){if(typeof n=="number"&&(n=n.toString()),typeof n!="string")throw new Error("Color should be defined as hex string");let o=n.slice().replace("#","").split("");if(o.length<3||o.length===5||o.length>8)throw new Error("Invalid hex color: "+n);(o.length===3||o.length===4)&&(o=Array.prototype.concat.apply([],o.map(function(i){return[i,i]}))),o.length===6&&o.push("F","F");const r=parseInt(o.join(""),16);return{r:r>>24&255,g:r>>16&255,b:r>>8&255,a:r&255,hex:"#"+o.slice(0,6).join("")}}e.getOptions=function(o){o||(o={}),o.color||(o.color={});const r=typeof o.margin>"u"||o.margin===null||o.margin<0?4:o.margin,i=o.width&&o.width>=21?o.width:void 0,a=o.scale||4;return{width:i,scale:i?4:a,margin:r,color:{dark:t(o.color.dark||"#000000ff"),light:t(o.color.light||"#ffffffff")},type:o.type,rendererOpts:o.rendererOpts||{}}},e.getScale=function(o,r){return r.width&&r.width>=o+r.margin*2?r.width/(o+r.margin*2):r.scale},e.getImageWidth=function(o,r){const i=e.getScale(o,r);return Math.floor((o+r.margin*2)*i)},e.qrToImageData=function(o,r,i){const a=r.modules.size,l=r.modules.data,s=e.getScale(a,i),d=Math.floor((a+i.margin*2)*s),u=i.margin*s,m=[i.color.light,i.color.dark];for(let p=0;p=u&&f>=u&&p"u"&&(!a||!a.getContext)&&(s=a,a=void 0),a||(d=o()),s=t.getOptions(s);const u=t.getImageWidth(i.modules.size,s),m=d.getContext("2d"),p=m.createImageData(u,u);return t.qrToImageData(p.data,i,s),n(m,d,u),m.putImageData(p,0,0),d},e.renderToDataURL=function(i,a,l){let s=l;typeof s>"u"&&(!a||!a.getContext)&&(s=a,a=void 0),s||(s={});const d=e.render(i,a,s),u=s.type||"image/png",m=s.rendererOpts||{};return d.toDataURL(u,m.quality)}})(rt)),rt}var it={},Jt;function Fn(){if(Jt)return it;Jt=1;const e=Xt();function t(r,i){const a=r.a/255,l=i+'="'+r.hex+'"';return a<1?l+" "+i+'-opacity="'+a.toFixed(2).slice(1)+'"':l}function n(r,i,a){let l=r+i;return typeof a<"u"&&(l+=" "+a),l}function o(r,i,a){let l="",s=0,d=!1,u=0;for(let m=0;m0&&p>0&&r[m-1]||(l+=d?n("M",p+a,.5+f+a):n("m",s,0),s=0,d=!1),p+1':"",f="',h='viewBox="0 0 '+m+" "+m+'"',R=''+p+f+` +`;return typeof l=="function"&&l(null,R),R},it}var Wt;function Vn(){if(Wt)return de;Wt=1;const e=En(),t=Un(),n=qn(),o=Fn();function r(i,a,l,s,d){const u=[].slice.call(arguments,1),m=u.length,p=typeof u[m-1]=="function";if(!p&&!e())throw new Error("Callback required as last argument");if(p){if(m<2)throw new Error("Too few arguments provided");m===2?(d=l,l=a,a=s=void 0):m===3&&(a.getContext&&typeof d>"u"?(d=s,s=void 0):(d=s,s=l,l=a,a=void 0))}else{if(m<1)throw new Error("Too few arguments provided");return m===1?(l=a,a=s=void 0):m===2&&!a.getContext&&(s=l,l=a,a=void 0),new Promise(function(f,h){try{const w=t.create(l,s);f(i(w,a,s))}catch(w){h(w)}})}try{const f=t.create(l,s);d(null,i(f,a,s))}catch(f){d(f)}}return de.create=t.create,de.toCanvas=r.bind(null,n.render),de.toDataURL=r.bind(null,n.renderToDataURL),de.toString=r.bind(null,function(i,a,l){return o.render(i,l)}),de}var Kn=Vn();const jn=Cn(Kn),c=e=>document.getElementById(e),zn=Array.from(document.querySelectorAll(".screen")),M=c("notice"),en=navigator.userAgent.includes("Firefox");let Le="",fe="",ge="",ie="",g,dt,Te="held",K=1,ae=0;const we=8;let x,H=[],Z,P,_e,tn="activity",G,ve="",oe="";const he=20,On={1:"Mining Reward",2:"Transfer",3:"Token Creation",4:"NFT/RWA Creation",5:"Marketing",6:"Swap",7:"Loan",8:"Loan Payment",9:"Collateral Claim",10:"Asset Burn",11:"Token Issuance",12:"Vanity Address",100:"Storage Key",101:"Data Storage Bool",102:"Data Storage U8",103:"Data Storage U16",104:"Data Storage U32",105:"Data Storage U64",106:"Data Storage U128",107:"Data Storage String",108:"Data Storage I8",109:"Data Storage I16",110:"Data Storage I32",111:"Data Storage I64",112:"Data Storage I128",113:"Delete Stored Data",200:"Proposal",201:"Proposal Vote",202:"Activation Vote"};async function $(e,t={}){const n=await chrome.runtime.sendMessage({type:e,...t});if(!(n!=null&&n.ok))throw new Error((n==null?void 0:n.error)??"Wallet request failed.");return n.result}function I(e,t=!0){zn.forEach(n=>{n.hidden=n.id!==e}),t&&(M.textContent="")}function V(e){g=e;const t=e.networkName,n=e.networkSymbol;document.querySelectorAll("[data-network-name]").forEach(o=>{o.textContent=t}),document.querySelectorAll("[data-network-symbol]").forEach(o=>{o.textContent=n}),c("base-asset-name").textContent=`Contractless ${t}`,c("wallet-address").textContent=e.address??"",c("wallet-address-short").textContent=e.address?`${e.address.slice(0,6)}...${e.address.slice(-6)}`:"Account",c("settings-testnet-api-url").value=e.testnetApiUrl,c("settings-mainnet-api-url").value=e.mainnetApiUrl,document.querySelectorAll("[data-select-network]").forEach(o=>{const r=o.dataset.selectNetwork===e.selectedNetwork;o.classList.toggle("active",r),o.setAttribute("aria-pressed",String(r))}),c("settings-lock-timeout").value=String(e.autoLockMinutes),Xn(e),e.exists?e.unlocked?at().then(o=>{o||(I("dashboard-screen"),Be())}):I("locked-screen"):I("create-screen")}function Jn(e,t){return e==="contractless_connect"?"Connect application":e==="contractless_signMessage"?"Sign text message":e==="contractless_sendTransaction"||e==="contractless_signDualTransaction"?`Sign ${String(t.transaction_type??"transaction")}`:"Review request"}function j(e,t,n){const o=document.createElement("dt");o.textContent=t.replaceAll("_"," ");const r=document.createElement("dd");r.textContent=typeof n=="object"?JSON.stringify(n,null,2):String(n),e.append(o,r)}function te(e,t,n,o){const r=re(e[t]),i=String(e[n]??"").trim().toUpperCase(),a=Number(e[o]??0);return`${r} ${i}${a>0?` #${a}`:""}`}function Ht(e,t,n){const o=n===1?2:1,r=(g==null?void 0:g.networkSymbol)??"CLTC";j(e,"Your miner tip",te(t,`tip${n}`,`ticker${n}`,`nft_series${n}`)),j(e,"Your transaction fee",`${re(t[`txfee${n}`])} ${r}`),j(e,"Other party miner tip",te(t,`tip${o}`,`ticker${o}`,`nft_series${o}`)),j(e,"Other party transaction fee",`${re(t[`txfee${o}`])} ${r}`)}function Wn(e,t){var a;const n=((a=g==null?void 0:g.address)==null?void 0:a.trim().toLowerCase())??"",o=String(t.sender1??"").trim().toLowerCase(),r=String(t.sender2??"").trim().toLowerCase(),i=n===o?1:n===r?2:0;if(i===1){j(e,"Sending",te(t,"value1","ticker1","nft_series1")),j(e,"Receiving",te(t,"value2","ticker2","nft_series2")),Ht(e,t,1);return}if(i===2){j(e,"Sending",te(t,"value2","ticker2","nft_series2")),j(e,"Receiving",te(t,"value1","ticker1","nft_series1")),Ht(e,t,2);return}j(e,"First party sending",te(t,"value1","ticker1","nft_series1")),j(e,"Second party sending",te(t,"value2","ticker2","nft_series2"))}function ye(e){const t=c("approve-provider-approval"),n=c("reject-provider-approval"),o=e!==null;t.disabled=o,n.disabled=o,t.classList.toggle("is-processing",e==="approve"),n.classList.toggle("is-processing",e==="reject"),t.textContent=e==="approve"?"Processing...":"Approve",n.textContent=e==="reject"?"Rejecting...":"Reject"}async function at(){const e=await $("pending-provider-approval");if(!(e!=null&&e.request))return!1;const t=e.request;ye(null),oe=t.id;const n=(e.total??0)>1?` · Request ${e.position??1} of ${e.total}`:"";c("provider-approval-origin").textContent=`${t.origin}${n}`;const o=c("provider-approval-risk");o.className=`risk-label risk-${t.risk}`,o.textContent=t.risk==="safe"?"Safe":t.risk==="low"?"Low risk":"High risk";const r=t.display??{},i=t.method==="contractless_sendTransaction"||t.method==="contractless_signDualTransaction";c("provider-approval-title").textContent=Jn(t.method,r),c("provider-approval-intro").textContent=i?"Review every transaction field before signing.":"Review this request before approving it.";const a=c("provider-approval-details");return a.replaceChildren(),Object.entries(r).forEach(([l,s])=>{if(l==="transaction_fields"&&s&&typeof s=="object"){const d=s,u=Number(d.txtype??0),m=new Set(["ticker1","nft_series1","value1","ticker2","nft_series2","value2","tip1","tip2","txfee1","txfee2"]);u===6&&Wn(a,d),Object.entries(d).forEach(([p,f])=>{u===6&&m.has(p)||j(a,p==="txtype"?"transaction_type_id":p,dn(u,p,f))});return}j(a,l,s)}),I("provider-approval-screen"),!0}async function nn(e,t){if(!(e>0&&await at())){if(t){const n=Date.now()+1500;for(;Date.now()setTimeout(o,75))}}window.close()}}function Hn(e){return e.replace(/[.0]/g,"").length>0}function st(){const e=c("token-list");if(e.replaceChildren(),!x){const s=document.createElement("div");s.className="empty-assets";const d=document.createElement("strong");d.textContent="Loading token balances...";const u=document.createElement("span");u.textContent="Balances are retrieved from the selected Contractless API.",s.append(d,u),e.append(s);return}const t=(g==null?void 0:g.networkSymbol)??"CLTC",n=new Set(x.tokens.map(s=>s.token.trim().toUpperCase())),o=new Set(x.nfts.map(s=>`${s.name.trim().toUpperCase()}|${Number(s.series||0)}`)),r=x.balances.filter(s=>{const d=s.asset.trim().toUpperCase(),u=`${d}|${Number(s.nft_series||0)}`;return s.nft_series===0&&d!=="CLTC"&&d!=="CLC"&&n.has(d)&&!o.has(u)}),i=new Map(r.map(s=>[s.asset.trim().toUpperCase(),s])),a=Te==="all"?x.tokens.map(s=>s.token):r.filter(s=>Hn(s.balance)).map(s=>s.asset),l=[...new Set(a.map(s=>s.trim().toUpperCase()))].filter(s=>s!==t&&!o.has(`${s}|0`)).sort((s,d)=>s.localeCompare(d));if(l.length===0){const s=document.createElement("div");s.className="empty-assets";const d=document.createElement("strong");d.textContent=Te==="held"?"No token balances found":"No tokens found";const u=document.createElement("span");u.textContent=Te==="held"?"Switch to All tokens to view the complete network token list.":x.tokenCatalogError?`Token catalog unavailable: ${x.tokenCatalogError}`:"The selected API did not return any fungible tokens.",s.append(d,u),e.append(s);return}l.forEach(s=>{const d=i.get(s),u=document.createElement("div");u.className="asset-row";const m=document.createElement("div");m.className="asset-symbol token-symbol",m.textContent=s.slice(0,3);const p=document.createElement("div");p.className="asset-name";const f=document.createElement("strong");f.textContent=s;const h=document.createElement("span");h.textContent="Contractless token",p.append(f,h);const w=document.createElement("div");w.className="asset-value";const R=document.createElement("strong");R.textContent=(d==null?void 0:d.balance)??"0.00000000";const C=document.createElement("span");C.textContent=s,w.append(R,C),u.append(m,p,w),e.append(u)})}async function Be(){if(g!=null&&g.unlocked){x=void 0,c("base-balance").textContent="--",c("base-asset-balance").textContent="--",st();try{if(!g.apiUrl)throw new Error(`No ${g.networkName} API is configured.`);const e=await $("wallet-balances");x={balances:Array.isArray(e.balances)?e.balances:[],tokens:Array.isArray(e.tokens)?e.tokens:[],nfts:Array.isArray(e.nfts)?e.nfts:[],tokenCatalogError:e.tokenCatalogError,nftCatalogError:e.nftCatalogError};const t=g.networkSymbol,n=x.balances.find(r=>r.nft_series===0&&r.asset.toUpperCase()===t),o=(n==null?void 0:n.balance)??"0.00000000";c("base-balance").textContent=o,c("base-asset-balance").textContent=o,st()}catch(e){const t=c("token-list");t.replaceChildren();const n=document.createElement("div");n.className="empty-assets balance-error";const o=document.createElement("strong");o.textContent="Unable to load balances";const r=document.createElement("span");r.textContent=F(e),n.append(o,r),t.append(n),M.textContent=F(e)}}}function Ne(e,t){return`${e.trim().toUpperCase()}|${t}`}function Yn(e){try{return Y(e,"Balance")>0n}catch{return!1}}function rn(e){if(e.ownership_type!==1)return"Complete ownership";try{const t=Y(e.balance,"Balance"),n=Y(e.supply,"Supply");if(n<=0n)return"0% ownership";const o=t*100000000n/n,r=o/1000000n,i=(o%1000000n).toString().padStart(6,"0").replace(/0+$/,"");return`${r}${i?`.${i}`:""}% ownership`}catch{return"Fractional ownership"}}function ut(e){var t;return(t=e.details)!=null&&t.metadata&&typeof e.details.metadata=="object"?e.details.metadata:{}}function ft(e){const t=ut(e).name;return typeof t=="string"&&t.trim()?t.trim():e.series>0?`${e.name} #${e.series}`:e.name}function on(e){const n=ut(e).contractless;return n&&typeof n=="object"&&String(n.metadata_type??"").toLowerCase()==="rwa"?"RWA":"NFT"}function be(e){return e.series>0?`${e.name}_${e.series}`:e.name}function Gn(e,t=7,n=7){return e.length>t+n+3?`${e.slice(0,t)}...${e.slice(-n)}`:e}function Qn(e,t){var r;const n=document.createElement("div");if(n.className=t,(r=e.details)!=null&&r.image_data_url){const i=document.createElement("img");return i.src=e.details.image_data_url,i.alt=ft(e),n.append(i),n}const o=document.createElement("span");return o.textContent=e.loading?"...":e.name.slice(0,2).toUpperCase(),n.append(o),n}function an(){const e=(K-1)*we;return H.slice(e,e+we)}function Ce(){const e=c("nfts-list");e.replaceChildren();const t=Math.max(1,Math.ceil(H.length/we));if(K=Math.min(Math.max(1,K),t),c("nfts-count").textContent=H.length===1?"1 owned asset":`${H.length} owned assets`,c("nfts-page").textContent=String(K),c("nfts-pages").textContent=String(t),c("nfts-pagination").hidden=t<=1,c("nfts-previous").disabled=K<=1,c("nfts-next").disabled=K>=t,!H.length){const n=document.createElement("div");n.className="nfts-empty";const o=document.createElement("strong");o.textContent="No NFTs or RWAs found";const r=document.createElement("span");r.textContent=x!=null&&x.nftCatalogError?`NFT catalog unavailable: ${x.nftCatalogError}`:"Assets owned by this wallet will appear here.",n.append(o,r),e.append(n);return}an().forEach(n=>{const o=document.createElement("button");o.type="button",o.className="nft-card",o.append(Qn(n,"nft-card-media"));const r=document.createElement("div");r.className="nft-card-identity";const i=document.createElement("span");i.textContent=on(n);const a=document.createElement("strong");a.textContent=ft(n);const l=document.createElement("small");l.textContent=be(n);const s=document.createElement("b");if(s.textContent=rn(n),r.append(i,a,l,s),n.error){const d=document.createElement("em");d.textContent="Media unavailable",r.append(d)}o.append(r),o.addEventListener("click",()=>Zn(n)),e.append(o)})}function Se(e,t,n){const o=document.createElement("div"),r=document.createElement("dt");r.textContent=t;const i=document.createElement("dd");i.textContent=n||"--",o.append(r,i),e.append(o)}function xe(e){Z=e;const t=e.details,n=ut(e),o=on(e),r=ft(e);c("nft-detail-title").textContent=r,c("nft-detail-kind").textContent=`${o} details`,c("nft-detail-badge").textContent=o,c("nft-detail-name").textContent=r,c("nft-detail-ownership").textContent=rn(e);const i=c("nft-detail-image");i.src=(t==null?void 0:t.image_data_url)??"",i.alt=r,i.hidden=!(t!=null&&t.image_data_url);const a=n.description??n.summary;c("nft-detail-description").textContent=typeof a=="string"&&a.trim()?a.trim():t?"No description was included in the NFT metadata.":"Loading NFT metadata and provenance...";const l=c("nft-detail-attributes");l.replaceChildren(),(Array.isArray(n.attributes)?n.attributes.slice(0,12):[]).forEach(m=>{if(!m||typeof m!="object")return;const p=m,f=document.createElement("div"),h=document.createElement("span");h.textContent=String(p.trait_type??p.type??"Attribute");const w=document.createElement("strong");w.textContent=String(p.value??""),f.append(h,w),l.append(f)}),l.hidden=l.childElementCount===0;const d=c("nft-detail-fields");d.replaceChildren(),Se(d,"Asset",be(e)),Se(d,"Creator",(t==null?void 0:t.creator)??"--"),Se(d,"IPFS CID",(t==null?void 0:t.item_ipfs)??"--"),Se(d,"Origin transaction",(t==null?void 0:t.genesis_txid)??e.origin_txid);const u=c("nft-detail-history");if(u.replaceChildren(),t)if(t.history.length)t.history.slice().reverse().forEach(m=>{const p=document.createElement("button");p.type="button",p.className="nft-history-row",p.title=`View full transaction ${m.txid}`;const f=document.createElement("strong");f.textContent=m.action;const h=document.createElement("span");h.textContent=`Block ${Number(m.block).toLocaleString()}`;const w=document.createElement("small");w.textContent=Gn(m.txid),p.append(f,h,w),p.addEventListener("click",()=>{fn({txid:m.txid,block_height:Number(m.block),transaction_hex:""},"nft")}),u.append(p)});else{const m=document.createElement("p");m.textContent="No provenance records were returned.",u.append(m)}else{const m=document.createElement("p");m.textContent="Loading provenance...",u.append(m)}}function Zn(e){xe(e),I("nft-detail-screen")}async function mt(e){const t=an().filter(r=>!r.details&&!r.loading);let n=0;const o=async()=>{for(;n[Ne(r.name,r.series),r]));H=x.nfts.map(r=>{const i=x==null?void 0:x.balances.find(l=>Ne(l.asset,Number(l.nft_series||0))===Ne(r.name,Number(r.series||0)));if(!i||!Yn(i.balance))return null;const a=n.get(Ne(r.name,r.series));return{name:r.name.trim(),series:Number(r.series||0),origin_txid:r.origin_txid,ownership_type:Number(r.ownership_type||0),supply:r.supply,balance:i.balance,details:a==null?void 0:a.details,error:a==null?void 0:a.error}}).filter(r=>r!==null).sort((r,i)=>r.name.localeCompare(i.name)||r.series-i.series);const o=Math.max(1,Math.ceil(H.length/we));K=Math.min(K,o),Ce(),await mt(t)}function Xn(e){const t=c("known-addresses");t.replaceChildren(),e.wallets.forEach(n=>{const o=document.createElement("div");o.className=`known-address${n.active?" active-address":""}`;const r=document.createElement("button");r.type="button",r.className="address-select",r.dataset.walletId=n.id;const i=document.createElement("span");i.className="address-avatar";const a=document.createElement("img");a.src=chrome.runtime.getURL("contractless-logo.png"),a.alt="",i.append(a);const l=document.createElement("span");l.className="address-details";const s=document.createElement("strong");s.textContent=n.label;const d=document.createElement("small");d.textContent=n.address,l.append(s,d);const u=document.createElement("span");u.className="selected-mark",u.textContent=n.active?"✓":"",r.append(i,l,u),r.addEventListener("click",()=>{if(n.active){z(!1);return}U(async()=>{z(!1),fe="",ie="",V(await $("switch-wallet",{walletId:n.id}))})});const m=document.createElement("button");m.type="button",m.className="rename-address",m.title="Rename wallet",m.setAttribute("aria-label",`Rename ${n.label}`),m.textContent="✎",m.addEventListener("click",()=>{const p=window.prompt("Wallet label",n.label);p===null||p.trim()===n.label||U(async()=>{const f=await $("rename-wallet",{walletId:n.id,label:p});V(f),z(!0)})}),o.append(r,m),t.append(o)})}function z(e){const t=c("account-drawer"),n=c("drawer-backdrop"),o=c("account-menu-button");t.classList.toggle("open",e),t.setAttribute("aria-hidden",String(!e)),o.setAttribute("aria-expanded",String(e)),n.hidden=!e}function gt(){c("revealed-private-key").value="",c("private-key-result").hidden=!0}function Ee(e){if(c("backup-key-modal").hidden=!e,c("backup-key-backdrop").hidden=!e,e){const t=c("backup-key-input");t.value="",t.focus()}else c("backup-key-input").value="",dt=void 0}async function er(){const e=await $("wallet-receive-address"),t=String(e.receiveAddress??"");if(!t)throw new Error("No active wallet address is available.");c("receive-address-label").textContent=e.vanity?"Vanity address":"Wallet address",c("receive-address-value").textContent=t;const n=c("receive-qr");await jn.toCanvas(n,t,{width:220,margin:1,errorCorrectionLevel:"M",color:{dark:"#071016",light:"#ffffff"}}),I("receive-screen")}function Y(e,t){const n=e.trim();if(!/^(?:0|[1-9]\d*)(?:\.\d{1,8})?$/.test(n))throw new Error(`${t} must be a positive number with no more than 8 decimal places.`);const[o,r=""]=n.split(".");return BigInt(o)*100000000n+BigInt(r.padEnd(8,"0"))}function ne(e){const t=e/100000000n,n=(e%100000000n).toString().padStart(8,"0");return`${t}.${n}`}function sn(){if(P)return P.balance;const e=c("send-asset").value,t=x==null?void 0:x.balances.find(n=>n.nft_series===0&&n.asset.trim().toUpperCase()===e);return(t==null?void 0:t.balance)??"0.00000000"}function cn(){const e=c("send-asset").value,t=(g==null?void 0:g.networkSymbol)??"CLTC";if(e!==t)return 100000000n;const n=c("send-amount").value.trim();if(!n)return 0n;try{return(Y(n,"Amount")+99n)/100n}catch{return 0n}}function Re(){const e=(P==null?void 0:P.name)??c("send-asset").value,t=(g==null?void 0:g.networkSymbol)??"CLTC",n=P?be(P):e;c("send-amount-symbol").textContent=n,c("send-available").textContent=`Available: ${sn()} ${n}`;const o=cn();c("send-fee").value=ne(o),c("send-fee-note").textContent=!P&&e===t?"Minimum fee: 1% of the transfer amount.":`Minimum fee: 1 ${t} for asset transfers.`}function tr(){const e=c("send-asset");e.replaceChildren();const t=(g==null?void 0:g.networkSymbol)??"CLTC",n=new Set([t]);x==null||x.tokens.forEach(o=>{const r=o.token.trim().toUpperCase();(x==null?void 0:x.balances.find(a=>a.nft_series===0&&a.asset.trim().toUpperCase()===r&&Y(a.balance,"Balance")>0n))&&n.add(r)}),n.forEach(o=>{const r=document.createElement("option");r.value=o,r.textContent=o,e.append(r)}),Re()}function nr(){if(!x)throw new Error("Wallet balances are still loading.");P=void 0,G=void 0,c("send-form").reset(),c("send-asset").disabled=!1,c("send-amount").disabled=!1,tr(),I("send-screen")}function rr(e){if(!x)throw new Error("Wallet balances are still loading.");P=e,G=void 0,c("send-form").reset();const t=c("send-asset");t.replaceChildren();const n=document.createElement("option");n.value=e.name,n.textContent=be(e),t.append(n),t.disabled=!0;const o=c("send-amount");e.ownership_type===0?(o.value="1.00000000",o.disabled=!0):(o.value="",o.disabled=!1),Re(),I("send-screen")}async function or(){var w;if(!(g!=null&&g.address))throw new Error("No active wallet is available.");const e=(P==null?void 0:P.name)??c("send-asset").value,t=P?be(P):e,n=Q(c("send-recipient").value,"Enter a receiving address."),o=Q(c("send-amount").value,"Enter an amount to send."),r=Q(c("send-fee").value,"Enter a transaction fee."),i=Y(o,"Amount"),a=Y(r,"Transaction fee");if(i===0n)throw new Error("The transfer amount must be greater than zero.");if((P==null?void 0:P.ownership_type)===0&&i!==100000000n)throw new Error("An indivisible NFT must be transferred as one complete asset.");if((P==null?void 0:P.ownership_type)===1&&i>100000000n)throw new Error("A fractional NFT/RWA transfer cannot exceed one complete asset.");const l=cn();if(as)throw new Error(`Insufficient ${e} balance.`);const d=g.networkSymbol,u=((w=x==null?void 0:x.balances.find(R=>R.nft_series===0&&R.asset.trim().toUpperCase()===d))==null?void 0:w.balance)??"0.00000000",m=Y(u,"Base balance");if((e===d?i+a:a)>m)throw new Error(`Insufficient ${d} balance including the fee.`);const f=await $("wallet-prepare-transfer",{receiver:n,asset:e,nftSeries:(P==null?void 0:P.series)??0,valueAtomic:i.toString(),feeAtomic:a.toString()}),h=e===d?`${ne(i+a)} ${d}`:`${ne(i)} ${t} + ${ne(a)} ${d}`;G={transaction:f.transaction,receiverInput:f.receiverInput,asset:e,amount:ne(i),fee:ne(a),total:h},c("review-amount").textContent=G.amount,c("review-asset").textContent=t,c("review-sender").textContent=g.address,c("review-recipient").textContent=f.receiverInput,c("review-resolved-recipient").textContent=String(f.review.receiver??""),c("review-fee").textContent=G.fee,c("review-total").textContent=h,I("send-review-screen")}function ln(e){if(!/^[0-9a-f]+$/i.test(e)||e.length<10)return;const t=e.slice(2,10).match(/../g);if(!t||t.length!==4)return;const n=t.reduce((r,i,a)=>r+Number.parseInt(i,16)*2**(a*8),0),o=new Date(n*1e3);return Number.isNaN(o.getTime())?void 0:o}function ir(e){return e.split("_").map(t=>t&&`${t[0].toUpperCase()}${t.slice(1)}`).join(" ")}function re(e){try{const t=ne(BigInt(String(e??"0"))),[n,o]=t.split("."),r=o.replace(/0+$/,"");return r?`${n}.${r}`:`${n}.0`}catch{return String(e??"")}}function ar(e,t){var o;return t.startsWith("txfee")?!0:((o={1:new Set(["value"]),2:new Set(["value"]),3:new Set(["number"]),6:new Set(["value1","value2","tip1","tip2"]),7:new Set(["loan_amount","collateral_amount","payment_amount","max_late_value"]),8:new Set(["payback_amount","tip"]),10:new Set(["value"]),11:new Set(["number"])}[e])==null?void 0:o.has(t))??!1}function dn(e,t,n){if(typeof n=="boolean")return n?"True":"False";if(ar(e,t))return re(n);if(t==="time"||t==="timestamp"||t==="offer_expiration"){const o=Number(n);if(Number.isFinite(o))return`${new Date(o*1e3).toLocaleString()} (${o})`}return t==="payment_period"?{d:"Daily",w:"Weekly",m:"Monthly"}[String(n).trim().toLowerCase()]??String(n??""):e===3&&t==="hard_limit"?Number(n)===1?"Yes":"No":e===4&&t==="series"?Number(n)===1?"Series":"Single NFT/RWA":e===4&&t==="ownership_type"?Number(n)===1?"Fractional ownership":"Complete ownership":(e===201||e===202)&&t==="vote"?Number(n)===1?"Approve":"Reject":(t==="previous"||t.endsWith("_hash"))&&/^0+$/.test(String(n))?"None":e===5&&(t==="impression_value"||t==="click_value")?(Number(n)/100).toFixed(2):typeof n=="string"?n.trim():String(n??"")}function q(e,t,n,o=!1,r=0){const i=document.createElement("div");i.className=`transaction-field${o?" technical-field":""}`;const a=document.createElement("span");a.textContent=ir(t);const l=document.createElement("strong");l.textContent=o?String(n??""):dn(r,t,n),i.append(a,l),e.append(i)}function ht(e,t){return!e||!t||t=he?`${n} confirmations`:`${n}/${he} confirmations`:`Pending · 0/${he} confirmations`}async function fn(e,t="activity"){_e=e,tn=t;const n=c("close-transaction");n.title=t==="nft"?"Back to NFT/RWA details":"Back to activity",n.setAttribute("aria-label",t==="nft"?"Back to NFT/RWA details":"Back to activity"),I("transaction-screen"),c("transaction-title").textContent="Loading transaction",c("transaction-header-status").textContent="",c("transaction-full-txid").textContent=e.txid,c("transaction-overview").replaceChildren(),c("transaction-fields").replaceChildren(),c("transaction-technical-fields").replaceChildren(),c("transaction-miner-earnings").replaceChildren(),c("miner-earnings-section").hidden=!0;try{const o=await $("wallet-transaction",{txid:e.txid}),r=await Yt((g==null?void 0:g.selectedNetwork)??"testnet"),i=JSON.parse(r.decode_transaction(o.transaction_hex)),l=ht(o.block_height,o.current_height){q(u,f,h,!1,i.txtype)});const m=e.miner_earnings??[];if(m.length>0){const f=c("transaction-miner-earnings");m.forEach((h,w)=>{q(f,`earning_${w+1}_type`,h.type),q(f,`earning_${w+1}_asset`,h.asset),q(f,`earning_${w+1}_nft_series`,h.nft_series),q(f,`earning_${w+1}_amount`,`${re(h.amount_atomic)} ${h.asset.trim()}`)}),c("miner-earnings-section").hidden=!1}const p=c("transaction-technical-fields");i.stored_hash&&q(p,"stored_hash",i.stored_hash,!0),i.signature&&q(p,"signature",i.signature,!0),i.signature1&&q(p,"signature_1",i.signature1,!0),i.signature2&&q(p,"signature_2",i.signature2,!0),q(p,"raw_transaction_bytes",o.transaction_hex,!0)}catch(o){c("transaction-title").textContent="Transaction unavailable";const r=c("transaction-fields");r.replaceChildren(),q(r,"error",F(o))}}function ue(e){const t=String(e??"").trim();return t.length>18?`${t.slice(0,8)}...${t.slice(-7)}`:t}function sr(e,t){const n=e.fields,o=t.trim().toLowerCase(),r=(g==null?void 0:g.networkSymbol)??"CLTC",i=s=>String(n[s]??"").trim(),a=(s,d,u="")=>`${u}${re(n[s])} ${d}`,l=()=>`-${re(n.txfee??0)} ${r}`;switch(e.txtype){case 1:return{title:"Mining reward",subtitle:"From network",amount:a("value",r,"+"),direction:"incoming"};case 2:{const s=i("coin"),d=String(n.sender??"").trim().toLowerCase()===o;return{title:d?`Sent ${s}`:`Received ${s}`,subtitle:d?`To: ${ue(n.receiver)}`:`From: ${ue(n.sender)}`,amount:a("value",s,d?"-":"+"),direction:d?"outgoing":"incoming"}}case 3:{const s=i("ticker");return{title:`Created ${s}`,subtitle:"Token creation",amount:a("number",s,"+"),secondary:l(),direction:"incoming"}}case 4:{const s=String(n.count??"1");return{title:`Created ${i("nft_name")}`,subtitle:Number(n.series??0)===0?"NFT/RWA":"NFT/RWA collection",amount:`+${s} ${Number(s)===1?"item":"items"}`,secondary:l(),direction:"incoming"}}case 6:{const s=String(n.sender1??"").trim().toLowerCase()===o,d=i(s?"ticker1":"ticker2"),u=i(s?"ticker2":"ticker1");return{title:"Completed swap",subtitle:`With: ${ue(n[s?"sender2":"sender1"])}`,amount:a(s?"value1":"value2",d,"-"),secondary:a(s?"value2":"value1",u,"+"),direction:"neutral"}}case 7:{const s=String(n.lender??"").trim().toLowerCase()===o;return{title:s?"Loan funded":"Loan received",subtitle:s?`To: ${ue(n.borrower)}`:`From: ${ue(n.lender)}`,amount:a("loan_amount",i("loan_coin"),s?"-":"+"),direction:s?"outgoing":"incoming"}}case 8:return{title:"Loan payment",subtitle:`Contract: ${ue(n.contract_hash)}`,amount:`-${re(n.payback_amount)}`,direction:"outgoing"};case 10:return{title:`Burned ${i("coin")}`,subtitle:"Removed from supply",amount:a("value",i("coin"),"-"),direction:"outgoing"};case 11:{const s=i("ticker");return{title:`Issued ${s}`,subtitle:"Additional token supply",amount:a("number",s,"+"),secondary:l(),direction:"incoming"}}case 12:return{title:"Created vanity address",subtitle:String(n.vanity_address??"").trim(),amount:l(),direction:"outgoing"};default:return{title:e.transaction_name,subtitle:"Contractless transaction",amount:l(),direction:"outgoing"}}}async function cr(e,t){const n=c("activity-list");if(n.replaceChildren(),c("activity-count").textContent=`${e.length} shown`,e.length===0){const r=document.createElement("div");r.className="activity-empty";const i=document.createElement("strong");i.textContent="No wallet activity found";const a=document.createElement("span");a.textContent="New transactions will appear here after they reach the selected API.",r.append(i,a),n.append(r);return}const o=await Yt((g==null?void 0:g.selectedNetwork)??"testnet");e.forEach(r=>{let i;try{i=JSON.parse(o.decode_transaction(r.transaction_hex))}catch{const E=Number.parseInt(r.transaction_hex.slice(0,2),16);i={txtype:E,transaction_name:On[E]??`Transaction Type ${E}`,fields:{}}}const a=sr(i,(g==null?void 0:g.address)??""),l=ln(r.transaction_hex),d=ht(r.block_height,t){fn(r)}),n.append(u)})}async function mn(){if(!(g!=null&&g.unlocked))return;c("activity-count").textContent="Loading";const e=c("activity-list");e.replaceChildren();const t=document.createElement("div");t.className="activity-empty",t.textContent="Loading recent activity...",e.append(t);try{const n=await $("wallet-activity");await cr(Array.isArray(n.transactions)?n.transactions:[],Number(n.current_height??0))}catch(n){e.replaceChildren();const o=document.createElement("div");o.className="activity-empty activity-error",o.textContent=F(n),e.append(o),c("activity-count").textContent="Unavailable"}}function F(e){return e instanceof Error?e.message:String(e).replace(/^JsValue\((.*)\)$/,"$1")}async function U(e,t=!1){M.textContent="",t&&I("loading");try{await e()}catch(n){throw M.textContent=F(n),n}}function Q(e,t){if(!e.trim())throw new Error(t);return e}function lr(e){let t="";for(let o=0;oURL.revokeObjectURL(r),1e3)}async function gn(e,t){g=e,fe=t,ie=String(await $("export-private-key-image",{password:t,imagePassword:t})),c("wallet-image-preview").src=`data:image/png;base64,${ie}`;const n=c("backup-confirmed");n.disabled=!0,n.checked=!1,c("open-wallet").disabled=!0,I("backup-screen")}document.querySelectorAll(".navigation").forEach(e=>{e.addEventListener("click",()=>{const t=String(e.dataset.screen);if(t==="image-screen"&&en){chrome.tabs.create({url:chrome.runtime.getURL("import-wallet.html"),active:!0});return}I(t)})});c("account-menu-button").addEventListener("click",()=>z(!0));c("close-account-drawer").addEventListener("click",()=>z(!1));c("drawer-backdrop").addEventListener("click",()=>z(!1));function hn(e){z(!1),c("cancel-wallet-add").hidden=!1,I(e)}c("menu-create-wallet").addEventListener("click",()=>{hn("create-screen")});c("menu-import-wallet").addEventListener("click",()=>{z(!1),en?chrome.tabs.create({url:chrome.runtime.getURL("import-wallet.html"),active:!0}):hn("image-screen")});c("cancel-wallet-add").addEventListener("click",()=>{c("cancel-wallet-add").hidden=!0,g&&V(g)});c("menu-settings").addEventListener("click",()=>{z(!1),gt(),g&&(c("settings-testnet-api-url").value=g.testnetApiUrl,c("settings-mainnet-api-url").value=g.mainnetApiUrl,c("settings-lock-timeout").value=String(g.autoLockMinutes)),I("settings-screen")});c("close-settings").addEventListener("click",()=>{gt(),g&&V(g)});c("drawer-lock-wallet").addEventListener("click",()=>{U(async()=>{z(!1),fe="",ie="",V(await $("lock"))})});c("open-receive").addEventListener("click",()=>{U(er)});c("open-send").addEventListener("click",()=>{U(async()=>nr())});c("close-send").addEventListener("click",()=>{G=void 0,P?(xe(P),I("nft-detail-screen")):g&&V(g)});c("send-asset").addEventListener("change",Re);c("send-amount").addEventListener("input",Re);c("send-form").addEventListener("submit",e=>{e.preventDefault(),U(or)});c("send-form").addEventListener("keydown",e=>{!(e instanceof KeyboardEvent)||e.key!=="Enter"||e.target instanceof HTMLInputElement&&(e.preventDefault(),c("send-form").requestSubmit())});c("edit-send").addEventListener("click",()=>{I("send-screen")});c("sign-broadcast-transfer").addEventListener("click",()=>{U(async()=>{if(!G)throw new Error("The reviewed transfer is unavailable.");ve=(await $("wallet-broadcast-transfer",{transaction:G.transaction})).txid,c("send-result-txid").textContent=ve,G=void 0,I("send-success-screen")})});c("copy-send-txid").addEventListener("click",()=>{ve&&navigator.clipboard.writeText(ve).then(()=>{M.textContent="Transaction ID copied."}).catch(e=>{M.textContent=F(e)})});c("finish-send").addEventListener("click",()=>{ve="";const e=!!P;P=void 0,g&&(Be(),e?(Z=void 0,I("nfts-screen"),pt(!0)):V(g))});c("open-activity").addEventListener("click",()=>{I("activity-screen"),mn()});c("open-nfts").addEventListener("click",()=>{K=1,Z=void 0,I("nfts-screen"),pt(!0)});c("close-nfts").addEventListener("click",()=>{ae+=1,g&&V(g)});c("refresh-nfts").addEventListener("click",()=>{pt(!0)});c("close-nft-detail").addEventListener("click",()=>{Z=void 0,I("nfts-screen")});c("transfer-nft").addEventListener("click",()=>{U(async()=>{if(!Z)throw new Error("No NFT or RWA is selected.");rr(Z)})});c("nfts-previous").addEventListener("click",()=>{if(K<=1)return;K-=1;const e=++ae;Ce(),mt(e)});c("nfts-next").addEventListener("click",()=>{const e=Math.max(1,Math.ceil(H.length/we));if(K>=e)return;K+=1;const t=++ae;Ce(),mt(t)});c("close-activity").addEventListener("click",()=>{g&&V(g)});c("close-transaction").addEventListener("click",()=>{tn==="nft"&&Z?(xe(Z),I("nft-detail-screen")):I("activity-screen")});c("copy-transaction-txid").addEventListener("click",()=>{const e=_e==null?void 0:_e.txid;e&&navigator.clipboard.writeText(e).then(()=>{M.textContent="Transaction ID copied."}).catch(t=>{M.textContent=F(t)})});c("refresh-activity").addEventListener("click",()=>{mn()});c("refresh-balances").addEventListener("click",()=>{Be()});document.querySelectorAll("[data-token-filter]").forEach(e=>{e.addEventListener("click",()=>{Te=e.dataset.tokenFilter==="all"?"all":"held",document.querySelectorAll("[data-token-filter]").forEach(t=>{t.classList.toggle("active",t===e)}),st()})});c("close-receive").addEventListener("click",()=>{g&&V(g)});c("copy-receive-address").addEventListener("click",()=>{var t;const e=(t=c("receive-address-value").textContent)==null?void 0:t.trim();if(!e){M.textContent="No active wallet address is available.";return}navigator.clipboard.writeText(e).then(()=>{M.textContent="Wallet address copied."}).catch(n=>{M.textContent=F(n)})});c("save-wallet-settings").addEventListener("click",()=>{U(async()=>{const e=c("settings-testnet-api-url").value.replace(/\/+$/,""),t=c("settings-mainnet-api-url").value.replace(/\/+$/,"");if(!e)throw new Error("A testnet API URL is required.");for(const n of[e,t].filter(Boolean)){const o=new URL(n);if(o.protocol!=="https:"&&o.hostname!=="localhost"&&o.hostname!=="127.0.0.1")throw new Error("Contractless API URLs must use HTTPS.");const r=`${o.origin}/*`;if(!await chrome.permissions.contains({origins:[r]})&&!await chrome.permissions.request({origins:[r]}))throw new Error("Permission to contact this API endpoint was denied.")}g=await $("save-settings",{testnetApiUrl:e,mainnetApiUrl:t,autoLockMinutes:c("settings-lock-timeout").value}),M.textContent="Settings saved."})});document.querySelectorAll("[data-select-network]").forEach(e=>{e.addEventListener("click",()=>{const t=e.dataset.selectNetwork;!t||t===(g==null?void 0:g.selectedNetwork)||U(async()=>{z(!1),V(await $("switch-network",{network:t}))})})});document.querySelectorAll("[data-backup-action]").forEach(e=>{e.addEventListener("click",()=>{dt=e.dataset.backupAction,Ee(!0)})});c("close-backup-key").addEventListener("click",()=>Ee(!1));c("backup-key-backdrop").addEventListener("click",()=>Ee(!1));c("confirm-backup-key").addEventListener("click",()=>{const e=dt,t=c("backup-key-input").value;if(e){if(!t){M.textContent="Enter the active wallet encryption key.";return}U(async()=>{var r;if(e==="image"){const i=String(await $("export-private-key-image",{password:t,imagePassword:t})),a=((r=g==null?void 0:g.address)==null?void 0:r.split(".")[0])??"contractless";pn(i,`${a}.wallet.png`),Ee(!1),M.textContent="Wallet image downloaded.";return}const n=await $("reveal-private-key",{password:t});if(Ee(!1),e==="private"){c("revealed-private-key").value=n.privateKey,c("private-key-result").hidden=!1,c("private-key-result").scrollIntoView({block:"nearest"});return}const o=crypto.randomUUID();await chrome.storage.session.set({[`print-backup:${o}`]:{label:n.label,address:n.address,privateKey:n.privateKey,encryptionKey:t}}),await chrome.windows.create({url:chrome.runtime.getURL(`print.html?id=${encodeURIComponent(o)}`),type:"popup",width:820,height:760})})}});c("clear-private-key").addEventListener("click",gt);c("copy-private-key").addEventListener("click",()=>{const e=c("revealed-private-key").value;e&&navigator.clipboard.writeText(e).then(()=>{M.textContent="Private key copied."}).catch(t=>{M.textContent=F(t)})});document.querySelectorAll(".reveal").forEach(e=>{e.addEventListener("click",()=>{const t=c(String(e.dataset.input)),n=t.type==="text";t.type=n?"password":"text",e.textContent=n?"Show":"Hide"})});c("continue-create").addEventListener("click",()=>{try{Le=Q(c("new-password").value,"Enter an encryption key."),c("verify-password").value="",I("verify-screen")}catch(e){M.textContent=F(e)}});c("create-wallet").addEventListener("click",()=>{if(c("verify-password").value!==Le){M.textContent="The encryption keys do not match.";return}U(async()=>{const t=Le,n=await $("create-wallet",{password:t});Le="",c("new-password").value="",c("verify-password").value="",await gn(n,t)},!0).catch(()=>I("verify-screen",!1))});const ct=c("wallet-image");ct.addEventListener("change",()=>{var t;const e=(t=ct.files)==null?void 0:t[0];ge="",c("image-file-name").textContent=(e==null?void 0:e.name)??"Choose wallet image",e&&e.arrayBuffer().then(n=>{ge=lr(new Uint8Array(n))}).catch(n=>{M.textContent=F(n)})});c("import-image-form").addEventListener("submit",e=>{e.preventDefault();const t=c("image-password").value;try{Q(t,"Enter the wallet image encryption key."),Q(ge,"Choose a wallet image.")}catch(n){M.textContent=F(n);return}U(async()=>{const n=await $("import-wallet-image",{imageBase64:ge,imagePassword:t,password:t});fe=t,ge="",ct.value="",c("image-password").value="",V(n)},!0).catch(()=>I("image-screen",!1))});c("import-private").addEventListener("click",()=>{const e=c("private-key").value.trim(),t=c("private-password").value;try{Q(e,"Paste the private key."),Q(t,"Enter a new encryption key.")}catch(n){M.textContent=F(n);return}U(async()=>{const n=await $("import-private-key",{privateKey:e,password:t});c("private-key").value="",c("private-password").value="",await gn(n,t)},!0).catch(()=>I("private-screen",!1))});c("save-image").addEventListener("click",()=>{U(async()=>{var n;if(!ie)throw new Error("The wallet image is not ready.");const e=((n=g==null?void 0:g.address)==null?void 0:n.split(".")[0])??"contractless";pn(ie,`${e}.wallet.png`);const t=c("backup-confirmed");t.disabled=!1,t.checked=!1,c("open-wallet").disabled=!0})});c("backup-confirmed").addEventListener("change",e=>{c("open-wallet").disabled=!e.currentTarget.checked});c("open-wallet").addEventListener("click",()=>{fe="",ie="",c("wallet-image-preview").removeAttribute("src"),g&&V(g)});c("unlock-form").addEventListener("submit",e=>{e.preventDefault();const t=c("unlock-password").value;try{Q(t,"Enter your encryption key.")}catch(n){M.textContent=F(n);return}U(async()=>{const n=await $("unlock",{password:t});fe=t,c("unlock-password").value="",V(n)},!0).catch(()=>I("locked-screen",!1))});c("approve-provider-approval").addEventListener("click",()=>{oe&&(ye("approve"),U(async()=>{const e=await $("resolve-provider-approval",{id:oe,approved:!0});oe="",await nn(e.remaining??0,!0)}).catch(()=>ye(null)))});c("reject-provider-approval").addEventListener("click",()=>{oe&&(ye("reject"),U(async()=>{const e=await $("resolve-provider-approval",{id:oe,approved:!1});oe="",await nn(e.remaining??0,!1)}).catch(()=>ye(null)))});$("status").then(e=>V(e)).catch(e=>{I("create-screen"),M.textContent=F(e)}); diff --git a/extension/dist/chrome/print.html b/extension/dist/chrome/print.html new file mode 100644 index 0000000..7eeccfa --- /dev/null +++ b/extension/dist/chrome/print.html @@ -0,0 +1,44 @@ + + + + + + Contractless Wallet Backup + + + + + +
+ +
+

Contractless Wallet Backup

+

Private recovery information

+
+
+ +

+ Keep this page private. Anyone with the private key and encryption key can + control this wallet. +

+ +
+

Wallet

+

+    
+
+

Address

+

+    
+
+

Private Key

+

+    
+
+

Encryption Key

+

+    
+ + + + diff --git a/extension/dist/chrome/print.js b/extension/dist/chrome/print.js new file mode 100644 index 0000000..c86b6e5 --- /dev/null +++ b/extension/dist/chrome/print.js @@ -0,0 +1 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";async function a(){const e=new URLSearchParams(location.search).get("id");if(!e)throw new Error("Missing printable wallet backup.");const n=`print-backup:${e}`,o=await chrome.storage.session.get(n);await chrome.storage.session.remove(n);const t=o[n];if(!t)throw new Error("The printable wallet backup has expired.");document.getElementById("wallet-label").textContent=t.label,document.getElementById("wallet-address").textContent=t.address,document.getElementById("wallet-private-key").textContent=t.privateKey,document.getElementById("wallet-encryption-key").textContent=t.encryptionKey,setTimeout(()=>{window.focus(),window.print()},150)}a().catch(e=>{document.getElementById("print-error").textContent=e instanceof Error?e.message:String(e)}); diff --git a/extension/dist/chrome/provider.js b/extension/dist/chrome/provider.js new file mode 100644 index 0000000..47444f0 --- /dev/null +++ b/extension/dist/chrome/provider.js @@ -0,0 +1 @@ +const a=new Map;window.addEventListener("message",e=>{var s,t,o;if(e.source!==window||((s=e.data)==null?void 0:s.source)!=="contractless-wallet")return;const r=a.get(e.data.id);r&&(a.delete(e.data.id),(t=e.data.response)!=null&&t.ok?r.resolve(e.data.response.result):r.reject(new Error(((o=e.data.response)==null?void 0:o.error)??"Wallet request failed.")))});const n=Object.freeze({isContractless:!0,request({method:e,params:r}){const s=crypto.randomUUID();return new Promise((t,o)=>{a.set(s,{resolve:t,reject:o}),window.postMessage({source:"contractless-page",id:s,method:e,params:r},window.location.origin)})}});Object.defineProperty(window,"contractless",{value:n,writable:!1,configurable:!1}); diff --git a/extension/dist/chunks/core-CvwXoH-U.js b/extension/dist/chunks/core-CvwXoH-U.js new file mode 100644 index 0000000..559b415 --- /dev/null +++ b/extension/dist/chunks/core-CvwXoH-U.js @@ -0,0 +1 @@ +function I(t,e,r,_,o){let l,c;try{const w=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),d=a,y=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),m=a,b=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),p=a,q=s(_,n.__wbindgen_malloc,n.__wbindgen_realloc),v=a,F=s(o,n.__wbindgen_malloc,n.__wbindgen_realloc),U=a,k=n.complete_dual_transaction_request(w,d,y,m,b,p,q,v,F,U);var i=k[0],f=k[1];if(k[3])throw i=0,f=0,u(k[2]);return l=i,c=f,g(i,f)}finally{n.__wbindgen_free(l,c,1)}}function M(t,e){let r,_;try{const c=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),i=a,f=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),w=a,d=n.create_handshake_proof(c,i,f,w);var o=d[0],l=d[1];if(d[3])throw o=0,l=0,u(d[2]);return r=o,_=l,g(o,l)}finally{n.__wbindgen_free(r,_,1)}}function B(t,e){let r,_;try{const c=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),i=a,f=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),w=a,d=n.create_wallet_registration(c,i,f,w);var o=d[0],l=d[1];if(d[3])throw o=0,l=0,u(d[2]);return r=o,_=l,g(o,l)}finally{n.__wbindgen_free(r,_,1)}}function C(t,e,r){let _,o;try{const i=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),f=a,w=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),d=a,y=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),m=a,b=n.create_wallet_registration_from_keys(i,f,w,d,y,m);var l=b[0],c=b[1];if(b[3])throw l=0,c=0,u(b[2]);return _=l,o=c,g(l,c)}finally{n.__wbindgen_free(_,o,1)}}function D(t){let e,r;try{const l=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),c=a,i=n.decode_transaction(l,c);var _=i[0],o=i[1];if(i[3])throw _=0,o=0,u(i[2]);return e=_,r=o,g(_,o)}finally{n.__wbindgen_free(e,r,1)}}function j(t,e,r){let _,o;try{const i=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),f=a,w=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),d=a,y=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),m=a,b=n.export_legacy_wallet(i,f,w,d,y,m);var l=b[0],c=b[1];if(b[3])throw l=0,c=0,u(b[2]);return _=l,o=c,g(l,c)}finally{n.__wbindgen_free(_,o,1)}}function z(t){let e,r;try{const l=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),c=a,i=n.generate_browser_wallet(l,c);var _=i[0],o=i[1];if(i[3])throw _=0,o=0,u(i[2]);return e=_,r=o,g(_,o)}finally{n.__wbindgen_free(e,r,1)}}function N(t,e,r){let _,o;try{const i=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),f=a,w=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),d=a,y=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),m=a,b=n.import_legacy_wallet(i,f,w,d,y,m);var l=b[0],c=b[1];if(b[3])throw l=0,c=0,u(b[2]);return _=l,o=c,g(l,c)}finally{n.__wbindgen_free(_,o,1)}}function V(t,e){let r,_;try{const c=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),i=a,f=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),w=a,d=n.import_private_key(c,i,f,w);var o=d[0],l=d[1];if(d[3])throw o=0,l=0,u(d[2]);return r=o,_=l,g(o,l)}finally{n.__wbindgen_free(r,_,1)}}function G(t,e,r){let _,o;try{const i=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),f=a,w=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),d=a,y=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),m=a,b=n.import_private_key_image(i,f,w,d,y,m);var l=b[0],c=b[1];if(b[3])throw l=0,c=0,u(b[2]);return _=l,o=c,g(l,c)}finally{n.__wbindgen_free(_,o,1)}}function H(t){let e,r;try{const l=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),c=a,i=n.inspect_transaction_request(l,c);var _=i[0],o=i[1];if(i[3])throw _=0,o=0,u(i[2]);return e=_,r=o,g(_,o)}finally{n.__wbindgen_free(e,r,1)}}function P(t,e,r){let _,o;try{const i=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),f=a,w=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),d=a,y=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),m=a,b=n.sign_message_request(i,f,w,d,y,m);var l=b[0],c=b[1];if(b[3])throw l=0,c=0,u(b[2]);return _=l,o=c,g(l,c)}finally{n.__wbindgen_free(_,o,1)}}function X(t,e,r,_){let o,l;try{const f=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),w=a,d=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),y=a,m=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),b=a,p=n.sign_transaction_request(f,w,d,y,m,b,_);var c=p[0],i=p[1];if(p[3])throw c=0,i=0,u(p[2]);return o=c,l=i,g(c,i)}finally{n.__wbindgen_free(o,l,1)}}function Y(){let t,e;try{const o=n.transaction_type_registry();var r=o[0],_=o[1];if(o[3])throw r=0,_=0,u(o[2]);return t=r,e=_,g(r,_)}finally{n.__wbindgen_free(t,e,1)}}function J(t,e){let r,_;try{const c=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),i=a,f=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),w=a,d=n.unlock_browser_wallet(c,i,f,w);var o=d[0],l=d[1];if(d[3])throw o=0,l=0,u(d[2]);return r=o,_=l,g(o,l)}finally{n.__wbindgen_free(r,_,1)}}function K(t,e,r,_){let o,l;try{const f=s(t,n.__wbindgen_malloc,n.__wbindgen_realloc),w=a,d=s(e,n.__wbindgen_malloc,n.__wbindgen_realloc),y=a,m=s(r,n.__wbindgen_malloc,n.__wbindgen_realloc),b=a,p=s(_,n.__wbindgen_malloc,n.__wbindgen_realloc),q=a,v=n.verify_message_proof(f,w,d,y,m,b,p,q);var c=v[0],i=v[1];if(v[3])throw c=0,i=0,u(v[2]);return o=c,l=i,g(c,i)}finally{n.__wbindgen_free(o,l,1)}}function Q(){return{__proto__:null,"./contractless_browser_core_bg.js":{__proto__:null,__wbg___wbindgen_is_function_1ff95bcc5517c252:function(e){return typeof e=="function"},__wbg___wbindgen_is_object_a27215656b807791:function(e){const r=e;return typeof r=="object"&&r!==null},__wbg___wbindgen_is_string_ea5e6cc2e4141dfe:function(e){return typeof e=="string"},__wbg___wbindgen_is_undefined_c05833b95a3cf397:function(e){return e===void 0},__wbg___wbindgen_throw_344f42d3211c4765:function(e,r){throw new Error(g(e,r))},__wbg_call_a6e5c5dce5018821:function(){return R(function(e,r,_){return e.call(r,_)},arguments)},__wbg_crypto_38df2bab126b63dc:function(e){return e.crypto},__wbg_getRandomValues_c44a50d8cfdaebeb:function(){return R(function(e,r){e.getRandomValues(r)},arguments)},__wbg_length_1f0964f4a5e2c6d8:function(e){return e.length},__wbg_msCrypto_bd5a034af96bcba6:function(e){return e.msCrypto},__wbg_new_with_length_e6785c33c8e4cce8:function(e){return new Uint8Array(e>>>0)},__wbg_node_84ea875411254db1:function(e){return e.node},__wbg_process_44c7a14e11e9f69e:function(e){return e.process},__wbg_prototypesetcall_4770620bbe4688a0:function(e,r,_){Uint8Array.prototype.set.call(S(e,r),_)},__wbg_randomFillSync_6c25eac9869eb53c:function(){return R(function(e,r){e.randomFillSync(r)},arguments)},__wbg_require_b4edbdcf3e2a1ef0:function(){return R(function(){return module.require},arguments)},__wbg_static_accessor_GLOBAL_4ef717fb391d88b7:function(){const e=typeof global>"u"?null:global;return W(e)?0:h(e)},__wbg_static_accessor_GLOBAL_THIS_8d1badc68b5a74f4:function(){const e=typeof globalThis>"u"?null:globalThis;return W(e)?0:h(e)},__wbg_static_accessor_SELF_146583524fe1469b:function(){const e=typeof self>"u"?null:self;return W(e)?0:h(e)},__wbg_static_accessor_WINDOW_f2829a2234d7819e:function(){const e=typeof window>"u"?null:window;return W(e)?0:h(e)},__wbg_subarray_3ed232c8a6baee09:function(e,r,_){return e.subarray(r>>>0,_>>>0)},__wbg_versions_276b2795b1c6a219:function(e){return e.versions},__wbindgen_cast_0000000000000001:function(e,r){return S(e,r)},__wbindgen_cast_0000000000000002:function(e,r){return g(e,r)},__wbindgen_init_externref_table:function(){const e=n.__wbindgen_externrefs,r=e.grow(4);e.set(0,void 0),e.set(r+0,void 0),e.set(r+1,null),e.set(r+2,!0),e.set(r+3,!1)}}}}function h(t){const e=n.__externref_table_alloc();return n.__wbindgen_externrefs.set(e,t),e}function S(t,e){return t=t>>>0,A().subarray(t/1,t/1+e)}function g(t,e){return $(t>>>0,e)}let x=null;function A(){return(x===null||x.byteLength===0)&&(x=new Uint8Array(n.memory.buffer)),x}function R(t,e){try{return t.apply(this,e)}catch(r){const _=h(r);n.__wbindgen_exn_store(_)}}function W(t){return t==null}function s(t,e,r){if(r===void 0){const i=T.encode(t),f=e(i.length,1)>>>0;return A().subarray(f,f+i.length).set(i),a=i.length,f}let _=t.length,o=e(_,1)>>>0;const l=A();let c=0;for(;c<_;c++){const i=t.charCodeAt(c);if(i>127)break;l[o+c]=i}if(c!==_){c!==0&&(t=t.slice(c)),o=r(o,_,_=c+t.length*3,1)>>>0;const i=A().subarray(o+c,o+_),f=T.encodeInto(t,i);c+=f.written,o=r(o,_,c,1)>>>0}return a=c,o}function u(t){const e=n.__wbindgen_externrefs.get(t);return n.__externref_table_dealloc(t),e}let E=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});E.decode();const Z=2146435072;let O=0;function $(t,e){return O+=e,O>=Z&&(E=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),E.decode(),O=e),E.decode(A().subarray(t,t+e))}const T=new TextEncoder;"encodeInto"in T||(T.encodeInto=function(t,e){const r=T.encode(t);return e.set(r),{read:t.length,written:r.length}});let a=0,n;function ee(t,e){return n=t.exports,x=null,n.__wbindgen_start(),n}async function ne(t,e){if(typeof Response=="function"&&t instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(t,e)}catch(o){if(t.ok&&r(t.type)&&t.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",o);else throw o}const _=await t.arrayBuffer();return await WebAssembly.instantiate(_,e)}else{const _=await WebAssembly.instantiate(t,e);return _ instanceof WebAssembly.Instance?{instance:_,module:t}:_}function r(_){switch(_){case"basic":case"cors":case"default":return!0}return!1}}async function te(t){if(n!==void 0)return n;t!==void 0&&(Object.getPrototypeOf(t)===Object.prototype?{module_or_path:t}=t:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),t===void 0&&(t=new URL("/assets/contractless_browser_core_bg-C_vtp2Os.wasm",import.meta.url));const e=Q();(typeof t=="string"||typeof Request=="function"&&t instanceof Request||typeof URL=="function"&&t instanceof URL)&&(t=fetch(t));const{instance:r,module:_}=await ne(await t,e);return ee(r)}let L;async function re(){return L??(L=te()),await L,{complete_dual_transaction_request:I,create_handshake_proof:M,create_wallet_registration:B,create_wallet_registration_from_keys:C,decode_transaction:D,export_legacy_wallet:j,generate_browser_wallet:z,import_legacy_wallet:N,import_private_key:V,import_private_key_image:G,inspect_transaction_request:H,sign_message_request:P,sign_transaction_request:X,transaction_type_registry:Y,unlock_browser_wallet:J,verify_message_proof:K}}export{re as c}; diff --git a/extension/dist/chunks/modulepreload-polyfill-B5Qt9EMX.js b/extension/dist/chunks/modulepreload-polyfill-B5Qt9EMX.js new file mode 100644 index 0000000..3e0a78a --- /dev/null +++ b/extension/dist/chunks/modulepreload-polyfill-B5Qt9EMX.js @@ -0,0 +1 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})(); diff --git a/extension/dist/content.js b/extension/dist/content.js new file mode 100644 index 0000000..0ce4e56 --- /dev/null +++ b/extension/dist/content.js @@ -0,0 +1 @@ +const o=document.createElement("script");o.src=chrome.runtime.getURL("provider.js");o.onload=()=>o.remove();(document.head||document.documentElement).appendChild(o);window.addEventListener("message",e=>{var t;e.source!==window||((t=e.data)==null?void 0:t.source)!=="contractless-page"||chrome.runtime.sendMessage({type:"provider-request",method:e.data.method,params:e.data.params},a=>{window.postMessage({source:"contractless-wallet",id:e.data.id,response:a},window.location.origin)})}); diff --git a/extension/dist/contractless-logo.png b/extension/dist/contractless-logo.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/dist/contractless-logo.png differ diff --git a/extension/dist/firefox/assets/contractless-logo-BgfMS-Kf.png b/extension/dist/firefox/assets/contractless-logo-BgfMS-Kf.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/dist/firefox/assets/contractless-logo-BgfMS-Kf.png differ diff --git a/extension/dist/firefox/assets/contractless_browser_core_bg-CM1Z7v2F.wasm b/extension/dist/firefox/assets/contractless_browser_core_bg-CM1Z7v2F.wasm new file mode 100644 index 0000000..99ed81f Binary files /dev/null and b/extension/dist/firefox/assets/contractless_browser_core_bg-CM1Z7v2F.wasm differ diff --git a/extension/dist/firefox/assets/contractless_browser_core_bg-Dl61GrlP.wasm b/extension/dist/firefox/assets/contractless_browser_core_bg-Dl61GrlP.wasm new file mode 100644 index 0000000..c1c41b8 Binary files /dev/null and b/extension/dist/firefox/assets/contractless_browser_core_bg-Dl61GrlP.wasm differ diff --git a/extension/dist/firefox/assets/import-wallet-D9ROrgVZ.css b/extension/dist/firefox/assets/import-wallet-D9ROrgVZ.css new file mode 100644 index 0000000..e51e6f5 --- /dev/null +++ b/extension/dist/firefox/assets/import-wallet-D9ROrgVZ.css @@ -0,0 +1 @@ +:root{font:16px/1.5 Arial,Helvetica,sans-serif;color:#fff;background:#08131c}*{box-sizing:border-box}body{min-width:320px;min-height:100vh;margin:0;background:#08131c}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:wait;opacity:.72}.site-header{display:flex;align-items:center;justify-content:space-between;min-height:76px;padding:0 32px;border-bottom:1px solid #2e4757;background:#0d1d28}.brand{display:flex;align-items:center;gap:12px}.brand img{width:39px;filter:brightness(0) invert(1)}.brand div{display:grid}.brand strong{font-size:18px}.brand span,.local-page{color:#b9cbd6;font-size:13px}main{display:grid;place-items:start center;padding:54px 20px}.import-panel,.success-panel{width:min(100%,500px);padding:34px;border:1px solid #385666;background:#10222e}.eyebrow{color:#58d5e7;font-size:12px;font-weight:700;text-transform:uppercase}h1{margin:7px 0 10px;font-size:31px;line-height:1.15;letter-spacing:0}.intro,.success-panel p{margin:0 0 26px;color:#c8d6de}form{display:grid}label{margin:21px 0 8px;font-size:13px;font-weight:700}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.file-picker{display:flex;align-items:center;gap:14px;min-height:78px;margin-top:8px;padding:13px;border:1px solid #5ac9de;background:#132d3b;cursor:pointer}.file-picker>span:last-child{display:grid;gap:3px}.file-picker small{color:#afc6d1;font-weight:400}.file-icon{display:grid;place-items:center;width:44px;height:44px;border:1px solid #62d7e9;color:#62d7e9;font-size:25px}.input-row{display:grid;grid-template-columns:1fr auto}.input-row input{min-width:0;height:50px;padding:0 14px;border:1px solid #5a7180;border-right:0;background:#08151e;color:#fff}.input-row button{width:68px;border:1px solid #5a7180;background:#173345;color:#fff}.security-note,.address-result{display:grid;gap:4px;margin-top:20px;padding:13px 14px;border-left:3px solid #57d0df;background:#0a1922}.security-note span,.address-result span{color:#b7cad4;font-size:13px}.address-result strong{overflow-wrap:anywhere}.primary{min-height:50px;margin-top:24px;border:1px solid #67d5e4;background:#236eaa;color:#fff;font-weight:700}#notice{margin:14px 0 0;padding:10px 12px;border-left:3px solid #ff7168;background:#321c20;color:#ffc0bb;font-size:13px}#notice:empty{display:none}.success-mark{display:grid;place-items:center;width:58px;height:58px;margin-bottom:25px;border:2px solid #62d7e9;border-radius:50%;color:#62d7e9;font-size:31px;font-weight:700}.completion-message{margin-top:24px!important}@media(max-width:560px){.site-header{padding:0 18px}.local-page{display:none}main{padding:24px 12px}.import-panel,.success-panel{padding:24px 20px}h1{font-size:27px}} diff --git a/extension/dist/firefox/assets/popup-B3vY8vDf.css b/extension/dist/firefox/assets/popup-B3vY8vDf.css new file mode 100644 index 0000000..aaa5acc --- /dev/null +++ b/extension/dist/firefox/assets/popup-B3vY8vDf.css @@ -0,0 +1 @@ +:root{--blue: #2167a5;--blue-deep: #174d7c;--ink: #13202a;--muted-white: rgb(255 255 255 / 90%);--danger: #b9382e;--danger-soft: #fff0ee;--success: #1d8054;--dark: #0c151d;--surface: #111e28;--surface-raised: #172936;--surface-active: #1b3547;--dark-line: #314957;--dark-muted: #c4d0d7;--accent: #55c5dc;font:14px/1.45 Arial,Helvetica,sans-serif}*{box-sizing:border-box}body{width:390px;min-width:390px;min-height:600px;margin:0;background:var(--blue);color:#fff}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}.wallet-shell{position:relative;width:390px;height:600px;overflow:hidden;background:var(--blue)}.screen{width:100%;height:100%;background:var(--blue)}.screen[hidden]{display:none}.wallet-header{height:72px;display:flex;align-items:center;gap:11px;padding:0 20px;border-bottom:1px solid rgb(255 255 255 / 70%)}.wallet-header img{width:37px;filter:brightness(0) invert(1)}.wallet-header div{display:grid}.wallet-header strong{font-size:18px}.wallet-header small{color:var(--muted-white);font-size:14px}.wallet-header>span{margin-left:auto;font-size:13px;font-weight:700}.screen-content{padding:28px 24px 22px}.screen-content.compact{padding-top:18px}.eyebrow{font-size:11px;font-weight:700;text-transform:uppercase}h1{margin:7px 0 9px;font-size:27px;line-height:1.12;letter-spacing:0}.intro{margin:0 0 24px;color:var(--muted-white);line-height:1.5}label{display:block;margin-bottom:8px;font-size:12px;font-weight:700}.input-row{height:49px;display:grid;grid-template-columns:1fr auto;border:1px solid white;background:#fff}.input-row:focus-within,textarea:focus{outline:2px solid rgb(255 255 255 / 28%);outline-offset:2px}.input-row input{min-width:0;border:0;padding:0 13px;color:var(--ink);outline:0}.reveal{border:0;border-left:1px solid #d9e0e5;padding:0 13px;background:#fff;color:var(--blue);font-size:11px;font-weight:700}textarea{width:100%;min-height:82px;margin-bottom:13px;resize:none;border:1px solid white;padding:11px 13px;color:var(--ink);outline:0}.note{display:grid;gap:2px;margin:18px 0 23px;border-left:3px solid var(--accent);padding:13px;background:var(--surface-raised);color:#fff;font-size:12px}.note span{color:var(--dark-muted)}.compact-note{margin:12px 0 14px;padding:10px 13px}.warning{margin:18px 0 25px;border-left:3px solid var(--danger);padding:13px;background:#321c20;color:#ffbbb5;font-size:12px;line-height:1.4}.warning strong{display:block}.primary,.secondary{width:100%;min-height:48px;display:flex;align-items:center;justify-content:center;gap:10px;border:1px solid var(--accent);background:var(--accent);color:#08202b;font-weight:700}.primary span{font-size:18px}.secondary{min-height:42px;margin-top:9px;background:transparent;color:#dcecf3;font-size:12px}.secondary:disabled{border-color:#ffffff59;color:#ffffff7f}.text-button{width:100%;margin-top:8px;border:0;padding:8px;background:transparent;color:var(--muted-white)}.divider{display:flex;align-items:center;gap:10px;margin:17px 0 12px;color:#ffffffad;font-size:12px;text-transform:uppercase}.divider:before,.divider:after{height:1px;flex:1;background:#ffffff4d;content:""}.file-heading{margin-top:20px}.file-picker{min-height:86px;display:flex;align-items:center;gap:13px;margin-bottom:24px;border:1px dashed rgb(255 255 255 / 72%);padding:14px;background:#ffffff1a;cursor:pointer}.file-picker>span:last-child{display:grid;gap:4px}.file-picker small{color:#ffffffad;font-weight:400}.file-icon{width:34px;height:34px;display:grid;flex:0 0 34px;place-items:center;border:1px solid white;font-size:22px;font-weight:400}.visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}.loading-screen{position:relative}.loading-brand{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-content:center;justify-items:center;gap:34px}.loading-brand img,.large-logo{width:148px;filter:brightness(0) invert(1)}.spinner{width:28px;aspect-ratio:1;border:3px solid rgb(255 255 255 / 28%);border-top-color:#fff;border-radius:50%;animation:spin .85s linear infinite}.loading-screen>p{position:absolute;bottom:35px;width:100%;margin:0;color:#ffffffb8;font-size:12px;text-align:center;text-transform:uppercase}@keyframes spin{to{transform:rotate(360deg)}}.success-mark{width:38px;height:38px;display:grid;place-items:center;border-radius:50%;border:1px solid #55c98a;background:#123727;color:#66dfa0;font-size:22px;font-weight:700}.image-preview{position:relative;height:142px;margin:18px 0;overflow:hidden;border:1px solid var(--dark-line);background:var(--surface)}.image-preview img{width:100%;height:100%;display:block;object-fit:contain}.confirmation{display:grid;grid-template-columns:18px 1fr;gap:9px;align-items:start;margin:14px 0;color:#ffffffd6;font-size:11px;line-height:1.35}.confirmation input{width:16px;height:16px;margin:0}.centered-content{height:100%;display:flex;flex-direction:column;justify-content:center;padding:36px 28px;text-align:center}.centered-content{align-items:stretch}.centered-content .large-logo{align-self:center;width:92px;margin-bottom:25px}.centered-content h1{text-align:center}.centered-content .primary{margin-top:22px}.dashboard-header{height:66px}.account-button{min-width:105px;max-width:132px;height:34px;display:flex;align-items:center;gap:7px;margin-left:auto;overflow:hidden;border:1px solid rgb(255 255 255 / 45%);padding:0 10px;background:transparent;color:#fff;font-size:14px}.account-button>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.network-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:#63d18d}.assets-dashboard{height:calc(100% - 66px);overflow:hidden}.portfolio-balance{padding:22px 22px 19px;border-bottom:1px solid rgb(255 255 255 / 24%);background:var(--blue-deep)}.portfolio-balance>span{color:var(--muted-white);font-size:14px;text-transform:uppercase}.portfolio-balance>strong{display:block;margin-top:4px;font-size:31px;line-height:1.2}.portfolio-balance>strong small{font-size:13px}.portfolio-balance p{max-width:100%;margin:7px 0 0;overflow:hidden;color:#ffffffd1;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.wallet-actions{height:88px;display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid rgb(255 255 255 / 24%)}.wallet-actions button{display:grid;place-items:center;align-content:center;gap:5px;border:0;border-right:1px solid rgb(255 255 255 / 18%);background:transparent;color:#fff;font-size:14px;font-weight:700}.wallet-actions button:last-child{border-right:0}.action-icon{width:29px;height:29px;display:grid;place-items:center;border:1px solid rgb(255 255 255 / 65%);font-size:16px;font-weight:400}.asset-ledger{height:calc(100% - 209px);background:var(--dark);color:#fff}.asset-filter{height:42px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--dark-line);padding:5px 19px;background:#0f1a22}.asset-filter button{border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dark-muted);font-size:14px;font-weight:700}.asset-filter button.active{border-bottom-color:var(--accent);color:#fff}.asset-ledger-heading{height:45px;display:flex;align-items:center;justify-content:space-between;padding:0 19px;border-bottom:1px solid var(--dark-line)}.asset-ledger-heading h1{margin:0;font-size:19px}.asset-ledger-heading button{width:30px;height:30px;border:0;background:transparent;color:var(--accent);font-size:17px}.asset-row{min-height:66px;display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:11px;align-items:center;padding:10px 19px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.asset-symbol{width:36px;height:36px;display:grid;place-items:center;background:var(--blue);color:#fff;font-size:12px;font-weight:700}.contractless-symbol img{width:25px;filter:brightness(0) invert(1)}.asset-name,.asset-value{min-width:0;display:grid;gap:2px}.asset-name strong,.asset-value strong{overflow:hidden;font-size:16px;text-overflow:ellipsis;white-space:nowrap}.asset-name span,.asset-value span{color:var(--dark-muted);font-size:13px}.asset-value{max-width:125px;text-align:right}.token-list{max-height:calc(100% - 148px);overflow-y:auto}.empty-assets{display:grid;gap:5px;padding:27px 20px;color:var(--dark-muted);text-align:center}.empty-assets strong{color:#dce8ee;font-size:16px}.empty-assets span{font-size:14px}.receive-screen{background:var(--dark)}.receive-screen .wallet-header{background:var(--blue)}.send-screen,.send-review-screen,.send-success-screen{background:var(--dark);color:#fff}.send-screen .wallet-header,.send-review-screen .wallet-header,.send-success-screen .wallet-header{background:var(--blue)}.send-content,.send-review-content,.send-success-content{height:calc(100% - 72px);padding:22px;overflow-y:auto}.send-content{display:grid;align-content:start;gap:17px}.send-content label{color:#fff;font-size:14px}.send-content input,.send-content select{width:100%;min-height:48px;border:1px solid var(--dark-line);padding:0 13px;background:var(--surface);color:#fff;outline:0}.send-content input:focus,.send-content select:focus{border-color:var(--accent)}.send-amount-row{display:grid;grid-template-columns:minmax(0,1fr) auto}.send-amount-row input{border-right:0}.send-amount-row>span{min-width:64px;display:grid;place-items:center;border:1px solid var(--dark-line);background:var(--surface-raised);color:var(--accent);font-weight:700}.field-note{display:block;margin-top:7px;color:var(--dark-muted);font-size:13px}.send-continue{margin-top:5px}.send-review-content{padding:0 22px 24px}.send-review-amount{margin:0 -22px 20px;padding:24px 22px;background:var(--blue-deep)}.send-review-amount>span{color:var(--muted-white);font-size:13px;text-transform:uppercase}.send-review-amount strong{display:block;margin-top:5px;font-size:29px}.send-review-amount small{color:var(--accent);font-size:15px}.send-review-details{margin:0 0 18px}.send-review-details>div{padding:12px 0;border-bottom:1px solid var(--dark-line)}.send-review-details dt{margin-bottom:4px;color:var(--dark-muted);font-size:13px}.send-review-details dd{margin:0;overflow-wrap:anywhere;color:#fff;font-weight:700}.signing-notice{margin:18px 0;padding-left:12px;border-left:3px solid var(--accent);color:var(--dark-muted)}.send-success-content{text-align:center}.send-success-content p{color:var(--dark-muted)}.send-txid{margin:24px 0 14px;padding:14px;border:1px solid var(--dark-line);background:var(--surface);text-align:left}.send-txid span{display:block;margin-bottom:5px;color:var(--dark-muted);font-size:13px}.send-txid strong{overflow-wrap:anywhere}.send-success-content .secondary{margin-bottom:10px}.receive-content{height:calc(100% - 72px);overflow-y:auto;padding:25px 24px 22px;text-align:center}.receive-label{color:var(--accent);font-size:12px;font-weight:700;text-transform:uppercase}.receive-content h1{margin-top:8px}.receive-content>p{margin:0 auto 20px;color:var(--dark-muted);font-size:11px;line-height:1.45}.qr-frame{width:238px;height:238px;display:grid;place-items:center;margin:0 auto 19px;border:1px solid #708693;padding:8px;background:#fff}.qr-frame canvas{width:220px;height:220px;display:block}.receive-address{display:grid;gap:6px;border:1px solid var(--dark-line);padding:13px;background:var(--surface);text-align:left}.receive-address span{color:var(--dark-muted);font-size:11px;font-weight:700;text-transform:uppercase}.receive-address strong{overflow-wrap:anywhere;color:#e5f0f5;font:12px/1.4 Consolas,monospace}.receive-copy{margin-top:13px}.nfts-screen,.nft-detail-screen{background:var(--dark);color:#fff}.nfts-screen .wallet-header,.nft-detail-screen .wallet-header{background:var(--blue)}.header-refresh{width:34px;height:34px;display:grid;place-items:center;margin-left:auto;border:1px solid rgb(255 255 255 / 70%);background:transparent;color:#fff;font-size:18px}.nfts-content,.nft-detail-content{height:calc(100% - 72px);overflow-y:auto}.nfts-summary{min-height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:#0f1a22;color:var(--dark-muted);font-size:13px;font-weight:700;text-transform:uppercase}.nfts-summary span:first-child{color:var(--accent)}.nfts-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;padding:1px;background:var(--dark-line)}.nft-card{min-width:0;display:grid;align-content:start;border:0;padding:0;background:var(--surface);color:#fff;text-align:left}.nft-card:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.nft-card-media{width:100%;aspect-ratio:1;display:grid;place-items:center;overflow:hidden;background:#17303e;color:var(--accent);font-size:28px;font-weight:700}.nft-card-media img{width:100%;height:100%;display:block;object-fit:cover}.nft-card-identity{min-height:116px;display:grid;align-content:start;gap:3px;padding:11px 12px 13px}.nft-card-identity>span{color:var(--accent);font-size:11px;font-weight:700}.nft-card-identity strong{overflow:hidden;font-size:15px;text-overflow:ellipsis;white-space:nowrap}.nft-card-identity small{overflow:hidden;color:var(--dark-muted);font-size:12px;text-overflow:ellipsis;white-space:nowrap}.nft-card-identity b{margin-top:5px;color:#fff;font-size:12px}.nft-card-identity em{color:#e9c667;font-size:11px;font-style:normal}.nfts-empty{grid-column:1 / -1;min-height:300px;display:grid;place-content:center;gap:6px;padding:30px;background:var(--surface);color:var(--dark-muted);text-align:center}.nfts-empty strong{color:#fff;font-size:17px}.nfts-pagination{min-height:52px;display:grid;grid-template-columns:42px 1fr 42px;align-items:center;gap:10px;padding:7px 14px;border-top:1px solid var(--dark-line);background:#0f1a22;text-align:center}.nfts-pagination[hidden]{display:none}.nfts-pagination button{width:38px;height:38px;border:1px solid var(--dark-line);background:var(--surface);color:var(--accent);font-size:17px}.nfts-pagination button:disabled{color:#61727c}.nfts-pagination span{color:var(--dark-muted);font-size:13px}.nft-detail-media{width:100%;aspect-ratio:16 / 11;display:grid;place-items:center;overflow:hidden;border-bottom:1px solid var(--dark-line);background:#142934}.nft-detail-media img{width:100%;height:100%;display:block;object-fit:contain}.nft-detail-heading{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:15px;align-items:end;padding:18px 20px 14px}.nft-detail-heading>div{min-width:0}.nft-detail-heading span{color:var(--accent);font-size:11px;font-weight:700}.nft-detail-heading h1{overflow-wrap:anywhere;margin:4px 0 0;font-size:23px}.nft-detail-heading>strong{color:#63d18d;font-size:13px;text-align:right}.nft-detail-description{margin:0;padding:0 20px 17px;color:var(--dark-muted);font-size:14px;line-height:1.5}.nft-detail-attributes{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;padding:0 20px 18px}.nft-detail-attributes[hidden]{display:none}.nft-detail-attributes>div{min-width:0;display:grid;gap:2px;border:1px solid var(--dark-line);padding:9px;background:var(--surface-raised)}.nft-detail-attributes span{overflow:hidden;color:var(--dark-muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.nft-detail-attributes strong{overflow-wrap:anywhere;font-size:13px}.nft-detail-fields{margin:0;border-top:1px solid var(--dark-line)}.nft-detail-fields>div{display:grid;gap:4px;padding:12px 20px;border-bottom:1px solid var(--dark-line)}.nft-detail-fields dt{color:var(--dark-muted);font-size:12px}.nft-detail-fields dd{margin:0;overflow-wrap:anywhere;color:#fff;font:12px/1.45 Consolas,monospace}.nft-detail-actions{padding:18px 20px 0}.nft-provenance{padding:18px 20px 25px}.nft-provenance h2{margin:0 0 11px;font-size:16px}.nft-provenance p{color:var(--dark-muted)}.nft-history-row{width:100%;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;border:0;padding:10px 0;border-bottom:1px solid var(--dark-line);background:transparent;color:#fff;text-align:left}.nft-history-row:hover,.nft-history-row:focus-visible{background:var(--surface-raised)}.nft-history-row:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.nft-history-row strong{font-size:13px}.nft-history-row span{color:var(--dark-muted);font-size:12px;text-align:right}.nft-history-row small{grid-column:1 / -1;color:var(--accent);font:11px Consolas,monospace}.activity-screen{background:var(--dark);color:#fff}.activity-screen .wallet-header{background:var(--blue)}.activity-content{height:calc(100% - 72px);overflow-y:auto}.activity-summary{min-height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:#0f1a22;color:var(--dark-muted);font-size:13px;text-transform:uppercase}.activity-summary strong{color:var(--accent);font-size:13px}.activity-summary>div{display:flex;align-items:center;gap:8px}.activity-summary button{width:30px;height:30px;border:1px solid var(--dark-line);background:transparent;color:var(--accent);font-size:17px}.activity-list{background:var(--surface)}.activity-row{width:100%;min-height:98px;display:grid;grid-template-columns:31px minmax(0,1fr) auto;gap:10px;align-items:center;padding:11px 17px;border-bottom:1px solid var(--dark-line);border-top:0;border-right:0;border-left:0;background:transparent;color:#fff;text-align:left}.activity-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.activity-marker{width:29px;height:29px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:13px;font-weight:700}.activity-marker.incoming,.activity-status .incoming{color:#63d18d}.activity-marker.outgoing,.activity-status .outgoing{color:#fff}.activity-details,.activity-status{min-width:0;display:grid;gap:2px}.activity-details strong{overflow:hidden;font-size:16px;text-overflow:ellipsis;white-space:nowrap}.activity-details span{color:#c1d0d8;font:13px Consolas,monospace}.activity-details small,.activity-status span{color:var(--dark-muted);font-size:13px}.activity-status{max-width:132px;text-align:right}.activity-status strong{font-size:14px;white-space:nowrap}.activity-status .confirmed{color:#63d18d}.activity-status .pending{color:#e9c667}.activity-empty{display:grid;gap:6px;padding:42px 24px;color:var(--dark-muted);font-size:14px;text-align:center}.activity-empty strong{color:#dce8ee;font-size:16px}.activity-error{color:#ffaaa4}.transaction-screen{background:var(--dark);color:#fff}.transaction-screen .wallet-header{background:var(--blue)}.transaction-confirmed{color:#84e7aa}.transaction-pending{color:#f1d27b}.transaction-content{height:calc(100% - 72px);overflow-y:auto;padding-bottom:24px}.transaction-identity{display:grid;gap:13px;padding:18px;border-bottom:1px solid var(--dark-line);background:var(--surface-raised)}.transaction-identity>div{display:grid;gap:7px}.transaction-identity span,.transaction-field span{color:var(--dark-muted);font-size:12px;font-weight:700;text-transform:uppercase}.transaction-identity strong{overflow-wrap:anywhere;color:#fff;font:13px/1.55 Consolas,monospace}.transaction-identity button{min-height:40px;border:1px solid var(--accent);background:transparent;color:var(--accent);font-size:14px;font-weight:700}.transaction-section{border-top:1px solid var(--dark-line)}.transaction-section h2{margin:0;padding:16px 18px 10px;font-size:16px}.transaction-fields{display:grid}.transaction-field{display:grid;gap:6px;padding:13px 18px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.transaction-field strong{overflow-wrap:anywhere;color:#fff;font-size:15px;font-weight:600;line-height:1.45}.transaction-technical{border-top:1px solid var(--dark-line)}.transaction-technical summary{padding:17px 18px;color:var(--accent);font-size:15px;font-weight:700;cursor:pointer}.technical-field strong{font:12px/1.55 Consolas,monospace;word-break:break-all}.drawer-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#091a278a}.account-drawer{position:absolute;top:0;right:0;bottom:0;z-index:11;width:338px;display:flex;flex-direction:column;border-left:1px solid var(--dark-line);background:var(--dark);color:#fff;transform:translate(100%);transition:transform .18s ease-out}.account-drawer.open{transform:translate(0)}.drawer-header{height:72px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--dark-line);background:var(--blue);color:#fff}.drawer-header>div{display:grid;gap:2px}.drawer-header strong{font-size:19px}.drawer-header span{color:var(--muted-white);font-size:14px}.drawer-header button{width:34px;height:34px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:23px;line-height:1}.drawer-section{max-height:244px;overflow-y:auto;padding:17px 15px 14px;border-bottom:1px solid var(--dark-line)}.drawer-label{display:block;margin:0 3px 8px;color:var(--dark-muted);font-size:13px;font-weight:700;text-transform:uppercase}.known-address{width:100%;min-height:58px;display:grid;grid-template-columns:minmax(0,1fr) 30px;gap:4px;align-items:center;border:1px solid var(--dark-line);padding:4px;background:var(--surface);color:#fff}.known-address+.known-address{margin-top:7px}.known-address.active-address{border-color:var(--accent);background:var(--surface-active)}.address-select{min-width:0;min-height:48px;display:grid;grid-template-columns:36px minmax(0,1fr) 20px;gap:10px;align-items:center;border:0;padding:5px 6px;background:transparent;color:#fff;text-align:left}.rename-address{width:28px;height:34px;border:0;background:transparent;color:var(--accent);font-size:16px}.address-avatar{width:34px;height:34px;display:grid;place-items:center;background:#204f70}.address-avatar img{width:24px;filter:brightness(0) invert(1)}.address-details{min-width:0;display:grid;gap:2px}.address-details strong{font-size:15px}.address-details small{overflow:hidden;color:var(--dark-muted);font-size:13px;text-overflow:ellipsis;white-space:nowrap}.selected-mark{color:var(--success);font-size:14px;font-weight:700}.drawer-navigation{display:grid;padding:8px 0}.drawer-navigation button{min-height:72px;display:grid;grid-template-columns:34px minmax(0,1fr) 16px;gap:10px;align-items:center;border:0;border-bottom:1px solid var(--dark-line);padding:9px 18px;background:transparent;color:#fff;text-align:left}.drawer-navigation button>span:nth-child(2){display:grid;gap:2px}.drawer-navigation strong{font-size:16px}.drawer-navigation small{color:var(--dark-muted);font-size:13px}.menu-icon{width:32px;height:32px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:15px;font-weight:700}.menu-arrow{color:var(--dark-muted);font-size:20px;text-align:right}.drawer-lock{min-height:46px;display:flex;align-items:center;justify-content:center;gap:8px;margin:auto 15px 15px;border:1px solid var(--dark-line);background:var(--surface-raised);color:#dce8ee;font-size:14px;font-weight:700}.header-back{width:34px;height:34px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:18px}.settings-screen{background:var(--dark);color:#fff}.settings-screen .wallet-header{background:var(--blue);color:#fff}.settings-content{height:calc(100% - 72px);overflow-y:auto;padding-bottom:20px}.settings-group{padding:18px 20px;border-bottom:1px solid var(--dark-line);background:var(--surface)}.settings-heading{display:grid;gap:3px;margin-bottom:15px}.settings-heading strong{font-size:13px}.settings-heading span{color:var(--dark-muted);font-size:12px;line-height:1.4}.settings-group label{color:#b7c6ce;font-size:12px;text-transform:uppercase}.settings-group input,.settings-group select{width:100%;height:44px;border:1px solid #8699a5;padding:0 11px;background:#fff;color:var(--ink);outline:0}.settings-group input:focus,.settings-group select:focus{border-color:var(--blue);outline:2px solid rgb(33 103 165 / 14%)}.settings-save{width:calc(100% - 40px);margin:16px 20px;border-color:var(--accent);background:var(--accent);color:#08202b}.backup-settings{padding-right:0;padding-left:0}.backup-settings .settings-heading{padding:0 20px}.settings-action{width:100%;min-height:61px;display:grid;grid-template-columns:34px minmax(0,1fr) 16px;gap:10px;align-items:center;border:0;border-top:1px solid var(--dark-line);padding:9px 20px;background:transparent;color:#fff;text-align:left}.settings-action:last-child{border-bottom:1px solid var(--dark-line)}.settings-action>span:nth-child(2){display:grid;gap:2px}.settings-action strong{font-size:14px}.settings-action small{color:var(--dark-muted);font-size:11px}.settings-action>span:last-child{color:var(--dark-muted);font-size:20px}.settings-action-icon{width:32px;height:32px;display:grid;place-items:center;border:1px solid #607987;color:var(--accent);font-size:15px;font-weight:700}.private-key-result{margin:16px 20px 0;border:1px solid var(--dark-line);padding:13px;background:var(--surface-raised)}.private-key-result>div{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px}.private-key-result>div>strong{font-size:11px;text-transform:uppercase}.private-key-result>div>button{width:28px;height:28px;border:0;background:transparent;color:var(--dark-muted);font-size:19px}.private-key-result textarea{height:95px;min-height:95px;margin:0 0 10px;border:1px solid #8699a5;padding:9px;background:#fff;color:var(--ink);font-size:11px}.private-key-result .secondary{border-color:var(--accent);color:var(--accent)}.modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:30;background:#091a27ad}.backup-key-modal{position:absolute;top:50%;right:20px;left:20px;z-index:31;border:1px solid white;background:var(--blue);color:#fff;transform:translateY(-50%)}.backup-key-modal>header{min-height:65px;display:flex;align-items:center;justify-content:space-between;padding:12px 15px;border-bottom:1px solid rgb(255 255 255 / 40%)}.backup-key-modal>header>div{display:grid;gap:2px}.backup-key-modal>header strong{font-size:13px}.backup-key-modal>header span{color:var(--muted-white);font-size:11px}.backup-key-modal>header button{width:30px;height:30px;border:1px solid rgb(255 255 255 / 45%);background:transparent;color:#fff;font-size:20px}.backup-key-modal>div{padding:17px 15px}.backup-key-modal .primary{margin-top:16px}#notice{position:absolute;right:18px;bottom:14px;left:18px;z-index:40;margin:0;padding:10px 12px;border:1px solid #8d4448;border-left:3px solid #ff7168;background:#321c20;color:#ffc0bb;font-size:11px}#notice:empty{display:none}.provider-approval-screen{display:flex;flex-direction:column}.provider-approval-screen .screen-content{min-height:0;flex:1;overflow-y:auto;padding-bottom:18px}.approval-context{display:flex;align-items:center;justify-content:space-between;gap:12px}.approval-context .eyebrow{min-width:0;overflow-wrap:anywhere}.risk-label{flex:0 0 auto;padding:5px 8px;border:1px solid currentColor;font-size:11px;font-weight:700;text-transform:uppercase}.risk-safe{background:#123b2d;color:#63e5a9}.risk-low{background:#403612;color:#ffe16c}.risk-high{background:#481f24;color:#ff8b8b}.approval-details{display:grid;grid-template-columns:minmax(100px,.8fr) minmax(0,1.4fr);margin:20px 0;border:1px solid #315466;background:#091923}.approval-details dt,.approval-details dd{min-width:0;margin:0;padding:11px 12px;border-bottom:1px solid #203d4c;overflow-wrap:anywhere}.approval-details dt{color:#85dce8;font-size:12px;font-weight:700;text-transform:capitalize}.approval-details dd{color:#f5f9fb;font:13px/1.45 Arial,Helvetica,sans-serif;white-space:pre-wrap}.approval-details dt:last-of-type,.approval-details dd:last-of-type{border-bottom:0}.approval-actions{display:grid;flex:0 0 auto;grid-template-columns:1fr 1fr;gap:10px;padding:14px 24px 18px;border-top:1px solid rgb(255 255 255 / 35%);background:var(--blue-deep)}.approval-actions button{min-height:48px;margin:0}.approval-actions button.is-processing:before{width:15px;height:15px;content:"";border:2px solid currentcolor;border-right-color:transparent;border-radius:50%;animation:approval-spin .7s linear infinite}.approval-actions button:disabled{opacity:.72}@keyframes approval-spin{to{transform:rotate(360deg)}}.network-choice{display:grid;grid-template-columns:1fr 1fr;width:100%;margin:0 0 18px;border:1px solid #5b8caf;background:#0a1b27}.network-choice button{min-height:42px;border:0;border-right:1px solid #315466;background:transparent;color:#b9cfdb;font:700 13px/1 Arial,Helvetica,sans-serif}.network-choice button:last-child{border-right:0}.network-choice button.active{background:#43d6e8;color:#06151e}.drawer-network{margin:8px 0 0}.settings-help{display:block;margin-top:8px;color:#a9c2cf;font-size:12px;line-height:1.45} diff --git a/extension/dist/firefox/assets/print-DTb91-bp.css b/extension/dist/firefox/assets/print-DTb91-bp.css new file mode 100644 index 0000000..b6c42aa --- /dev/null +++ b/extension/dist/firefox/assets/print-DTb91-bp.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{max-width:760px;margin:0 auto;padding:34px;color:#111820;font:14px/1.45 Arial,Helvetica,sans-serif}header{display:flex;align-items:center;gap:15px;padding-bottom:18px;border-bottom:3px solid #2167a5}header img{width:58px}h1{margin:0;font-size:24px}header p{margin:2px 0 0;color:#5c6972}.warning{margin:22px 0;border:2px solid #9b1c1c;padding:12px;color:#7a1111;font-weight:700}section{margin-top:18px}h2{margin:0 0 7px;color:#42515b;font-size:12px;text-transform:uppercase}pre{margin:0;overflow-wrap:anywhere;white-space:pre-wrap;border:1px solid #aebac2;padding:11px;font:10px/1.35 Consolas,monospace}.print-error{color:#9b1c1c;font-weight:700}@media print{body{max-width:none;padding:0}} diff --git a/extension/dist/firefox/background.js b/extension/dist/firefox/background.js new file mode 100644 index 0000000..79b546a --- /dev/null +++ b/extension/dist/firefox/background.js @@ -0,0 +1,3 @@ +import{c as St}from"./chunks/core-D2IK1U91.js";const z="contractlessWallet",F="contractlessWallets",B="contractlessSettings",V="contractlessAutoLock",x="contractlessUnlockedWallet",M="contractlessApplicationSessions",pt=12e4,At=10,ot={testnetApiUrl:"https://api.contractless.dev",autoLockMinutes:60};let s;const q="pending-provider-approvals";async function S(){const t=(await chrome.storage.local.get(B))[B]??{},r=t.selectedNetwork==="mainnet"?"mainnet":"testnet",a=t.testnetApiUrl??t.apiUrl??ot.testnetApiUrl,l=t.mainnetApiUrl??"";return{selectedNetwork:r,testnetApiUrl:a,mainnetApiUrl:l,apiUrl:r==="mainnet"?l:a,autoLockMinutes:t.autoLockMinutes??ot.autoLockMinutes}}function y(e){if(!e.apiUrl)throw new Error(`No ${e.selectedNetwork} API is configured.`);return e.apiUrl}function ut(e){if(e.endsWith(".cltc"))return"testnet";if(e.endsWith(".clc"))return"mainnet";throw new Error("The wallet address does not identify a supported Contractless network.")}function ht(e){const t=JSON.parse(e);if(typeof t.short_address!="string"||typeof t.public_key!="string")throw new Error("The encrypted wallet metadata is invalid.");return{address:t.short_address,publicKey:t.public_key,network:ut(t.short_address)}}async function C(){const e=await chrome.storage.local.get([F,z]),t=e[F];if((t==null?void 0:t.version)===2&&Array.isArray(t.wallets))return t;if((t==null?void 0:t.version)===1&&Array.isArray(t.wallets)){const f=t,v=f.wallets.map(b=>({...b,network:ut(b.address)})),m=v.find(b=>b.id===f.activeWalletId),A={version:2,activeWalletIds:m?{[m.network]:m.id}:{},wallets:v};return await L(A),A}const r=e[z];if(typeof r!="string")return{version:2,activeWalletIds:{},wallets:[]};const a=ht(r),l={id:crypto.randomUUID(),label:"Contractless Account 1",address:a.address,publicKey:a.publicKey,walletJson:r,network:a.network},d={version:2,activeWalletIds:{[a.network]:l.id},wallets:[l]};return await chrome.storage.local.set({[F]:d}),await chrome.storage.local.remove(z),d}async function L(e){await chrome.storage.local.set({[F]:e})}function O(e,t){const r=e.wallets.filter(a=>a.network===t);return r.find(a=>a.id===e.activeWalletIds[t])??r[0]}async function R(e){const t=await C(),r=ht(e),a=t.wallets.find(d=>d.address===r.address);if(a)return a.walletJson=e,a.publicKey=r.publicKey,t.activeWalletIds[r.network]=a.id,await L(t),a;const l={id:crypto.randomUUID(),label:`Contractless Account ${t.wallets.filter(d=>d.network===r.network).length+1}`,address:r.address,publicKey:r.publicKey,walletJson:e,network:r.network};return t.wallets.push(l),t.activeWalletIds[r.network]=l.id,await L(t),l}async function E(){await gt();const e=await C(),t=await S(),r=O(e,t.selectedNetwork),a=e.wallets.filter(l=>l.network===t.selectedNetwork);return{exists:a.length>0,unlocked:!!(s&&s.walletId===(r==null?void 0:r.id)&&s.network===t.selectedNetwork),activeWalletId:r==null?void 0:r.id,activeWalletLabel:r==null?void 0:r.label,address:s==null?void 0:s.short_address,publicKey:s==null?void 0:s.public_key,wallets:a.map(l=>({id:l.id,label:l.label,address:l.address,publicKey:l.publicKey,network:l.network,active:l.id===(r==null?void 0:r.id)})),selectedNetwork:t.selectedNetwork,networkName:t.selectedNetwork==="testnet"?"Testnet":"Mainnet",networkSymbol:t.selectedNetwork==="testnet"?"CLTC":"CLC",testnetApiUrl:t.testnetApiUrl,mainnetApiUrl:t.mainnetApiUrl,apiUrl:t.apiUrl,autoLockMinutes:t.autoLockMinutes}}async function T(){if(!s)return;const e=await S();s.expiresAt=Date.now()+e.autoLockMinutes*6e4,await chrome.storage.session.set({[x]:s}),await chrome.alarms.clear(V),await chrome.alarms.create(V,{when:s.expiresAt})}function Tt(e,t){const r=new Uint8Array(e);let a="";const l=32768;for(let d=0;d{});if(!d.ok||(f==null?void 0:f.success)!==!0)throw new Error((f==null?void 0:f.error)??`Wallet registration failed with HTTP ${d.status}.`);return f.data}async function Et(e,t){var f;const r=await S();if(!r.apiUrl)return;const a=e.create_handshake_proof(t.public_key,t.private_key),l=await fetch(`${y(r)}/api/v1/addresses/registration?address=${encodeURIComponent(t.short_address)}`,{headers:{"X-Contractless-Address":t.short_address,"X-Contractless-Public-Key":t.public_key,"X-Contractless-Signature":a}}),d=await l.json().catch(()=>{});if(!l.ok||(d==null?void 0:d.success)!==!0)throw new Error((d==null?void 0:d.error)??`Wallet registration lookup failed with HTTP ${l.status}.`);((f=d.data)==null?void 0:f.registered)!==!0&&await G(e,t)}async function gt(){const e=await S();if(s){if(s.expiresAt>Date.now()&&s.network===e.selectedNetwork)return;await D();return}const r=(await chrome.storage.session.get(x))[x];if(!r||r.expiresAt<=Date.now()){await chrome.storage.session.remove(x);return}if(r.network!==e.selectedNetwork){await chrome.storage.session.remove(x),await Q();return}r.sessionId||(r.sessionId=crypto.randomUUID(),await Q(),await chrome.storage.session.set({[x]:r})),s=r}async function D(){s&&(s.private_key="",s=void 0),await chrome.storage.session.remove([x,M]),await chrome.alarms.clear(V)}async function et(){const t=(await chrome.storage.session.get(M))[M];return Array.isArray(t)?t:[]}async function ft(e){await chrome.storage.session.set({[M]:e})}async function Q(){await chrome.storage.session.remove(M)}function Nt(e){const t=crypto.getRandomValues(new Uint8Array(e));return Array.from(t,r=>r.toString(16).padStart(2,"0")).join("")}async function It(e){const t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return Array.from(new Uint8Array(t),r=>r.toString(16).padStart(2,"0")).join("")}function J(e){return!e.params||typeof e.params!="object"?{}:e.params}function P(e){const t=String(e??"").trim();if(!/^[A-Za-z0-9_-]{5,32}$/.test(t))throw new Error("The application ID must contain 5 to 32 URL-safe characters.");return t}function rt(e,t){if(String(e.origin??"")!==t)throw new Error("The application origin does not match the requesting website.")}function _t(e){const t=Number(e),r=Math.floor(Date.now()/1e3);if(!Number.isInteger(t)||Math.abs(r-t)>300)throw new Error("The application connection timestamp must be within five minutes.");return t}function mt(e){const t=String(e??"").trim().toLowerCase();if(!/^[0-9a-f]{32,128}$/.test(t)||t.length%2!==0)throw new Error("The application nonce must contain 16 to 64 random hexadecimal bytes.");return t}function kt(e){const t=String(e??"").trim();if(!t||new TextEncoder().encode(t).length>75)throw new Error("The application connection message must contain 1 to 75 bytes.");return t}function yt(e){const t=String(e??"").trim();if(!t||new TextEncoder().encode(t).length>75)throw new Error("A signed website message must contain 1 to 75 bytes.");return t}function it(e,t){if(e==="contractless_connect"||e==="contractless_signMessage")return"safe";const r=t.transaction;if(!r||typeof r!="object")return"high";const a=Number(r.txtype);return Number.isInteger(a)&&a>=100&&a<=113?"low":"high"}function vt(e){if(!e||typeof e!="object")throw new Error("A complete transaction object is required.");const t=e,r=Number(t.txtype);if(!Number.isInteger(r))throw new Error("The transaction type is invalid.");if(r===6||r===7)throw new Error("Swaps and loan creation require a staged signing workflow.");return t}function Z(e){if(!e||typeof e!="object")throw new Error("A complete or partially signed transaction object is required.");const t=e,r=t.unsigned_swap??t.unsigned_loan_contract??t;if(!r||typeof r!="object")throw new Error("The unsigned transaction fields are missing.");const a=Number(r.txtype);if(a!==6&&a!==7)throw new Error("Staged signing is only available for swaps and loan creation.");return t}async function ct(e,t){var b;if(!s)throw new Error("Wallet is locked.");const r=t.unsigned_swap??t.unsigned_loan_contract??t;if(!r||typeof r!="object")return;const a=r,l=Number(a.txtype),d=l===2?[["Receiving",String(a.receiver??"")]]:l===6?[["First swap participant",String(a.sender1??"")],["Second swap participant",String(a.sender2??"")]]:l===7?[["Lender",String(a.lender??"")],["Borrower",String(a.borrower??"")]]:[];if(d.length===0)return;e.inspect_transaction_request(JSON.stringify(t));const f=await S(),v=e.create_handshake_proof(s.public_key,s.private_key),m={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":v},A=new Map;for(const[W,K]of d){const N=K.trim().toLowerCase();let I=A.get(N);if(I===void 0){const $=await fetch(`${y(f)}/api/v1/addresses/registration?address=${encodeURIComponent(N)}`,{headers:m}),n=await $.json().catch(()=>{});if(!$.ok||(n==null?void 0:n.success)!==!0)throw new Error((n==null?void 0:n.error)??`Address registration lookup failed with HTTP ${$.status}.`);I=((b=n.data)==null?void 0:b.registered)===!0,A.set(N,I)}if(!I)throw new Error(`${W} wallet is not registered.`)}}async function bt(e,t){if(t.complete!==!0||typeof t.bytes_hex!="string"||typeof t.hash!="string")throw new Error("The transaction could not be completed.");if(!s)throw new Error("Wallet is locked.");const r=await S(),a=e.create_handshake_proof(s.public_key,s.private_key),l=await fetch(`${y(r)}/api/v1/transactions/broadcast`,{method:"POST",headers:{"Content-Type":"application/json","X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":a},body:JSON.stringify({transaction_hex:t.bytes_hex})}),d=await l.json().catch(()=>{});if(!l.ok||(d==null?void 0:d.success)!==!0)throw new Error((d==null?void 0:d.error)??`Transaction broadcast failed with HTTP ${l.status}.`);return await T(),{txid:t.hash,bytes:t.bytes_hex,broadcast:d.data}}async function U(e,t){if(!s)throw new Error("Wallet is locked.");rt(e,t);const r=P(e.appId),a=String(e.accessKey??""),l=(await et()).find(d=>d.origin===t&&d.appId===r&&d.accessKey===a&&d.walletId===(s==null?void 0:s.walletId)&&d.unlockSessionId===(s==null?void 0:s.sessionId));if(!l)throw new Error("The application session is invalid or has expired. Reconnect the wallet.");return l}async function Y(){const t=(await chrome.storage.session.get(q))[q],r=Array.isArray(t)?t.filter(m=>typeof m=="string"&&m.length>0):typeof t=="string"&&t.length>0?[t]:[];if(r.length===0)return[];const a=r.map(m=>`approval:${m}`),l=await chrome.storage.session.get(a),d=Date.now(),f=[],v=[];for(const m of r){const A=`approval:${m}`,b=l[A];!b||d-b.createdAt>=pt?v.push(A):f.push(m)}return(v.length>0||f.length!==r.length)&&(await chrome.storage.session.set({[q]:f}),v.length>0&&await chrome.storage.session.remove(v)),f}async function tt(e){const t=(await Y()).filter(r=>r!==e);return await chrome.storage.session.set({[q]:t}),t}async function lt(e){const t=await Y(),r=await chrome.storage.session.get(t.map(l=>`approval:${l}`));if(e.method==="contractless_connect"&&t.some(l=>{const d=r[`approval:${l}`];if(!d||d.method!=="contractless_connect")return!1;const f=J(d),v=J(e);return d.origin===e.origin&&f.appId===v.appId}))throw new Error("This application already has a wallet connection request pending.");if(t.length>=At)throw new Error("The wallet approval queue is full. Resolve a pending request first.");await chrome.storage.session.set({[q]:[...t,e.id],[`approval:${e.id}`]:e});try{await chrome.action.openPopup()}catch{}}async function dt(e){const t=`approval-result:${e}`,r=Date.now()+pt;for(;Date.now()setTimeout(l,150))}throw await chrome.storage.session.remove([`approval:${e}`,t]),await tt(e),new Error("Wallet approval request expired.")}function $t(e){const t=e.url??e.origin;if(!t)throw new Error("The requesting website origin is unavailable.");const r=new URL(t).origin;if(r==="null")throw new Error("Requests from opaque origins are not supported.");return r}async function wt(e,t){if(!s)throw new Error("Wallet is locked.");switch(await T(),t.method){case"contractless_connect":{const r=J(t);rt(r,t.origin);const a=String(r.walletSessionId??"");if(a&&a!==s.sessionId)throw new Error("The active wallet changed while the connection was being approved.");const l=P(r.appId),d=_t(r.timestamp),f=mt(r.nonce),v=kt(r.message),m=Nt(32),A=await It(m),b=Math.floor(Date.now()/1e3),W=Math.min(b+900,Math.floor(s.expiresAt/1e3)),K=["Contractless application connection",`Application ID: ${l}`,`Origin: ${t.origin}`,`Requested timestamp: ${d}`,`Message: ${v}`,`Session key hash: ${A}`].join(` +`),N=JSON.parse(e.sign_message_request(JSON.stringify({origin:t.origin,message:K,nonce:f,issued_at:b,expires_at:W}),s.private_key,s.public_key)),I={origin:t.origin,appId:l,accessKey:m,walletId:s.walletId,unlockSessionId:s.sessionId,createdAt:Date.now()},$=await et();return $.push(I),await ft($),{address:s.short_address,publicKey:s.public_key,signature:N.signature,proof:N,accessKey:m,appId:l,origin:t.origin,sessionExpiresAt:s.expiresAt}}case"contractless_signMessage":{const r=J(t);await U(r,t.origin);const a=Math.floor(Date.now()/1e3),l=Number(r.expires_at??a+300),d=P(r.appId),f={origin:t.origin,message:["Contractless application message",`Application ID: ${d}`,`Message: ${yt(r.message)}`].join(` +`),nonce:String(r.nonce??""),issued_at:a,expires_at:l};return JSON.parse(e.sign_message_request(JSON.stringify(f),s.private_key,s.public_key))}case"contractless_sendTransaction":{const r=J(t);await U(r,t.origin);const a=vt(r.transaction),l=JSON.parse(e.sign_transaction_request(JSON.stringify(a),s.private_key,s.public_key,1));return bt(e,l)}case"contractless_signDualTransaction":{const r=J(t);await U(r,t.origin);const a=Z(r.transaction),l=Number(r.signerSlot);if(l!==1&&l!==2)throw new Error("The signer slot must be 1 or 2.");const d=JSON.parse(e.sign_transaction_request(JSON.stringify(a),s.private_key,s.public_key,l));return{complete:!1,txid:d.hash,unsignedBytes:d.unsigned_bytes_hex,canonicalUnsignedJson:JSON.parse(e.inspect_transaction_request(JSON.stringify(a))).canonical_unsigned_json,signedTransaction:d.signed_transaction,signature:l===1?d.signature1:d.signature2,signature1:d.signature1,signature2:d.signature2,signerSlot:l,signerPublicKey:s.public_key}}default:throw new Error("The website requested an unsupported wallet method.")}}async function Ct(e,t){var l,d,f,v,m,A,b,W,K,N,I,$;const r=await S();if(e.type==="switch-network"){const n=e.network==="mainnet"?"mainnet":"testnet";await D();const i={...r,selectedNetwork:n,apiUrl:n==="mainnet"?r.mainnetApiUrl:r.testnetApiUrl};return await chrome.storage.local.set({[B]:i}),E()}const a=await St(r.selectedNetwork);switch(await gt(),e.type){case"status":return E();case"create-wallet":{const n=String(e.password),i=a.generate_browser_wallet(n),o=JSON.parse(a.unlock_browser_wallet(i,n)),u=await R(i);if(await j({walletId:u.id,network:r.selectedNetwork,...o}),r.apiUrl)try{await G(a,o)}catch(c){console.warn("Automatic wallet registration failed during creation.",c)}return E()}case"import-wallet":{const n=a.import_legacy_wallet(String(e.walletJson),String(e.legacyPassword),String(e.password)),i=await R(n),o=JSON.parse(a.unlock_browser_wallet(n,String(e.password)));return await j({walletId:i.id,network:r.selectedNetwork,...o}),E()}case"import-wallet-image":{const n=String(e.password),i=a.import_private_key_image(String(e.imageBase64),String(e.imagePassword),n),o=await R(i),u=JSON.parse(a.unlock_browser_wallet(i,n));return await j({walletId:o.id,network:r.selectedNetwork,...u}),E()}case"import-private-key":{const n=String(e.password),i=a.import_private_key(String(e.privateKey),n),o=await R(i),u=JSON.parse(a.unlock_browser_wallet(i,n));return await j({walletId:o.id,network:r.selectedNetwork,...u}),E()}case"unlock":{const n=await C(),i=O(n,r.selectedNetwork);if(!i)throw new Error("No wallet exists.");const o=JSON.parse(a.unlock_browser_wallet(i.walletJson,String(e.password)));await j({walletId:i.id,network:r.selectedNetwork,...o});try{await Et(a,o)}catch(u){console.warn("Automatic wallet registration failed during unlock.",u)}return E()}case"switch-wallet":{const n=await C(),i=String(e.walletId),o=n.wallets.find(u=>u.id===i);if(!o||o.network!==r.selectedNetwork)throw new Error("The selected wallet does not exist.");return await D(),n.activeWalletIds[r.selectedNetwork]=i,await L(n),E()}case"rename-wallet":{const n=await C(),i=n.wallets.find(u=>u.id===String(e.walletId));if(!i)throw new Error("The selected wallet does not exist.");const o=String(e.label).trim();if(o.length<1||o.length>40)throw new Error("Wallet labels must contain between 1 and 40 characters.");return i.label=o,await L(n),E()}case"lock":return await D(),E();case"save-settings":{const n=String(e.testnetApiUrl??r.testnetApiUrl).replace(/\/+$/,""),i=String(e.mainnetApiUrl??r.mainnetApiUrl).replace(/\/+$/,""),o={selectedNetwork:r.selectedNetwork,testnetApiUrl:n,mainnetApiUrl:i,apiUrl:r.selectedNetwork==="mainnet"?i:n,autoLockMinutes:Math.max(1,Number(e.autoLockMinutes))};return await chrome.storage.local.set({[B]:o}),await T(),E()}case"register-wallet":{if(!s)throw new Error("Wallet is locked.");const n=await G(a,s);return await T(),n}case"wallet-balances":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},u=encodeURIComponent(s.short_address),[c,h,p]=await Promise.all([fetch(`${y(n)}/api/v1/balances?address=${u}`,{headers:o}),fetch(`${y(n)}/api/v1/tokens`,{headers:o}),fetch(`${y(n)}/api/v1/nfts`,{headers:o})]),[w,_,g]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{}),p.json().catch(()=>{})]);if(!c.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Balance lookup failed with HTTP ${c.status}.`);return await T(),{address:s.short_address,balances:((l=w.data)==null?void 0:l.balances)??[],tokens:h.ok&&(_==null?void 0:_.success)===!0?((d=_.data)==null?void 0:d.tokens)??[]:[],nfts:p.ok&&(g==null?void 0:g.success)===!0?((f=g.data)==null?void 0:f.nfts)??[]:[],tokenCatalogError:h.ok&&(_==null?void 0:_.success)===!0?void 0:(_==null?void 0:_.error)??`Token lookup failed with HTTP ${h.status}.`,nftCatalogError:p.ok&&(g==null?void 0:g.success)===!0?void 0:(g==null?void 0:g.error)??`NFT lookup failed with HTTP ${p.status}.`}}case"wallet-nft-details":{if(!s)throw new Error("Wallet is locked.");const n=String(e.name??"").trim(),i=Number(e.series??0);if(!n||n.length>15||!Number.isInteger(i)||i<0||i>4294967295)throw new Error("A valid NFT or RWA identifier is required.");const o=await S(),u=a.create_handshake_proof(s.public_key,s.private_key),c={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":u},h=`name=${encodeURIComponent(n)}&series=${i}`,[p,w]=await Promise.all([fetch(`${y(o)}/api/v1/nfts/details?${h}`,{headers:c}),fetch(`${y(o)}/api/v1/nfts/media?${h}`,{headers:c})]),[_,g]=await Promise.all([p.json().catch(()=>{}),w.json().catch(()=>{})]);if(!p.ok||(_==null?void 0:_.success)!==!0)throw new Error((_==null?void 0:_.error)??`NFT details lookup failed with HTTP ${p.status}.`);let k="",st=!1,at={},X;if(w.ok&&(g==null?void 0:g.success)===!0){at=(v=g.data)!=null&&v.metadata&&typeof g.data.metadata=="object"?g.data.metadata:{},st=((m=g.data)==null?void 0:m.image_available)===!0;const nt=String(((A=g.data)==null?void 0:A.image_url)??"");if(nt)try{const H=await fetch(new URL(nt,`${y(o)}/`).toString(),{headers:c});H.ok?k=Tt(await H.arrayBuffer(),H.headers.get("Content-Type")??"image/png"):X=`NFT image returned HTTP ${H.status}.`}catch{X="NFT image could not be retrieved."}}else X=(g==null?void 0:g.error)??`NFT media lookup failed with HTTP ${w.status}.`;return await T(),{..._.data,metadata:at,image_data_url:k,image_available:st,media_error:X}}case"wallet-receive-address":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o=encodeURIComponent(s.short_address),u=await fetch(`${y(n)}/api/v1/addresses/vanity?address=${o}`,{headers:{"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i}}),c=await u.json().catch(()=>{});if(!u.ok||(c==null?void 0:c.success)!==!0)throw new Error((c==null?void 0:c.error)??`Vanity address lookup failed with HTTP ${u.status}.`);await T();const h=typeof((b=c.data)==null?void 0:b.vanity_address)=="string"&&c.data.vanity_address.trim()!==""?c.data.vanity_address:void 0;return{shortAddress:s.short_address,receiveAddress:h??s.short_address,vanity:!!h}}case"wallet-prepare-transfer":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},u=String(e.receiver??"").trim().toLowerCase(),c=s.short_address.endsWith(".cltc")?"cltc":"clc";let h=u;if(new RegExp(`^[0-9a-f]{40}\\.${c}$`).test(u)){const g=await fetch(`${y(n)}/api/v1/addresses/registration?address=${encodeURIComponent(h)}`,{headers:o}),k=await g.json().catch(()=>{});if(!g.ok||(k==null?void 0:k.success)!==!0)throw new Error((k==null?void 0:k.error)??`Address lookup failed with HTTP ${g.status}.`);if(((K=k.data)==null?void 0:K.registered)!==!0)throw new Error("The receiving wallet is not registered.")}else{if(!new RegExp(`^[a-z]{1,20}\\.${c}$`).test(u))throw new Error(`Enter a valid Contractless ${c.toUpperCase()} address.`);const g=await fetch(`${y(n)}/api/v1/addresses/vanity/resolve?address=${encodeURIComponent(u)}`,{headers:o}),k=await g.json().catch(()=>{});if(!g.ok||(k==null?void 0:k.success)!==!0)throw new Error((k==null?void 0:k.error)??`Vanity address lookup failed with HTTP ${g.status}.`);if(h=String(((W=k.data)==null?void 0:W.owner_address)??""),!h)throw new Error("That vanity address is not registered.")}const p=Number(e.nftSeries??0);if(!Number.isInteger(p)||p<0||p>4294967295)throw new Error("The NFT/RWA series number is invalid.");const w={txtype:2,time:Math.floor(Date.now()/1e3),value:String(e.valueAtomic??""),coin:String(e.asset??"").trim().padEnd(15," "),nft_series:p,sender:s.short_address,receiver:h,txfee:String(e.feeAtomic??"")},_=JSON.parse(a.inspect_transaction_request(JSON.stringify(w)));return await T(),{transaction:w,review:_.review,hash:_.hash,receiverInput:u}}case"wallet-broadcast-transfer":{if(!s)throw new Error("Wallet is locked.");const n=e.transaction;if(!n||typeof n!="object")throw new Error("The reviewed transfer is unavailable.");const i=JSON.parse(a.sign_transaction_request(JSON.stringify(n),s.private_key,s.public_key,1));return bt(a,i)}case"wallet-activity":{if(!s)throw new Error("Wallet is locked.");const n=await S(),i=a.create_handshake_proof(s.public_key,s.private_key),o=encodeURIComponent(s.short_address),u={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":i},[c,h]=await Promise.all([fetch(`${y(n)}/api/v1/addresses/latest?address=${o}&limit=25`,{headers:u}),fetch(`${y(n)}/api/v1/height`,{headers:u})]),[p,w]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{})]);if(!c.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Activity lookup failed with HTTP ${c.status}.`);if(!h.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Block height lookup failed with HTTP ${h.status}.`);return await T(),{address:s.short_address,current_height:Number(((N=w.data)==null?void 0:N.height)??0),transactions:Array.isArray((I=p.data)==null?void 0:I.transactions)?p.data.transactions:[]}}case"wallet-transaction":{if(!s)throw new Error("Wallet is locked.");const n=String(e.txid);if(!/^[0-9a-f]{64}$/i.test(n))throw new Error("A valid transaction ID is required.");const i=await S(),o=a.create_handshake_proof(s.public_key,s.private_key),u={"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":o},[c,h]=await Promise.all([fetch(`${y(i)}/api/v1/transactions/lookup?txid=${encodeURIComponent(n)}`,{headers:u}),fetch(`${y(i)}/api/v1/height`,{headers:u})]),[p,w]=await Promise.all([c.json().catch(()=>{}),h.json().catch(()=>{})]);if(!c.ok||(p==null?void 0:p.success)!==!0)throw new Error((p==null?void 0:p.error)??`Transaction lookup failed with HTTP ${c.status}.`);if(!h.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Block height lookup failed with HTTP ${h.status}.`);return await T(),{...p.data,current_height:Number((($=w.data)==null?void 0:$.height)??0)}}case"export-wallet":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");return n.walletJson}case"export-legacy-wallet":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");return a.export_legacy_wallet(n.walletJson,String(e.password),String(e.legacyPassword))}case"export-private-key-image":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");const i=JSON.parse(a.export_legacy_wallet(n.walletJson,String(e.password),String(e.imagePassword)));if(typeof i.private_key!="string")throw new Error("The wallet image could not be created.");return i.private_key}case"reveal-private-key":{const n=O(await C(),r.selectedNetwork);if(!n)throw new Error("No wallet exists.");const i=JSON.parse(a.unlock_browser_wallet(n.walletJson,String(e.password)));if(typeof i.private_key!="string")throw new Error("The private key could not be decrypted.");return await T(),{label:n.label,address:n.address,privateKey:i.private_key}}case"provider-request":{const n=$t(t),i=String(e.method),o=e.params&&typeof e.params=="object"?e.params:{};if(i==="contractless_detect")return{installed:!0,providerVersion:"1.0.0"};if(i==="contractless_connect"){rt(o,n);const c=P(o.appId),h=_t(o.timestamp);mt(o.nonce);const p=kt(o.message),w={id:crypto.randomUUID(),method:i,params:{...o,walletSessionId:s==null?void 0:s.sessionId},origin:n,createdAt:Date.now(),risk:it(i,o),display:{request:"Connect application",application_id:c,website:n,wallet_address:(s==null?void 0:s.short_address)??"Unlock wallet to continue",message:p,requested_timestamp:h,access:"Address access and future wallet requests until this wallet locks"}};if(await lt(w),!await dt(w.id))throw new Error("The wallet connection request was rejected.");return wt(a,w)}if(i==="contractless_accounts")try{return await U(o,n),s?[s.short_address]:[]}catch{return[]}if(i==="contractless_balances"){if(await U(o,n),!s)throw new Error("Wallet is locked.");const c=await S(),h=a.create_handshake_proof(s.public_key,s.private_key),p=await fetch(`${y(c)}/api/v1/balances?address=${encodeURIComponent(s.short_address)}`,{headers:{"X-Contractless-Address":s.short_address,"X-Contractless-Public-Key":s.public_key,"X-Contractless-Signature":h}}),w=await p.json().catch(()=>{});if(!p.ok||(w==null?void 0:w.success)!==!0)throw new Error((w==null?void 0:w.error)??`Balance lookup failed with HTTP ${p.status}.`);return await T(),w.data}if(i==="contractless_disconnect"){const c=await U(o,n),h=(await et()).filter(p=>!(p.origin===c.origin&&p.appId===c.appId&&p.accessKey===c.accessKey));return await ft(h),{disconnected:!0}}if(i==="contractless_completeDualTransaction"){await U(o,n);const c=Z(o.transaction);return JSON.parse(a.complete_dual_transaction_request(JSON.stringify(c),String(o.signature1??""),String(o.publicKey1??""),String(o.signature2??""),String(o.publicKey2??"")))}if(!["contractless_signMessage","contractless_sendTransaction","contractless_signDualTransaction"].includes(i))throw new Error("The website requested an unsupported wallet method.");if(await U(o,n),i==="contractless_sendTransaction"){const c=vt(o.transaction);await ct(a,c)}else if(i==="contractless_signDualTransaction"){const c=Z(o.transaction);await ct(a,c)}const u={id:crypto.randomUUID(),method:i,params:o,origin:n,createdAt:Date.now(),risk:it(i,o),display:i==="contractless_signMessage"?{request:"Sign a text message",application_id:P(o.appId),website:n,message:yt(o.message),warning:"This is a text signature. It cannot move wallet assets."}:i==="contractless_signDualTransaction"?(()=>{const c=JSON.parse(a.inspect_transaction_request(JSON.stringify(o.transaction)));return{application_id:P(o.appId),website:n,transaction_type:c.transaction_name,signer_slot:o.signerSlot,transaction_fields:c.review,action:"Sign and return this signature to the application without broadcasting."}})():(()=>{const c=JSON.parse(a.inspect_transaction_request(JSON.stringify(o.transaction)));return{application_id:P(o.appId),website:n,transaction_type:c.transaction_name,transaction_fields:c.review,action:"Sign and broadcast this transaction."}})()};if(await lt(u),!await dt(u.id))throw new Error("The wallet request was rejected.");return wt(a,u)}case"pending-provider-approval":{const n=await Y(),i=n[0]??"";if(!i)return null;const o=`approval:${i}`,c=(await chrome.storage.session.get(o))[o];return c?s?(c.method==="contractless_connect"&&(c.params={...c.params,walletSessionId:s.sessionId},c.display={...c.display??{},wallet_address:s.short_address},await chrome.storage.session.set({[o]:c})),{locked:!1,request:c,position:1,total:n.length}):{locked:!0}:(await tt(i),null)}case"resolve-provider-approval":{if(!s)throw new Error("Unlock the wallet before approving this request.");const n=String(e.id??""),i=await Y();if(!n||i[0]!==n)throw new Error("The wallet approval request has expired.");const o=`approval:${n}`,c=(await chrome.storage.session.get(o))[o];if(!c)throw new Error("The wallet approval request has expired.");return c.method==="contractless_connect"&&(c.params={...c.params,walletSessionId:s.sessionId},await chrome.storage.session.set({[o]:c})),await chrome.storage.session.set({[`approval-result:${n}`]:{approved:e.approved===!0}}),await chrome.storage.session.remove(o),{resolved:!0,remaining:(await tt(n)).length}}default:throw new Error("Unsupported wallet request.")}}chrome.runtime.onMessage.addListener((e,t,r)=>(Ct(e,t).then(a=>r({ok:!0,result:a})).catch(a=>r({ok:!1,error:a instanceof Error?a.message:String(a)})),!0));chrome.alarms.onAlarm.addListener(e=>{e.name===V&&D()}); diff --git a/extension/dist/firefox/chunks/core-D2IK1U91.js b/extension/dist/firefox/chunks/core-D2IK1U91.js new file mode 100644 index 0000000..ace83d1 --- /dev/null +++ b/extension/dist/firefox/chunks/core-D2IK1U91.js @@ -0,0 +1 @@ +function _e(n){let e,t;try{const i=F(n,o.__wbindgen_malloc),l=w,s=o.bytes_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function re(n,e,t,_,r){let i,l;try{const f=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),A=w,W=u(_,o.__wbindgen_malloc,o.__wbindgen_realloc),$=w,z=u(r,o.__wbindgen_malloc,o.__wbindgen_realloc),G=w,T=o.complete_dual_transaction_request(f,d,m,p,b,A,W,$,z,G);var s=T[0],a=T[1];if(T[3])throw s=0,a=0,x(T[2]);return i=s,l=a,h(s,a)}finally{o.__wbindgen_free(i,l,1)}}function oe(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.create_handshake_proof(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function ce(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.create_wallet_registration(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function ie(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.create_wallet_registration_from_keys(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function se(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.decode_transaction(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function le(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.decode_transfer(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function ae(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.export_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function de(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.generate_browser_wallet(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function fe(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.import_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function be(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.import_private_key(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function we(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.import_private_key_image(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function ge(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.inspect_transaction_request(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function ue(n){let e,t;try{const i=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=o.public_key_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,x(s[2]);return e=_,t=r,h(_,r)}finally{o.__wbindgen_free(e,t,1)}}function ye(n){const e=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),t=w,_=o.short_address_to_bytes(e,t);if(_[3])throw x(_[2]);var r=Y(_[0],_[1]).slice();return o.__wbindgen_free(_[0],_[1]*1,1),r}function me(n,e,t){let _,r;try{const s=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),d=w,m=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),p=w,b=o.sign_message_request(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,x(b[2]);return _=i,r=l,h(i,l)}finally{o.__wbindgen_free(_,r,1)}}function pe(n,e,t,_){let r,i;try{const a=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),m=w,p=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),b=w,A=o.sign_transaction_request(a,f,d,m,p,b,_);var l=A[0],s=A[1];if(A[3])throw l=0,s=0,x(A[2]);return r=l,i=s,h(l,s)}finally{o.__wbindgen_free(r,i,1)}}function he(n){let e,t;try{const _=F(n,o.__wbindgen_malloc),r=w,i=o.skein_128_hex(_,r);return e=i[0],t=i[1],h(i[0],i[1])}finally{o.__wbindgen_free(e,t,1)}}function ve(n){let e,t;try{const _=F(n,o.__wbindgen_malloc),r=w,i=o.skein_256_hex(_,r);return e=i[0],t=i[1],h(i[0],i[1])}finally{o.__wbindgen_free(e,t,1)}}function xe(n){let e,t;try{const _=F(n,o.__wbindgen_malloc),r=w,i=o.skein_512_hex(_,r);return e=i[0],t=i[1],h(i[0],i[1])}finally{o.__wbindgen_free(e,t,1)}}function ke(){let n,e;try{const r=o.transaction_type_registry();var t=r[0],_=r[1];if(r[3])throw t=0,_=0,x(r[2]);return n=t,e=_,h(t,_)}finally{o.__wbindgen_free(n,e,1)}}function Ae(n,e){let t,_;try{const l=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),s=w,a=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=o.unlock_browser_wallet(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,x(d[2]);return t=r,_=i,h(r,i)}finally{o.__wbindgen_free(t,_,1)}}function $e(n,e,t){const _=F(n,o.__wbindgen_malloc),r=w,i=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=o.verify_bytes_hex(_,r,i,l,s,a);if(f[2])throw x(f[1]);return f[0]!==0}function Te(n,e,t){const _=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=w,i=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),l=w,s=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),a=w,f=o.verify_hash_hex(_,r,i,l,s,a);if(f[2])throw x(f[1]);return f[0]!==0}function We(n,e,t,_){let r,i;try{const a=u(n,o.__wbindgen_malloc,o.__wbindgen_realloc),f=w,d=u(e,o.__wbindgen_malloc,o.__wbindgen_realloc),m=w,p=u(t,o.__wbindgen_malloc,o.__wbindgen_realloc),b=w,A=u(_,o.__wbindgen_malloc,o.__wbindgen_realloc),W=w,$=o.verify_message_proof(a,f,d,m,p,b,A,W);var l=$[0],s=$[1];if($[3])throw l=0,s=0,x($[2]);return r=l,i=s,h(l,s)}finally{o.__wbindgen_free(r,i,1)}}function J(){return{__proto__:null,"./contractless_browser_core_bg.js":{__proto__:null,__wbg___wbindgen_is_function_1ff95bcc5517c252:function(e){return typeof e=="function"},__wbg___wbindgen_is_object_a27215656b807791:function(e){const t=e;return typeof t=="object"&&t!==null},__wbg___wbindgen_is_string_ea5e6cc2e4141dfe:function(e){return typeof e=="string"},__wbg___wbindgen_is_undefined_c05833b95a3cf397:function(e){return e===void 0},__wbg___wbindgen_throw_344f42d3211c4765:function(e,t){throw new Error(h(e,t))},__wbg_call_a6e5c5dce5018821:function(){return U(function(e,t,_){return e.call(t,_)},arguments)},__wbg_crypto_38df2bab126b63dc:function(e){return e.crypto},__wbg_getRandomValues_c44a50d8cfdaebeb:function(){return U(function(e,t){e.getRandomValues(t)},arguments)},__wbg_length_1f0964f4a5e2c6d8:function(e){return e.length},__wbg_msCrypto_bd5a034af96bcba6:function(e){return e.msCrypto},__wbg_new_with_length_e6785c33c8e4cce8:function(e){return new Uint8Array(e>>>0)},__wbg_node_84ea875411254db1:function(e){return e.node},__wbg_process_44c7a14e11e9f69e:function(e){return e.process},__wbg_prototypesetcall_4770620bbe4688a0:function(e,t,_){Uint8Array.prototype.set.call(Y(e,t),_)},__wbg_randomFillSync_6c25eac9869eb53c:function(){return U(function(e,t){e.randomFillSync(t)},arguments)},__wbg_require_b4edbdcf3e2a1ef0:function(){return U(function(){return module.require},arguments)},__wbg_static_accessor_GLOBAL_4ef717fb391d88b7:function(){const e=typeof global>"u"?null:global;return D(e)?0:q(e)},__wbg_static_accessor_GLOBAL_THIS_8d1badc68b5a74f4:function(){const e=typeof globalThis>"u"?null:globalThis;return D(e)?0:q(e)},__wbg_static_accessor_SELF_146583524fe1469b:function(){const e=typeof self>"u"?null:self;return D(e)?0:q(e)},__wbg_static_accessor_WINDOW_f2829a2234d7819e:function(){const e=typeof window>"u"?null:window;return D(e)?0:q(e)},__wbg_subarray_3ed232c8a6baee09:function(e,t,_){return e.subarray(t>>>0,_>>>0)},__wbg_versions_276b2795b1c6a219:function(e){return e.versions},__wbindgen_cast_0000000000000001:function(e,t){return Y(e,t)},__wbindgen_cast_0000000000000002:function(e,t){return h(e,t)},__wbindgen_init_externref_table:function(){const e=o.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}}}}function q(n){const e=o.__externref_table_alloc();return o.__wbindgen_externrefs.set(e,n),e}function Y(n,e){return n=n>>>0,S().subarray(n/1,n/1+e)}function h(n,e){return Oe(n>>>0,e)}let R=null;function S(){return(R===null||R.byteLength===0)&&(R=new Uint8Array(o.memory.buffer)),R}function U(n,e){try{return n.apply(this,e)}catch(t){const _=q(t);o.__wbindgen_exn_store(_)}}function D(n){return n==null}function F(n,e){const t=e(n.length*1,1)>>>0;return S().set(n,t/1),w=n.length,t}function u(n,e,t){if(t===void 0){const s=j.encode(n),a=e(s.length,1)>>>0;return S().subarray(a,a+s.length).set(s),w=s.length,a}let _=n.length,r=e(_,1)>>>0;const i=S();let l=0;for(;l<_;l++){const s=n.charCodeAt(l);if(s>127)break;i[r+l]=s}if(l!==_){l!==0&&(n=n.slice(l)),r=t(r,_,_=l+n.length*3,1)>>>0;const s=S().subarray(r+l,r+_),a=j.encodeInto(n,s);l+=a.written,r=t(r,_,l,1)>>>0}return w=l,r}function x(n){const e=o.__wbindgen_externrefs.get(n);return o.__externref_table_dealloc(n),e}let N=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});N.decode();const Se=2146435072;let H=0;function Oe(n,e){return H+=e,H>=Se&&(N=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),N.decode(),H=e),N.decode(S().subarray(n,n+e))}const j=new TextEncoder;"encodeInto"in j||(j.encodeInto=function(n,e){const t=j.encode(n);return e.set(t),{read:n.length,written:t.length}});let w=0,o;function K(n,e){return o=n.exports,R=null,o.__wbindgen_start(),o}async function qe(n,e){if(typeof Response=="function"&&n instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(n,e)}catch(r){if(n.ok&&t(n.type)&&n.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}const _=await n.arrayBuffer();return await WebAssembly.instantiate(_,e)}else{const _=await WebAssembly.instantiate(n,e);return _ instanceof WebAssembly.Instance?{instance:_,module:n}:_}function t(_){switch(_){case"basic":case"cors":case"default":return!0}return!1}}function Re(n){if(o!==void 0)return o;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module:n}=n:console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const e=J();n instanceof WebAssembly.Module||(n=new WebAssembly.Module(n));const t=new WebAssembly.Instance(n,e);return K(t)}async function Q(n){if(o!==void 0)return o;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module_or_path:n}=n:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),n===void 0&&(n=new URL("/assets/contractless_browser_core_bg-Dl61GrlP.wasm",import.meta.url));const e=J();(typeof n=="string"||typeof Request=="function"&&n instanceof Request||typeof URL=="function"&&n instanceof URL)&&(n=fetch(n));const{instance:t,module:_}=await qe(await n,e);return K(t)}const Ee=Object.freeze(Object.defineProperty({__proto__:null,bytes_to_short_address:_e,complete_dual_transaction_request:re,create_handshake_proof:oe,create_wallet_registration:ce,create_wallet_registration_from_keys:ie,decode_transaction:se,decode_transfer:le,default:Q,export_legacy_wallet:ae,generate_browser_wallet:de,import_legacy_wallet:fe,import_private_key:be,import_private_key_image:we,initSync:Re,inspect_transaction_request:ge,public_key_to_short_address:ue,short_address_to_bytes:ye,sign_message_request:me,sign_transaction_request:pe,skein_128_hex:he,skein_256_hex:ve,skein_512_hex:xe,transaction_type_registry:ke,unlock_browser_wallet:Ae,verify_bytes_hex:$e,verify_hash_hex:Te,verify_message_proof:We},Symbol.toStringTag,{value:"Module"}));function Me(n){let e,t;try{const i=I(n,c.__wbindgen_malloc),l=g,s=c.bytes_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function je(n,e,t,_,r){let i,l;try{const f=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),A=g,W=y(_,c.__wbindgen_malloc,c.__wbindgen_realloc),$=g,z=y(r,c.__wbindgen_malloc,c.__wbindgen_realloc),G=g,T=c.complete_dual_transaction_request(f,d,m,p,b,A,W,$,z,G);var s=T[0],a=T[1];if(T[3])throw s=0,a=0,k(T[2]);return i=s,l=a,v(s,a)}finally{c.__wbindgen_free(i,l,1)}}function Le(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.create_handshake_proof(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function Fe(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.create_wallet_registration(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function Ie(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.create_wallet_registration_from_keys(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ue(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.decode_transaction(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function De(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.decode_transfer(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function Be(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.export_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ce(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.generate_browser_wallet(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function Pe(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.import_legacy_wallet(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ne(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.import_private_key(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function Ve(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.import_private_key_image(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function ze(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.inspect_transaction_request(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function Ge(n){let e,t;try{const i=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=c.public_key_to_short_address(i,l);var _=s[0],r=s[1];if(s[3])throw _=0,r=0,k(s[2]);return e=_,t=r,v(_,r)}finally{c.__wbindgen_free(e,t,1)}}function He(n){const e=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),t=g,_=c.short_address_to_bytes(e,t);if(_[3])throw k(_[2]);var r=Z(_[0],_[1]).slice();return c.__wbindgen_free(_[0],_[1]*1,1),r}function Xe(n,e,t){let _,r;try{const s=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),d=g,m=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),p=g,b=c.sign_message_request(s,a,f,d,m,p);var i=b[0],l=b[1];if(b[3])throw i=0,l=0,k(b[2]);return _=i,r=l,v(i,l)}finally{c.__wbindgen_free(_,r,1)}}function Ye(n,e,t,_){let r,i;try{const a=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),m=g,p=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),b=g,A=c.sign_transaction_request(a,f,d,m,p,b,_);var l=A[0],s=A[1];if(A[3])throw l=0,s=0,k(A[2]);return r=l,i=s,v(l,s)}finally{c.__wbindgen_free(r,i,1)}}function Ze(n){let e,t;try{const _=I(n,c.__wbindgen_malloc),r=g,i=c.skein_128_hex(_,r);return e=i[0],t=i[1],v(i[0],i[1])}finally{c.__wbindgen_free(e,t,1)}}function Je(n){let e,t;try{const _=I(n,c.__wbindgen_malloc),r=g,i=c.skein_256_hex(_,r);return e=i[0],t=i[1],v(i[0],i[1])}finally{c.__wbindgen_free(e,t,1)}}function Ke(n){let e,t;try{const _=I(n,c.__wbindgen_malloc),r=g,i=c.skein_512_hex(_,r);return e=i[0],t=i[1],v(i[0],i[1])}finally{c.__wbindgen_free(e,t,1)}}function Qe(){let n,e;try{const r=c.transaction_type_registry();var t=r[0],_=r[1];if(r[3])throw t=0,_=0,k(r[2]);return n=t,e=_,v(t,_)}finally{c.__wbindgen_free(n,e,1)}}function en(n,e){let t,_;try{const l=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),s=g,a=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=c.unlock_browser_wallet(l,s,a,f);var r=d[0],i=d[1];if(d[3])throw r=0,i=0,k(d[2]);return t=r,_=i,v(r,i)}finally{c.__wbindgen_free(t,_,1)}}function nn(n,e,t){const _=I(n,c.__wbindgen_malloc),r=g,i=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=c.verify_bytes_hex(_,r,i,l,s,a);if(f[2])throw k(f[1]);return f[0]!==0}function tn(n,e,t){const _=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),r=g,i=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),l=g,s=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),a=g,f=c.verify_hash_hex(_,r,i,l,s,a);if(f[2])throw k(f[1]);return f[0]!==0}function _n(n,e,t,_){let r,i;try{const a=y(n,c.__wbindgen_malloc,c.__wbindgen_realloc),f=g,d=y(e,c.__wbindgen_malloc,c.__wbindgen_realloc),m=g,p=y(t,c.__wbindgen_malloc,c.__wbindgen_realloc),b=g,A=y(_,c.__wbindgen_malloc,c.__wbindgen_realloc),W=g,$=c.verify_message_proof(a,f,d,m,p,b,A,W);var l=$[0],s=$[1];if($[3])throw l=0,s=0,k($[2]);return r=l,i=s,v(l,s)}finally{c.__wbindgen_free(r,i,1)}}function ee(){return{__proto__:null,"./contractless_browser_core_bg.js":{__proto__:null,__wbg___wbindgen_is_function_1ff95bcc5517c252:function(e){return typeof e=="function"},__wbg___wbindgen_is_object_a27215656b807791:function(e){const t=e;return typeof t=="object"&&t!==null},__wbg___wbindgen_is_string_ea5e6cc2e4141dfe:function(e){return typeof e=="string"},__wbg___wbindgen_is_undefined_c05833b95a3cf397:function(e){return e===void 0},__wbg___wbindgen_throw_344f42d3211c4765:function(e,t){throw new Error(v(e,t))},__wbg_call_a6e5c5dce5018821:function(){return B(function(e,t,_){return e.call(t,_)},arguments)},__wbg_crypto_38df2bab126b63dc:function(e){return e.crypto},__wbg_getRandomValues_c44a50d8cfdaebeb:function(){return B(function(e,t){e.getRandomValues(t)},arguments)},__wbg_length_1f0964f4a5e2c6d8:function(e){return e.length},__wbg_msCrypto_bd5a034af96bcba6:function(e){return e.msCrypto},__wbg_new_with_length_e6785c33c8e4cce8:function(e){return new Uint8Array(e>>>0)},__wbg_node_84ea875411254db1:function(e){return e.node},__wbg_process_44c7a14e11e9f69e:function(e){return e.process},__wbg_prototypesetcall_4770620bbe4688a0:function(e,t,_){Uint8Array.prototype.set.call(Z(e,t),_)},__wbg_randomFillSync_6c25eac9869eb53c:function(){return B(function(e,t){e.randomFillSync(t)},arguments)},__wbg_require_b4edbdcf3e2a1ef0:function(){return B(function(){return module.require},arguments)},__wbg_static_accessor_GLOBAL_4ef717fb391d88b7:function(){const e=typeof global>"u"?null:global;return C(e)?0:E(e)},__wbg_static_accessor_GLOBAL_THIS_8d1badc68b5a74f4:function(){const e=typeof globalThis>"u"?null:globalThis;return C(e)?0:E(e)},__wbg_static_accessor_SELF_146583524fe1469b:function(){const e=typeof self>"u"?null:self;return C(e)?0:E(e)},__wbg_static_accessor_WINDOW_f2829a2234d7819e:function(){const e=typeof window>"u"?null:window;return C(e)?0:E(e)},__wbg_subarray_3ed232c8a6baee09:function(e,t,_){return e.subarray(t>>>0,_>>>0)},__wbg_versions_276b2795b1c6a219:function(e){return e.versions},__wbindgen_cast_0000000000000001:function(e,t){return Z(e,t)},__wbindgen_cast_0000000000000002:function(e,t){return v(e,t)},__wbindgen_init_externref_table:function(){const e=c.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}}}}function E(n){const e=c.__externref_table_alloc();return c.__wbindgen_externrefs.set(e,n),e}function Z(n,e){return n=n>>>0,O().subarray(n/1,n/1+e)}function v(n,e){return on(n>>>0,e)}let M=null;function O(){return(M===null||M.byteLength===0)&&(M=new Uint8Array(c.memory.buffer)),M}function B(n,e){try{return n.apply(this,e)}catch(t){const _=E(t);c.__wbindgen_exn_store(_)}}function C(n){return n==null}function I(n,e){const t=e(n.length*1,1)>>>0;return O().set(n,t/1),g=n.length,t}function y(n,e,t){if(t===void 0){const s=L.encode(n),a=e(s.length,1)>>>0;return O().subarray(a,a+s.length).set(s),g=s.length,a}let _=n.length,r=e(_,1)>>>0;const i=O();let l=0;for(;l<_;l++){const s=n.charCodeAt(l);if(s>127)break;i[r+l]=s}if(l!==_){l!==0&&(n=n.slice(l)),r=t(r,_,_=l+n.length*3,1)>>>0;const s=O().subarray(r+l,r+_),a=L.encodeInto(n,s);l+=a.written,r=t(r,_,l,1)>>>0}return g=l,r}function k(n){const e=c.__wbindgen_externrefs.get(n);return c.__externref_table_dealloc(n),e}let V=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});V.decode();const rn=2146435072;let X=0;function on(n,e){return X+=e,X>=rn&&(V=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),V.decode(),X=e),V.decode(O().subarray(n,n+e))}const L=new TextEncoder;"encodeInto"in L||(L.encodeInto=function(n,e){const t=L.encode(n);return e.set(t),{read:n.length,written:t.length}});let g=0,c;function ne(n,e){return c=n.exports,M=null,c.__wbindgen_start(),c}async function cn(n,e){if(typeof Response=="function"&&n instanceof Response){if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(n,e)}catch(r){if(n.ok&&t(n.type)&&n.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",r);else throw r}const _=await n.arrayBuffer();return await WebAssembly.instantiate(_,e)}else{const _=await WebAssembly.instantiate(n,e);return _ instanceof WebAssembly.Instance?{instance:_,module:n}:_}function t(_){switch(_){case"basic":case"cors":case"default":return!0}return!1}}function sn(n){if(c!==void 0)return c;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module:n}=n:console.warn("using deprecated parameters for `initSync()`; pass a single object instead"));const e=ee();n instanceof WebAssembly.Module||(n=new WebAssembly.Module(n));const t=new WebAssembly.Instance(n,e);return ne(t)}async function te(n){if(c!==void 0)return c;n!==void 0&&(Object.getPrototypeOf(n)===Object.prototype?{module_or_path:n}=n:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),n===void 0&&(n=new URL("/assets/contractless_browser_core_bg-CM1Z7v2F.wasm",import.meta.url));const e=ee();(typeof n=="string"||typeof Request=="function"&&n instanceof Request||typeof URL=="function"&&n instanceof URL)&&(n=fetch(n));const{instance:t,module:_}=await cn(await n,e);return ne(t)}const ln=Object.freeze(Object.defineProperty({__proto__:null,bytes_to_short_address:Me,complete_dual_transaction_request:je,create_handshake_proof:Le,create_wallet_registration:Fe,create_wallet_registration_from_keys:Ie,decode_transaction:Ue,decode_transfer:De,default:te,export_legacy_wallet:Be,generate_browser_wallet:Ce,import_legacy_wallet:Pe,import_private_key:Ne,import_private_key_image:Ve,initSync:sn,inspect_transaction_request:ze,public_key_to_short_address:Ge,short_address_to_bytes:He,sign_message_request:Xe,sign_transaction_request:Ye,skein_128_hex:Ze,skein_256_hex:Je,skein_512_hex:Ke,transaction_type_registry:Qe,unlock_browser_wallet:en,verify_bytes_hex:nn,verify_hash_hex:tn,verify_message_proof:_n},Symbol.toStringTag,{value:"Module"})),P={};async function an(n="testnet"){const e=n==="mainnet"?ln:Ee;return P[n]??(P[n]=n==="mainnet"?te():Q()),await P[n],e}export{an as c}; diff --git a/extension/dist/firefox/chunks/modulepreload-polyfill-B5Qt9EMX.js b/extension/dist/firefox/chunks/modulepreload-polyfill-B5Qt9EMX.js new file mode 100644 index 0000000..3e0a78a --- /dev/null +++ b/extension/dist/firefox/chunks/modulepreload-polyfill-B5Qt9EMX.js @@ -0,0 +1 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})(); diff --git a/extension/dist/firefox/content.js b/extension/dist/firefox/content.js new file mode 100644 index 0000000..0ce4e56 --- /dev/null +++ b/extension/dist/firefox/content.js @@ -0,0 +1 @@ +const o=document.createElement("script");o.src=chrome.runtime.getURL("provider.js");o.onload=()=>o.remove();(document.head||document.documentElement).appendChild(o);window.addEventListener("message",e=>{var t;e.source!==window||((t=e.data)==null?void 0:t.source)!=="contractless-page"||chrome.runtime.sendMessage({type:"provider-request",method:e.data.method,params:e.data.params},a=>{window.postMessage({source:"contractless-wallet",id:e.data.id,response:a},window.location.origin)})}); diff --git a/extension/dist/firefox/contractless-logo-128.png b/extension/dist/firefox/contractless-logo-128.png new file mode 100644 index 0000000..86681b0 Binary files /dev/null and b/extension/dist/firefox/contractless-logo-128.png differ diff --git a/extension/dist/firefox/contractless-logo-16.png b/extension/dist/firefox/contractless-logo-16.png new file mode 100644 index 0000000..6d69407 Binary files /dev/null and b/extension/dist/firefox/contractless-logo-16.png differ diff --git a/extension/dist/firefox/contractless-logo-32.png b/extension/dist/firefox/contractless-logo-32.png new file mode 100644 index 0000000..9d949e9 Binary files /dev/null and b/extension/dist/firefox/contractless-logo-32.png differ diff --git a/extension/dist/firefox/contractless-logo-48.png b/extension/dist/firefox/contractless-logo-48.png new file mode 100644 index 0000000..3697789 Binary files /dev/null and b/extension/dist/firefox/contractless-logo-48.png differ diff --git a/extension/dist/firefox/contractless-logo.png b/extension/dist/firefox/contractless-logo.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/dist/firefox/contractless-logo.png differ diff --git a/extension/dist/firefox/import-wallet.html b/extension/dist/firefox/import-wallet.html new file mode 100644 index 0000000..941281a --- /dev/null +++ b/extension/dist/firefox/import-wallet.html @@ -0,0 +1,70 @@ + + + + + + Import Contractless Wallet + + + + + + + +
+
+ Restore Contractless wallet +

Import wallet image

+

+ Select your encrypted Contractless private-key image and enter the + encryption key originally used to create it. +

+ +
+ + + + +
+ + +
+ +
+ Processed locally by Contractless Wallet + The wallet image and encryption key are not sent to the website you were visiting. +
+ + + +
+
+ + +
+ + + diff --git a/extension/dist/firefox/import-wallet.js b/extension/dist/firefox/import-wallet.js new file mode 100644 index 0000000..7b1b204 --- /dev/null +++ b/extension/dist/firefox/import-wallet.js @@ -0,0 +1 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";const a=t=>document.getElementById(t),u=a("import-form"),l=a("wallet-image"),r=a("image-password"),s=a("import-button"),i=a("notice");m("status").then(t=>{const e=t;a("selected-network").textContent=e.networkName}).catch(t=>{i.textContent=d(t)});function w(t){let e="";for(let o=0;o{var t,e;a("image-file-name").textContent=((e=(t=l.files)==null?void 0:t[0])==null?void 0:e.name)??"Choose wallet image",i.textContent=""});a("reveal-password").addEventListener("click",t=>{const e=t.currentTarget,n=r.type==="text";r.type=n?"password":"text",e.textContent=n?"Show":"Hide"});u.addEventListener("submit",t=>{t.preventDefault(),(async()=>{var c;const e=(c=l.files)==null?void 0:c[0];if(!e)throw new Error("Choose a wallet image.");if(!r.value.trim())throw new Error("Enter the wallet image encryption key.");i.textContent="",s.disabled=!0,s.textContent="Importing wallet...";const n=w(new Uint8Array(await e.arrayBuffer())),o=await m("import-wallet-image",{imageBase64:n,imagePassword:r.value,password:r.value});r.value="",a("imported-address").textContent=o.address??"Imported wallet",a("import-panel").hidden=!0,a("success-panel").hidden=!1})().catch(e=>{i.textContent=d(e),s.disabled=!1,s.textContent="Import wallet"})});a("close-page").addEventListener("click",()=>window.close()); diff --git a/extension/dist/firefox/manifest.json b/extension/dist/firefox/manifest.json new file mode 100644 index 0000000..389d071 --- /dev/null +++ b/extension/dist/firefox/manifest.json @@ -0,0 +1,57 @@ +{ + "manifest_version": 3, + "name": "Contractless Wallet", + "version": "0.1.1", + "description": "A local-signing Web3 wallet for Contractless.", + "browser_specific_settings": { + "gecko": { + "id": "wallet@contractless.dev", + "strict_min_version": "149.0", + "data_collection_permissions": { + "required": ["none"] + } + }, + "gecko_android": { + "strict_min_version": "149.0" + } + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "permissions": ["storage", "alarms", "activeTab", "clipboardWrite"], + "host_permissions": ["https://api.contractless.dev/*"], + "optional_host_permissions": ["https://*/"], + "action": { + "default_popup": "popup.html", + "default_title": "Contractless Wallet", + "default_icon": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + } + }, + "icons": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + }, + "background": { + "scripts": ["background.js"], + "type": "module" + }, + "content_scripts": [ + { + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"], + "js": ["content.js"], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": ["provider.js"], + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"] + } + ] +} diff --git a/extension/dist/firefox/popup.html b/extension/dist/firefox/popup.html new file mode 100644 index 0000000..1aaf6fa --- /dev/null +++ b/extension/dist/firefox/popup.html @@ -0,0 +1,654 @@ + + + + + + Contractless Wallet + + + + + + +
+
+
+ Contractless +
+
+

Opening wallet

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/extension/dist/firefox/popup.js b/extension/dist/firefox/popup.js new file mode 100644 index 0000000..f57b0b4 --- /dev/null +++ b/extension/dist/firefox/popup.js @@ -0,0 +1,8 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";import{c as Yt}from"./chunks/core-D2IK1U91.js";function Cn(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var de={},De,Ct;function En(){return Ct||(Ct=1,De=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}),De}var Ue={},ee={},Et;function se(){if(Et)return ee;Et=1;let e;const t=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];return ee.getSymbolSize=function(o){if(!o)throw new Error('"version" cannot be null or undefined');if(o<1||o>40)throw new Error('"version" should be in range from 1 to 40');return o*4+17},ee.getSymbolTotalCodewords=function(o){return t[o]},ee.getBCHDigit=function(n){let o=0;for(;n!==0;)o++,n>>>=1;return o},ee.setToSJISFunction=function(o){if(typeof o!="function")throw new Error('"toSJISFunc" is not a valid function.');e=o},ee.isKanjiModeEnabled=function(){return typeof e<"u"},ee.toSJIS=function(o){return e(o)},ee}var qe={},bt;function lt(){return bt||(bt=1,(function(e){e.L={bit:1},e.M={bit:0},e.Q={bit:3},e.H={bit:2};function t(n){if(typeof n!="string")throw new Error("Param is not a string");switch(n.toLowerCase()){case"l":case"low":return e.L;case"m":case"medium":return e.M;case"q":case"quartile":return e.Q;case"h":case"high":return e.H;default:throw new Error("Unknown EC Level: "+n)}}e.isValid=function(o){return o&&typeof o.bit<"u"&&o.bit>=0&&o.bit<4},e.from=function(o,r){if(e.isValid(o))return o;try{return t(o)}catch{return r}}})(qe)),qe}var Fe,kt;function bn(){if(kt)return Fe;kt=1;function e(){this.buffer=[],this.length=0}return e.prototype={get:function(t){const n=Math.floor(t/8);return(this.buffer[n]>>>7-t%8&1)===1},put:function(t,n){for(let o=0;o>>n-o-1&1)===1)},getLengthInBits:function(){return this.length},putBit:function(t){const n=Math.floor(this.length/8);this.buffer.length<=n&&this.buffer.push(0),t&&(this.buffer[n]|=128>>>this.length%8),this.length++}},Fe=e,Fe}var Ve,At;function kn(){if(At)return Ve;At=1;function e(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}return e.prototype.set=function(t,n,o,r){const i=t*this.size+n;this.data[i]=o,r&&(this.reservedBit[i]=!0)},e.prototype.get=function(t,n){return this.data[t*this.size+n]},e.prototype.xor=function(t,n,o){this.data[t*this.size+n]^=o},e.prototype.isReserved=function(t,n){return this.reservedBit[t*this.size+n]},Ve=e,Ve}var Ke={},Nt;function An(){return Nt||(Nt=1,(function(e){const t=se().getSymbolSize;e.getRowColCoords=function(o){if(o===1)return[];const r=Math.floor(o/7)+2,i=t(o),a=i===145?26:Math.ceil((i-13)/(2*r-2))*2,l=[i-7];for(let s=1;s=0&&r<=7},e.from=function(r){return e.isValid(r)?parseInt(r,10):void 0},e.getPenaltyN1=function(r){const i=r.size;let a=0,l=0,s=0,d=null,u=null;for(let m=0;m=5&&(a+=t.N1+(l-5)),d=f,l=1),f=r.get(p,m),f===u?s++:(s>=5&&(a+=t.N1+(s-5)),u=f,s=1)}l>=5&&(a+=t.N1+(l-5)),s>=5&&(a+=t.N1+(s-5))}return a},e.getPenaltyN2=function(r){const i=r.size;let a=0;for(let l=0;l=10&&(l===1488||l===93)&&a++,s=s<<1&2047|r.get(u,d),u>=10&&(s===1488||s===93)&&a++}return a*t.N3},e.getPenaltyN4=function(r){let i=0;const a=r.data.length;for(let s=0;s=0;){const a=i[0];for(let s=0;s0){const l=new Uint8Array(this.degree);return l.set(i,a),l}return i},Je=t,Je}var We={},He={},Ye={},Rt;function Qt(){return Rt||(Rt=1,Ye.isValid=function(t){return!isNaN(t)&&t>=1&&t<=40}),Ye}var O={},It;function Zt(){if(It)return O;It=1;const e="[0-9]+",t="[A-Z $%*+\\-./:]+";let n="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";n=n.replace(/u/g,"\\u");const o="(?:(?![A-Z0-9 $%*+\\-./:]|"+n+`)(?:.|[\r +]))+`;O.KANJI=new RegExp(n,"g"),O.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),O.BYTE=new RegExp(o,"g"),O.NUMERIC=new RegExp(e,"g"),O.ALPHANUMERIC=new RegExp(t,"g");const r=new RegExp("^"+n+"$"),i=new RegExp("^"+e+"$"),a=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");return O.testKanji=function(s){return r.test(s)},O.testNumeric=function(s){return i.test(s)},O.testAlphanumeric=function(s){return a.test(s)},O}var Pt;function ce(){return Pt||(Pt=1,(function(e){const t=Qt(),n=Zt();e.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},e.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},e.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},e.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},e.MIXED={bit:-1},e.getCharCountIndicator=function(i,a){if(!i.ccBits)throw new Error("Invalid mode: "+i);if(!t.isValid(a))throw new Error("Invalid version: "+a);return a>=1&&a<10?i.ccBits[0]:a<27?i.ccBits[1]:i.ccBits[2]},e.getBestModeForData=function(i){return n.testNumeric(i)?e.NUMERIC:n.testAlphanumeric(i)?e.ALPHANUMERIC:n.testKanji(i)?e.KANJI:e.BYTE},e.toString=function(i){if(i&&i.id)return i.id;throw new Error("Invalid mode")},e.isValid=function(i){return i&&i.bit&&i.ccBits};function o(r){if(typeof r!="string")throw new Error("Param is not a string");switch(r.toLowerCase()){case"numeric":return e.NUMERIC;case"alphanumeric":return e.ALPHANUMERIC;case"kanji":return e.KANJI;case"byte":return e.BYTE;default:throw new Error("Unknown mode: "+r)}}e.from=function(i,a){if(e.isValid(i))return i;try{return o(i)}catch{return a}}})(He)),He}var Mt;function Bn(){return Mt||(Mt=1,(function(e){const t=se(),n=Gt(),o=lt(),r=ce(),i=Qt(),a=7973,l=t.getBCHDigit(a);function s(p,f,h){for(let w=1;w<=40;w++)if(f<=e.getCapacity(w,h,p))return w}function d(p,f){return r.getCharCountIndicator(p,f)+4}function u(p,f){let h=0;return p.forEach(function(w){const R=d(w.mode,f);h+=R+w.getBitsLength()}),h}function m(p,f){for(let h=1;h<=40;h++)if(u(p,h)<=e.getCapacity(h,f,r.MIXED))return h}e.from=function(f,h){return i.isValid(f)?parseInt(f,10):h},e.getCapacity=function(f,h,w){if(!i.isValid(f))throw new Error("Invalid QR Code version");typeof w>"u"&&(w=r.BYTE);const R=t.getSymbolTotalCodewords(f),C=n.getTotalCodewordsCount(f,h),B=(R-C)*8;if(w===r.MIXED)return B;const E=B-d(w,f);switch(w){case r.NUMERIC:return Math.floor(E/10*3);case r.ALPHANUMERIC:return Math.floor(E/11*2);case r.KANJI:return Math.floor(E/13);case r.BYTE:default:return Math.floor(E/8)}},e.getBestVersionForData=function(f,h){let w;const R=o.from(h,o.M);if(Array.isArray(f)){if(f.length>1)return m(f,R);if(f.length===0)return 1;w=f[0]}else w=f;return s(w.mode,w.getLength(),R)},e.getEncodedBits=function(f){if(!i.isValid(f)||f<7)throw new Error("Invalid QR Code version");let h=f<<12;for(;t.getBCHDigit(h)-l>=0;)h^=a<=0;)s^=t<0&&(i=this.data.substr(r),a=parseInt(i,10),o.put(a,l*3+1))},Ze=t,Ze}var Xe,Ut;function In(){if(Ut)return Xe;Ut=1;const e=ce(),t=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function n(o){this.mode=e.ALPHANUMERIC,this.data=o}return n.getBitsLength=function(r){return 11*Math.floor(r/2)+6*(r%2)},n.prototype.getLength=function(){return this.data.length},n.prototype.getBitsLength=function(){return n.getBitsLength(this.data.length)},n.prototype.write=function(r){let i;for(i=0;i+2<=this.data.length;i+=2){let a=t.indexOf(this.data[i])*45;a+=t.indexOf(this.data[i+1]),r.put(a,11)}this.data.length%2&&r.put(t.indexOf(this.data[i]),6)},Xe=n,Xe}var et,qt;function Pn(){if(qt)return et;qt=1;const e=ce();function t(n){this.mode=e.BYTE,typeof n=="string"?this.data=new TextEncoder().encode(n):this.data=new Uint8Array(n)}return t.getBitsLength=function(o){return o*8},t.prototype.getLength=function(){return this.data.length},t.prototype.getBitsLength=function(){return t.getBitsLength(this.data.length)},t.prototype.write=function(n){for(let o=0,r=this.data.length;o=33088&&i<=40956)i-=33088;else if(i>=57408&&i<=60351)i-=49472;else throw new Error("Invalid SJIS character: "+this.data[r]+` +Make sure your charset is UTF-8`);i=(i>>>8&255)*192+(i&255),o.put(i,13)}},tt=n,tt}var nt={exports:{}},Vt;function $n(){return Vt||(Vt=1,(function(e){var t={single_source_shortest_paths:function(n,o,r){var i={},a={};a[o]=0;var l=t.PriorityQueue.make();l.push(o,0);for(var s,d,u,m,p,f,h,w,R;!l.empty();){s=l.pop(),d=s.value,m=s.cost,p=n[d]||{};for(u in p)p.hasOwnProperty(u)&&(f=p[u],h=m+f,w=a[u],R=typeof a[u]>"u",(R||w>h)&&(a[u]=h,l.push(u,h),i[u]=d))}if(typeof r<"u"&&typeof a[r]>"u"){var C=["Could not find a path from ",o," to ",r,"."].join("");throw new Error(C)}return i},extract_shortest_path_from_predecessor_list:function(n,o){for(var r=[],i=o;i;)r.push(i),n[i],i=n[i];return r.reverse(),r},find_path:function(n,o,r){var i=t.single_source_shortest_paths(n,o,r);return t.extract_shortest_path_from_predecessor_list(i,r)},PriorityQueue:{make:function(n){var o=t.PriorityQueue,r={},i;n=n||{};for(i in o)o.hasOwnProperty(i)&&(r[i]=o[i]);return r.queue=[],r.sorter=n.sorter||o.default_sorter,r},default_sorter:function(n,o){return n.cost-o.cost},push:function(n,o){var r={value:n,cost:o};this.queue.push(r),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};e.exports=t})(nt)),nt.exports}var Kt;function Dn(){return Kt||(Kt=1,(function(e){const t=ce(),n=Rn(),o=In(),r=Pn(),i=Mn(),a=Zt(),l=se(),s=$n();function d(C){return unescape(encodeURIComponent(C)).length}function u(C,B,E){const L=[];let D;for(;(D=C.exec(E))!==null;)L.push({data:D[0],index:D.index,mode:B,length:D[0].length});return L}function m(C){const B=u(a.NUMERIC,t.NUMERIC,C),E=u(a.ALPHANUMERIC,t.ALPHANUMERIC,C);let L,D;return l.isKanjiModeEnabled()?(L=u(a.BYTE,t.BYTE,C),D=u(a.KANJI,t.KANJI,C)):(L=u(a.BYTE_KANJI,t.BYTE,C),D=[]),B.concat(E,L,D).sort(function(N,A){return N.index-A.index}).map(function(N){return{data:N.data,mode:N.mode,length:N.length}})}function p(C,B){switch(B){case t.NUMERIC:return n.getBitsLength(C);case t.ALPHANUMERIC:return o.getBitsLength(C);case t.KANJI:return i.getBitsLength(C);case t.BYTE:return r.getBitsLength(C)}}function f(C){return C.reduce(function(B,E){const L=B.length-1>=0?B[B.length-1]:null;return L&&L.mode===E.mode?(B[B.length-1].data+=E.data,B):(B.push(E),B)},[])}function h(C){const B=[];for(let E=0;E=0&&k<=6&&(_===0||_===6)||_>=0&&_<=6&&(k===0||k===6)||k>=2&&k<=4&&_>=2&&_<=4?y.set(b+k,S+_,!0,!0):y.set(b+k,S+_,!1,!0))}}function h(y){const N=y.size;for(let A=8;A>k&1)===1,y.set(T,b,S,!0),y.set(b,T,S,!0)}function C(y,N,A){const v=y.size,T=u.getEncodedBits(N,A);let b,S;for(b=0;b<15;b++)S=(T>>b&1)===1,b<6?y.set(b,8,S,!0):b<8?y.set(b+1,8,S,!0):y.set(v-15+b,8,S,!0),b<8?y.set(8,v-b-1,S,!0):b<9?y.set(8,15-b-1+1,S,!0):y.set(8,15-b-1,S,!0);y.set(v-8,8,1,!0)}function B(y,N){const A=y.size;let v=-1,T=A-1,b=7,S=0;for(let k=A-1;k>0;k-=2)for(k===6&&k--;;){for(let _=0;_<2;_++)if(!y.isReserved(T,k-_)){let X=!1;S>>b&1)===1),y.set(T,k-_,X),b--,b===-1&&(S++,b=7)}if(T+=v,T<0||A<=T){T-=v,v=-v;break}}}function E(y,N,A){const v=new n;A.forEach(function(_){v.put(_.mode.bit,4),v.put(_.getLength(),m.getCharCountIndicator(_.mode,y)),_.write(v)});const T=e.getSymbolTotalCodewords(y),b=l.getTotalCodewordsCount(y,N),S=(T-b)*8;for(v.getLengthInBits()+4<=S&&v.put(0,4);v.getLengthInBits()%8!==0;)v.putBit(0);const k=(S-v.getLengthInBits())/8;for(let _=0;_=7&&R(_,N),B(_,S),isNaN(v)&&(v=a.getBestMask(_,C.bind(null,_,A))),a.applyMask(v,_),C(_,A,v),{modules:_,version:N,errorCorrectionLevel:A,maskPattern:v,segments:T}}return Ue.create=function(N,A){if(typeof N>"u"||N==="")throw new Error("No input text");let v=t.M,T,b;return typeof A<"u"&&(v=t.from(A.errorCorrectionLevel,t.M),T=d.from(A.version),b=a.from(A.maskPattern),A.toSJISFunc&&e.setToSJISFunction(A.toSJISFunc)),D(N,T,v,b)},Ue}var rt={},ot={},zt;function Xt(){return zt||(zt=1,(function(e){function t(n){if(typeof n=="number"&&(n=n.toString()),typeof n!="string")throw new Error("Color should be defined as hex string");let o=n.slice().replace("#","").split("");if(o.length<3||o.length===5||o.length>8)throw new Error("Invalid hex color: "+n);(o.length===3||o.length===4)&&(o=Array.prototype.concat.apply([],o.map(function(i){return[i,i]}))),o.length===6&&o.push("F","F");const r=parseInt(o.join(""),16);return{r:r>>24&255,g:r>>16&255,b:r>>8&255,a:r&255,hex:"#"+o.slice(0,6).join("")}}e.getOptions=function(o){o||(o={}),o.color||(o.color={});const r=typeof o.margin>"u"||o.margin===null||o.margin<0?4:o.margin,i=o.width&&o.width>=21?o.width:void 0,a=o.scale||4;return{width:i,scale:i?4:a,margin:r,color:{dark:t(o.color.dark||"#000000ff"),light:t(o.color.light||"#ffffffff")},type:o.type,rendererOpts:o.rendererOpts||{}}},e.getScale=function(o,r){return r.width&&r.width>=o+r.margin*2?r.width/(o+r.margin*2):r.scale},e.getImageWidth=function(o,r){const i=e.getScale(o,r);return Math.floor((o+r.margin*2)*i)},e.qrToImageData=function(o,r,i){const a=r.modules.size,l=r.modules.data,s=e.getScale(a,i),d=Math.floor((a+i.margin*2)*s),u=i.margin*s,m=[i.color.light,i.color.dark];for(let p=0;p=u&&f>=u&&p"u"&&(!a||!a.getContext)&&(s=a,a=void 0),a||(d=o()),s=t.getOptions(s);const u=t.getImageWidth(i.modules.size,s),m=d.getContext("2d"),p=m.createImageData(u,u);return t.qrToImageData(p.data,i,s),n(m,d,u),m.putImageData(p,0,0),d},e.renderToDataURL=function(i,a,l){let s=l;typeof s>"u"&&(!a||!a.getContext)&&(s=a,a=void 0),s||(s={});const d=e.render(i,a,s),u=s.type||"image/png",m=s.rendererOpts||{};return d.toDataURL(u,m.quality)}})(rt)),rt}var it={},Jt;function Fn(){if(Jt)return it;Jt=1;const e=Xt();function t(r,i){const a=r.a/255,l=i+'="'+r.hex+'"';return a<1?l+" "+i+'-opacity="'+a.toFixed(2).slice(1)+'"':l}function n(r,i,a){let l=r+i;return typeof a<"u"&&(l+=" "+a),l}function o(r,i,a){let l="",s=0,d=!1,u=0;for(let m=0;m0&&p>0&&r[m-1]||(l+=d?n("M",p+a,.5+f+a):n("m",s,0),s=0,d=!1),p+1':"",f="',h='viewBox="0 0 '+m+" "+m+'"',R=''+p+f+` +`;return typeof l=="function"&&l(null,R),R},it}var Wt;function Vn(){if(Wt)return de;Wt=1;const e=En(),t=Un(),n=qn(),o=Fn();function r(i,a,l,s,d){const u=[].slice.call(arguments,1),m=u.length,p=typeof u[m-1]=="function";if(!p&&!e())throw new Error("Callback required as last argument");if(p){if(m<2)throw new Error("Too few arguments provided");m===2?(d=l,l=a,a=s=void 0):m===3&&(a.getContext&&typeof d>"u"?(d=s,s=void 0):(d=s,s=l,l=a,a=void 0))}else{if(m<1)throw new Error("Too few arguments provided");return m===1?(l=a,a=s=void 0):m===2&&!a.getContext&&(s=l,l=a,a=void 0),new Promise(function(f,h){try{const w=t.create(l,s);f(i(w,a,s))}catch(w){h(w)}})}try{const f=t.create(l,s);d(null,i(f,a,s))}catch(f){d(f)}}return de.create=t.create,de.toCanvas=r.bind(null,n.render),de.toDataURL=r.bind(null,n.renderToDataURL),de.toString=r.bind(null,function(i,a,l){return o.render(i,l)}),de}var Kn=Vn();const jn=Cn(Kn),c=e=>document.getElementById(e),zn=Array.from(document.querySelectorAll(".screen")),M=c("notice"),en=navigator.userAgent.includes("Firefox");let Le="",fe="",ge="",ie="",g,dt,Te="held",K=1,ae=0;const we=8;let x,H=[],Z,P,_e,tn="activity",G,ve="",oe="";const he=20,On={1:"Mining Reward",2:"Transfer",3:"Token Creation",4:"NFT/RWA Creation",5:"Marketing",6:"Swap",7:"Loan",8:"Loan Payment",9:"Collateral Claim",10:"Asset Burn",11:"Token Issuance",12:"Vanity Address",100:"Storage Key",101:"Data Storage Bool",102:"Data Storage U8",103:"Data Storage U16",104:"Data Storage U32",105:"Data Storage U64",106:"Data Storage U128",107:"Data Storage String",108:"Data Storage I8",109:"Data Storage I16",110:"Data Storage I32",111:"Data Storage I64",112:"Data Storage I128",113:"Delete Stored Data",200:"Proposal",201:"Proposal Vote",202:"Activation Vote"};async function $(e,t={}){const n=await chrome.runtime.sendMessage({type:e,...t});if(!(n!=null&&n.ok))throw new Error((n==null?void 0:n.error)??"Wallet request failed.");return n.result}function I(e,t=!0){zn.forEach(n=>{n.hidden=n.id!==e}),t&&(M.textContent="")}function V(e){g=e;const t=e.networkName,n=e.networkSymbol;document.querySelectorAll("[data-network-name]").forEach(o=>{o.textContent=t}),document.querySelectorAll("[data-network-symbol]").forEach(o=>{o.textContent=n}),c("base-asset-name").textContent=`Contractless ${t}`,c("wallet-address").textContent=e.address??"",c("wallet-address-short").textContent=e.address?`${e.address.slice(0,6)}...${e.address.slice(-6)}`:"Account",c("settings-testnet-api-url").value=e.testnetApiUrl,c("settings-mainnet-api-url").value=e.mainnetApiUrl,document.querySelectorAll("[data-select-network]").forEach(o=>{const r=o.dataset.selectNetwork===e.selectedNetwork;o.classList.toggle("active",r),o.setAttribute("aria-pressed",String(r))}),c("settings-lock-timeout").value=String(e.autoLockMinutes),Xn(e),e.exists?e.unlocked?at().then(o=>{o||(I("dashboard-screen"),Be())}):I("locked-screen"):I("create-screen")}function Jn(e,t){return e==="contractless_connect"?"Connect application":e==="contractless_signMessage"?"Sign text message":e==="contractless_sendTransaction"||e==="contractless_signDualTransaction"?`Sign ${String(t.transaction_type??"transaction")}`:"Review request"}function j(e,t,n){const o=document.createElement("dt");o.textContent=t.replaceAll("_"," ");const r=document.createElement("dd");r.textContent=typeof n=="object"?JSON.stringify(n,null,2):String(n),e.append(o,r)}function te(e,t,n,o){const r=re(e[t]),i=String(e[n]??"").trim().toUpperCase(),a=Number(e[o]??0);return`${r} ${i}${a>0?` #${a}`:""}`}function Ht(e,t,n){const o=n===1?2:1,r=(g==null?void 0:g.networkSymbol)??"CLTC";j(e,"Your miner tip",te(t,`tip${n}`,`ticker${n}`,`nft_series${n}`)),j(e,"Your transaction fee",`${re(t[`txfee${n}`])} ${r}`),j(e,"Other party miner tip",te(t,`tip${o}`,`ticker${o}`,`nft_series${o}`)),j(e,"Other party transaction fee",`${re(t[`txfee${o}`])} ${r}`)}function Wn(e,t){var a;const n=((a=g==null?void 0:g.address)==null?void 0:a.trim().toLowerCase())??"",o=String(t.sender1??"").trim().toLowerCase(),r=String(t.sender2??"").trim().toLowerCase(),i=n===o?1:n===r?2:0;if(i===1){j(e,"Sending",te(t,"value1","ticker1","nft_series1")),j(e,"Receiving",te(t,"value2","ticker2","nft_series2")),Ht(e,t,1);return}if(i===2){j(e,"Sending",te(t,"value2","ticker2","nft_series2")),j(e,"Receiving",te(t,"value1","ticker1","nft_series1")),Ht(e,t,2);return}j(e,"First party sending",te(t,"value1","ticker1","nft_series1")),j(e,"Second party sending",te(t,"value2","ticker2","nft_series2"))}function ye(e){const t=c("approve-provider-approval"),n=c("reject-provider-approval"),o=e!==null;t.disabled=o,n.disabled=o,t.classList.toggle("is-processing",e==="approve"),n.classList.toggle("is-processing",e==="reject"),t.textContent=e==="approve"?"Processing...":"Approve",n.textContent=e==="reject"?"Rejecting...":"Reject"}async function at(){const e=await $("pending-provider-approval");if(!(e!=null&&e.request))return!1;const t=e.request;ye(null),oe=t.id;const n=(e.total??0)>1?` · Request ${e.position??1} of ${e.total}`:"";c("provider-approval-origin").textContent=`${t.origin}${n}`;const o=c("provider-approval-risk");o.className=`risk-label risk-${t.risk}`,o.textContent=t.risk==="safe"?"Safe":t.risk==="low"?"Low risk":"High risk";const r=t.display??{},i=t.method==="contractless_sendTransaction"||t.method==="contractless_signDualTransaction";c("provider-approval-title").textContent=Jn(t.method,r),c("provider-approval-intro").textContent=i?"Review every transaction field before signing.":"Review this request before approving it.";const a=c("provider-approval-details");return a.replaceChildren(),Object.entries(r).forEach(([l,s])=>{if(l==="transaction_fields"&&s&&typeof s=="object"){const d=s,u=Number(d.txtype??0),m=new Set(["ticker1","nft_series1","value1","ticker2","nft_series2","value2","tip1","tip2","txfee1","txfee2"]);u===6&&Wn(a,d),Object.entries(d).forEach(([p,f])=>{u===6&&m.has(p)||j(a,p==="txtype"?"transaction_type_id":p,dn(u,p,f))});return}j(a,l,s)}),I("provider-approval-screen"),!0}async function nn(e,t){if(!(e>0&&await at())){if(t){const n=Date.now()+1500;for(;Date.now()setTimeout(o,75))}}window.close()}}function Hn(e){return e.replace(/[.0]/g,"").length>0}function st(){const e=c("token-list");if(e.replaceChildren(),!x){const s=document.createElement("div");s.className="empty-assets";const d=document.createElement("strong");d.textContent="Loading token balances...";const u=document.createElement("span");u.textContent="Balances are retrieved from the selected Contractless API.",s.append(d,u),e.append(s);return}const t=(g==null?void 0:g.networkSymbol)??"CLTC",n=new Set(x.tokens.map(s=>s.token.trim().toUpperCase())),o=new Set(x.nfts.map(s=>`${s.name.trim().toUpperCase()}|${Number(s.series||0)}`)),r=x.balances.filter(s=>{const d=s.asset.trim().toUpperCase(),u=`${d}|${Number(s.nft_series||0)}`;return s.nft_series===0&&d!=="CLTC"&&d!=="CLC"&&n.has(d)&&!o.has(u)}),i=new Map(r.map(s=>[s.asset.trim().toUpperCase(),s])),a=Te==="all"?x.tokens.map(s=>s.token):r.filter(s=>Hn(s.balance)).map(s=>s.asset),l=[...new Set(a.map(s=>s.trim().toUpperCase()))].filter(s=>s!==t&&!o.has(`${s}|0`)).sort((s,d)=>s.localeCompare(d));if(l.length===0){const s=document.createElement("div");s.className="empty-assets";const d=document.createElement("strong");d.textContent=Te==="held"?"No token balances found":"No tokens found";const u=document.createElement("span");u.textContent=Te==="held"?"Switch to All tokens to view the complete network token list.":x.tokenCatalogError?`Token catalog unavailable: ${x.tokenCatalogError}`:"The selected API did not return any fungible tokens.",s.append(d,u),e.append(s);return}l.forEach(s=>{const d=i.get(s),u=document.createElement("div");u.className="asset-row";const m=document.createElement("div");m.className="asset-symbol token-symbol",m.textContent=s.slice(0,3);const p=document.createElement("div");p.className="asset-name";const f=document.createElement("strong");f.textContent=s;const h=document.createElement("span");h.textContent="Contractless token",p.append(f,h);const w=document.createElement("div");w.className="asset-value";const R=document.createElement("strong");R.textContent=(d==null?void 0:d.balance)??"0.00000000";const C=document.createElement("span");C.textContent=s,w.append(R,C),u.append(m,p,w),e.append(u)})}async function Be(){if(g!=null&&g.unlocked){x=void 0,c("base-balance").textContent="--",c("base-asset-balance").textContent="--",st();try{if(!g.apiUrl)throw new Error(`No ${g.networkName} API is configured.`);const e=await $("wallet-balances");x={balances:Array.isArray(e.balances)?e.balances:[],tokens:Array.isArray(e.tokens)?e.tokens:[],nfts:Array.isArray(e.nfts)?e.nfts:[],tokenCatalogError:e.tokenCatalogError,nftCatalogError:e.nftCatalogError};const t=g.networkSymbol,n=x.balances.find(r=>r.nft_series===0&&r.asset.toUpperCase()===t),o=(n==null?void 0:n.balance)??"0.00000000";c("base-balance").textContent=o,c("base-asset-balance").textContent=o,st()}catch(e){const t=c("token-list");t.replaceChildren();const n=document.createElement("div");n.className="empty-assets balance-error";const o=document.createElement("strong");o.textContent="Unable to load balances";const r=document.createElement("span");r.textContent=F(e),n.append(o,r),t.append(n),M.textContent=F(e)}}}function Ne(e,t){return`${e.trim().toUpperCase()}|${t}`}function Yn(e){try{return Y(e,"Balance")>0n}catch{return!1}}function rn(e){if(e.ownership_type!==1)return"Complete ownership";try{const t=Y(e.balance,"Balance"),n=Y(e.supply,"Supply");if(n<=0n)return"0% ownership";const o=t*100000000n/n,r=o/1000000n,i=(o%1000000n).toString().padStart(6,"0").replace(/0+$/,"");return`${r}${i?`.${i}`:""}% ownership`}catch{return"Fractional ownership"}}function ut(e){var t;return(t=e.details)!=null&&t.metadata&&typeof e.details.metadata=="object"?e.details.metadata:{}}function ft(e){const t=ut(e).name;return typeof t=="string"&&t.trim()?t.trim():e.series>0?`${e.name} #${e.series}`:e.name}function on(e){const n=ut(e).contractless;return n&&typeof n=="object"&&String(n.metadata_type??"").toLowerCase()==="rwa"?"RWA":"NFT"}function be(e){return e.series>0?`${e.name}_${e.series}`:e.name}function Gn(e,t=7,n=7){return e.length>t+n+3?`${e.slice(0,t)}...${e.slice(-n)}`:e}function Qn(e,t){var r;const n=document.createElement("div");if(n.className=t,(r=e.details)!=null&&r.image_data_url){const i=document.createElement("img");return i.src=e.details.image_data_url,i.alt=ft(e),n.append(i),n}const o=document.createElement("span");return o.textContent=e.loading?"...":e.name.slice(0,2).toUpperCase(),n.append(o),n}function an(){const e=(K-1)*we;return H.slice(e,e+we)}function Ce(){const e=c("nfts-list");e.replaceChildren();const t=Math.max(1,Math.ceil(H.length/we));if(K=Math.min(Math.max(1,K),t),c("nfts-count").textContent=H.length===1?"1 owned asset":`${H.length} owned assets`,c("nfts-page").textContent=String(K),c("nfts-pages").textContent=String(t),c("nfts-pagination").hidden=t<=1,c("nfts-previous").disabled=K<=1,c("nfts-next").disabled=K>=t,!H.length){const n=document.createElement("div");n.className="nfts-empty";const o=document.createElement("strong");o.textContent="No NFTs or RWAs found";const r=document.createElement("span");r.textContent=x!=null&&x.nftCatalogError?`NFT catalog unavailable: ${x.nftCatalogError}`:"Assets owned by this wallet will appear here.",n.append(o,r),e.append(n);return}an().forEach(n=>{const o=document.createElement("button");o.type="button",o.className="nft-card",o.append(Qn(n,"nft-card-media"));const r=document.createElement("div");r.className="nft-card-identity";const i=document.createElement("span");i.textContent=on(n);const a=document.createElement("strong");a.textContent=ft(n);const l=document.createElement("small");l.textContent=be(n);const s=document.createElement("b");if(s.textContent=rn(n),r.append(i,a,l,s),n.error){const d=document.createElement("em");d.textContent="Media unavailable",r.append(d)}o.append(r),o.addEventListener("click",()=>Zn(n)),e.append(o)})}function Se(e,t,n){const o=document.createElement("div"),r=document.createElement("dt");r.textContent=t;const i=document.createElement("dd");i.textContent=n||"--",o.append(r,i),e.append(o)}function xe(e){Z=e;const t=e.details,n=ut(e),o=on(e),r=ft(e);c("nft-detail-title").textContent=r,c("nft-detail-kind").textContent=`${o} details`,c("nft-detail-badge").textContent=o,c("nft-detail-name").textContent=r,c("nft-detail-ownership").textContent=rn(e);const i=c("nft-detail-image");i.src=(t==null?void 0:t.image_data_url)??"",i.alt=r,i.hidden=!(t!=null&&t.image_data_url);const a=n.description??n.summary;c("nft-detail-description").textContent=typeof a=="string"&&a.trim()?a.trim():t?"No description was included in the NFT metadata.":"Loading NFT metadata and provenance...";const l=c("nft-detail-attributes");l.replaceChildren(),(Array.isArray(n.attributes)?n.attributes.slice(0,12):[]).forEach(m=>{if(!m||typeof m!="object")return;const p=m,f=document.createElement("div"),h=document.createElement("span");h.textContent=String(p.trait_type??p.type??"Attribute");const w=document.createElement("strong");w.textContent=String(p.value??""),f.append(h,w),l.append(f)}),l.hidden=l.childElementCount===0;const d=c("nft-detail-fields");d.replaceChildren(),Se(d,"Asset",be(e)),Se(d,"Creator",(t==null?void 0:t.creator)??"--"),Se(d,"IPFS CID",(t==null?void 0:t.item_ipfs)??"--"),Se(d,"Origin transaction",(t==null?void 0:t.genesis_txid)??e.origin_txid);const u=c("nft-detail-history");if(u.replaceChildren(),t)if(t.history.length)t.history.slice().reverse().forEach(m=>{const p=document.createElement("button");p.type="button",p.className="nft-history-row",p.title=`View full transaction ${m.txid}`;const f=document.createElement("strong");f.textContent=m.action;const h=document.createElement("span");h.textContent=`Block ${Number(m.block).toLocaleString()}`;const w=document.createElement("small");w.textContent=Gn(m.txid),p.append(f,h,w),p.addEventListener("click",()=>{fn({txid:m.txid,block_height:Number(m.block),transaction_hex:""},"nft")}),u.append(p)});else{const m=document.createElement("p");m.textContent="No provenance records were returned.",u.append(m)}else{const m=document.createElement("p");m.textContent="Loading provenance...",u.append(m)}}function Zn(e){xe(e),I("nft-detail-screen")}async function mt(e){const t=an().filter(r=>!r.details&&!r.loading);let n=0;const o=async()=>{for(;n[Ne(r.name,r.series),r]));H=x.nfts.map(r=>{const i=x==null?void 0:x.balances.find(l=>Ne(l.asset,Number(l.nft_series||0))===Ne(r.name,Number(r.series||0)));if(!i||!Yn(i.balance))return null;const a=n.get(Ne(r.name,r.series));return{name:r.name.trim(),series:Number(r.series||0),origin_txid:r.origin_txid,ownership_type:Number(r.ownership_type||0),supply:r.supply,balance:i.balance,details:a==null?void 0:a.details,error:a==null?void 0:a.error}}).filter(r=>r!==null).sort((r,i)=>r.name.localeCompare(i.name)||r.series-i.series);const o=Math.max(1,Math.ceil(H.length/we));K=Math.min(K,o),Ce(),await mt(t)}function Xn(e){const t=c("known-addresses");t.replaceChildren(),e.wallets.forEach(n=>{const o=document.createElement("div");o.className=`known-address${n.active?" active-address":""}`;const r=document.createElement("button");r.type="button",r.className="address-select",r.dataset.walletId=n.id;const i=document.createElement("span");i.className="address-avatar";const a=document.createElement("img");a.src=chrome.runtime.getURL("contractless-logo.png"),a.alt="",i.append(a);const l=document.createElement("span");l.className="address-details";const s=document.createElement("strong");s.textContent=n.label;const d=document.createElement("small");d.textContent=n.address,l.append(s,d);const u=document.createElement("span");u.className="selected-mark",u.textContent=n.active?"✓":"",r.append(i,l,u),r.addEventListener("click",()=>{if(n.active){z(!1);return}U(async()=>{z(!1),fe="",ie="",V(await $("switch-wallet",{walletId:n.id}))})});const m=document.createElement("button");m.type="button",m.className="rename-address",m.title="Rename wallet",m.setAttribute("aria-label",`Rename ${n.label}`),m.textContent="✎",m.addEventListener("click",()=>{const p=window.prompt("Wallet label",n.label);p===null||p.trim()===n.label||U(async()=>{const f=await $("rename-wallet",{walletId:n.id,label:p});V(f),z(!0)})}),o.append(r,m),t.append(o)})}function z(e){const t=c("account-drawer"),n=c("drawer-backdrop"),o=c("account-menu-button");t.classList.toggle("open",e),t.setAttribute("aria-hidden",String(!e)),o.setAttribute("aria-expanded",String(e)),n.hidden=!e}function gt(){c("revealed-private-key").value="",c("private-key-result").hidden=!0}function Ee(e){if(c("backup-key-modal").hidden=!e,c("backup-key-backdrop").hidden=!e,e){const t=c("backup-key-input");t.value="",t.focus()}else c("backup-key-input").value="",dt=void 0}async function er(){const e=await $("wallet-receive-address"),t=String(e.receiveAddress??"");if(!t)throw new Error("No active wallet address is available.");c("receive-address-label").textContent=e.vanity?"Vanity address":"Wallet address",c("receive-address-value").textContent=t;const n=c("receive-qr");await jn.toCanvas(n,t,{width:220,margin:1,errorCorrectionLevel:"M",color:{dark:"#071016",light:"#ffffff"}}),I("receive-screen")}function Y(e,t){const n=e.trim();if(!/^(?:0|[1-9]\d*)(?:\.\d{1,8})?$/.test(n))throw new Error(`${t} must be a positive number with no more than 8 decimal places.`);const[o,r=""]=n.split(".");return BigInt(o)*100000000n+BigInt(r.padEnd(8,"0"))}function ne(e){const t=e/100000000n,n=(e%100000000n).toString().padStart(8,"0");return`${t}.${n}`}function sn(){if(P)return P.balance;const e=c("send-asset").value,t=x==null?void 0:x.balances.find(n=>n.nft_series===0&&n.asset.trim().toUpperCase()===e);return(t==null?void 0:t.balance)??"0.00000000"}function cn(){const e=c("send-asset").value,t=(g==null?void 0:g.networkSymbol)??"CLTC";if(e!==t)return 100000000n;const n=c("send-amount").value.trim();if(!n)return 0n;try{return(Y(n,"Amount")+99n)/100n}catch{return 0n}}function Re(){const e=(P==null?void 0:P.name)??c("send-asset").value,t=(g==null?void 0:g.networkSymbol)??"CLTC",n=P?be(P):e;c("send-amount-symbol").textContent=n,c("send-available").textContent=`Available: ${sn()} ${n}`;const o=cn();c("send-fee").value=ne(o),c("send-fee-note").textContent=!P&&e===t?"Minimum fee: 1% of the transfer amount.":`Minimum fee: 1 ${t} for asset transfers.`}function tr(){const e=c("send-asset");e.replaceChildren();const t=(g==null?void 0:g.networkSymbol)??"CLTC",n=new Set([t]);x==null||x.tokens.forEach(o=>{const r=o.token.trim().toUpperCase();(x==null?void 0:x.balances.find(a=>a.nft_series===0&&a.asset.trim().toUpperCase()===r&&Y(a.balance,"Balance")>0n))&&n.add(r)}),n.forEach(o=>{const r=document.createElement("option");r.value=o,r.textContent=o,e.append(r)}),Re()}function nr(){if(!x)throw new Error("Wallet balances are still loading.");P=void 0,G=void 0,c("send-form").reset(),c("send-asset").disabled=!1,c("send-amount").disabled=!1,tr(),I("send-screen")}function rr(e){if(!x)throw new Error("Wallet balances are still loading.");P=e,G=void 0,c("send-form").reset();const t=c("send-asset");t.replaceChildren();const n=document.createElement("option");n.value=e.name,n.textContent=be(e),t.append(n),t.disabled=!0;const o=c("send-amount");e.ownership_type===0?(o.value="1.00000000",o.disabled=!0):(o.value="",o.disabled=!1),Re(),I("send-screen")}async function or(){var w;if(!(g!=null&&g.address))throw new Error("No active wallet is available.");const e=(P==null?void 0:P.name)??c("send-asset").value,t=P?be(P):e,n=Q(c("send-recipient").value,"Enter a receiving address."),o=Q(c("send-amount").value,"Enter an amount to send."),r=Q(c("send-fee").value,"Enter a transaction fee."),i=Y(o,"Amount"),a=Y(r,"Transaction fee");if(i===0n)throw new Error("The transfer amount must be greater than zero.");if((P==null?void 0:P.ownership_type)===0&&i!==100000000n)throw new Error("An indivisible NFT must be transferred as one complete asset.");if((P==null?void 0:P.ownership_type)===1&&i>100000000n)throw new Error("A fractional NFT/RWA transfer cannot exceed one complete asset.");const l=cn();if(as)throw new Error(`Insufficient ${e} balance.`);const d=g.networkSymbol,u=((w=x==null?void 0:x.balances.find(R=>R.nft_series===0&&R.asset.trim().toUpperCase()===d))==null?void 0:w.balance)??"0.00000000",m=Y(u,"Base balance");if((e===d?i+a:a)>m)throw new Error(`Insufficient ${d} balance including the fee.`);const f=await $("wallet-prepare-transfer",{receiver:n,asset:e,nftSeries:(P==null?void 0:P.series)??0,valueAtomic:i.toString(),feeAtomic:a.toString()}),h=e===d?`${ne(i+a)} ${d}`:`${ne(i)} ${t} + ${ne(a)} ${d}`;G={transaction:f.transaction,receiverInput:f.receiverInput,asset:e,amount:ne(i),fee:ne(a),total:h},c("review-amount").textContent=G.amount,c("review-asset").textContent=t,c("review-sender").textContent=g.address,c("review-recipient").textContent=f.receiverInput,c("review-resolved-recipient").textContent=String(f.review.receiver??""),c("review-fee").textContent=G.fee,c("review-total").textContent=h,I("send-review-screen")}function ln(e){if(!/^[0-9a-f]+$/i.test(e)||e.length<10)return;const t=e.slice(2,10).match(/../g);if(!t||t.length!==4)return;const n=t.reduce((r,i,a)=>r+Number.parseInt(i,16)*2**(a*8),0),o=new Date(n*1e3);return Number.isNaN(o.getTime())?void 0:o}function ir(e){return e.split("_").map(t=>t&&`${t[0].toUpperCase()}${t.slice(1)}`).join(" ")}function re(e){try{const t=ne(BigInt(String(e??"0"))),[n,o]=t.split("."),r=o.replace(/0+$/,"");return r?`${n}.${r}`:`${n}.0`}catch{return String(e??"")}}function ar(e,t){var o;return t.startsWith("txfee")?!0:((o={1:new Set(["value"]),2:new Set(["value"]),3:new Set(["number"]),6:new Set(["value1","value2","tip1","tip2"]),7:new Set(["loan_amount","collateral_amount","payment_amount","max_late_value"]),8:new Set(["payback_amount","tip"]),10:new Set(["value"]),11:new Set(["number"])}[e])==null?void 0:o.has(t))??!1}function dn(e,t,n){if(typeof n=="boolean")return n?"True":"False";if(ar(e,t))return re(n);if(t==="time"||t==="timestamp"||t==="offer_expiration"){const o=Number(n);if(Number.isFinite(o))return`${new Date(o*1e3).toLocaleString()} (${o})`}return t==="payment_period"?{d:"Daily",w:"Weekly",m:"Monthly"}[String(n).trim().toLowerCase()]??String(n??""):e===3&&t==="hard_limit"?Number(n)===1?"Yes":"No":e===4&&t==="series"?Number(n)===1?"Series":"Single NFT/RWA":e===4&&t==="ownership_type"?Number(n)===1?"Fractional ownership":"Complete ownership":(e===201||e===202)&&t==="vote"?Number(n)===1?"Approve":"Reject":(t==="previous"||t.endsWith("_hash"))&&/^0+$/.test(String(n))?"None":e===5&&(t==="impression_value"||t==="click_value")?(Number(n)/100).toFixed(2):typeof n=="string"?n.trim():String(n??"")}function q(e,t,n,o=!1,r=0){const i=document.createElement("div");i.className=`transaction-field${o?" technical-field":""}`;const a=document.createElement("span");a.textContent=ir(t);const l=document.createElement("strong");l.textContent=o?String(n??""):dn(r,t,n),i.append(a,l),e.append(i)}function ht(e,t){return!e||!t||t=he?`${n} confirmations`:`${n}/${he} confirmations`:`Pending · 0/${he} confirmations`}async function fn(e,t="activity"){_e=e,tn=t;const n=c("close-transaction");n.title=t==="nft"?"Back to NFT/RWA details":"Back to activity",n.setAttribute("aria-label",t==="nft"?"Back to NFT/RWA details":"Back to activity"),I("transaction-screen"),c("transaction-title").textContent="Loading transaction",c("transaction-header-status").textContent="",c("transaction-full-txid").textContent=e.txid,c("transaction-overview").replaceChildren(),c("transaction-fields").replaceChildren(),c("transaction-technical-fields").replaceChildren(),c("transaction-miner-earnings").replaceChildren(),c("miner-earnings-section").hidden=!0;try{const o=await $("wallet-transaction",{txid:e.txid}),r=await Yt((g==null?void 0:g.selectedNetwork)??"testnet"),i=JSON.parse(r.decode_transaction(o.transaction_hex)),l=ht(o.block_height,o.current_height){q(u,f,h,!1,i.txtype)});const m=e.miner_earnings??[];if(m.length>0){const f=c("transaction-miner-earnings");m.forEach((h,w)=>{q(f,`earning_${w+1}_type`,h.type),q(f,`earning_${w+1}_asset`,h.asset),q(f,`earning_${w+1}_nft_series`,h.nft_series),q(f,`earning_${w+1}_amount`,`${re(h.amount_atomic)} ${h.asset.trim()}`)}),c("miner-earnings-section").hidden=!1}const p=c("transaction-technical-fields");i.stored_hash&&q(p,"stored_hash",i.stored_hash,!0),i.signature&&q(p,"signature",i.signature,!0),i.signature1&&q(p,"signature_1",i.signature1,!0),i.signature2&&q(p,"signature_2",i.signature2,!0),q(p,"raw_transaction_bytes",o.transaction_hex,!0)}catch(o){c("transaction-title").textContent="Transaction unavailable";const r=c("transaction-fields");r.replaceChildren(),q(r,"error",F(o))}}function ue(e){const t=String(e??"").trim();return t.length>18?`${t.slice(0,8)}...${t.slice(-7)}`:t}function sr(e,t){const n=e.fields,o=t.trim().toLowerCase(),r=(g==null?void 0:g.networkSymbol)??"CLTC",i=s=>String(n[s]??"").trim(),a=(s,d,u="")=>`${u}${re(n[s])} ${d}`,l=()=>`-${re(n.txfee??0)} ${r}`;switch(e.txtype){case 1:return{title:"Mining reward",subtitle:"From network",amount:a("value",r,"+"),direction:"incoming"};case 2:{const s=i("coin"),d=String(n.sender??"").trim().toLowerCase()===o;return{title:d?`Sent ${s}`:`Received ${s}`,subtitle:d?`To: ${ue(n.receiver)}`:`From: ${ue(n.sender)}`,amount:a("value",s,d?"-":"+"),direction:d?"outgoing":"incoming"}}case 3:{const s=i("ticker");return{title:`Created ${s}`,subtitle:"Token creation",amount:a("number",s,"+"),secondary:l(),direction:"incoming"}}case 4:{const s=String(n.count??"1");return{title:`Created ${i("nft_name")}`,subtitle:Number(n.series??0)===0?"NFT/RWA":"NFT/RWA collection",amount:`+${s} ${Number(s)===1?"item":"items"}`,secondary:l(),direction:"incoming"}}case 6:{const s=String(n.sender1??"").trim().toLowerCase()===o,d=i(s?"ticker1":"ticker2"),u=i(s?"ticker2":"ticker1");return{title:"Completed swap",subtitle:`With: ${ue(n[s?"sender2":"sender1"])}`,amount:a(s?"value1":"value2",d,"-"),secondary:a(s?"value2":"value1",u,"+"),direction:"neutral"}}case 7:{const s=String(n.lender??"").trim().toLowerCase()===o;return{title:s?"Loan funded":"Loan received",subtitle:s?`To: ${ue(n.borrower)}`:`From: ${ue(n.lender)}`,amount:a("loan_amount",i("loan_coin"),s?"-":"+"),direction:s?"outgoing":"incoming"}}case 8:return{title:"Loan payment",subtitle:`Contract: ${ue(n.contract_hash)}`,amount:`-${re(n.payback_amount)}`,direction:"outgoing"};case 10:return{title:`Burned ${i("coin")}`,subtitle:"Removed from supply",amount:a("value",i("coin"),"-"),direction:"outgoing"};case 11:{const s=i("ticker");return{title:`Issued ${s}`,subtitle:"Additional token supply",amount:a("number",s,"+"),secondary:l(),direction:"incoming"}}case 12:return{title:"Created vanity address",subtitle:String(n.vanity_address??"").trim(),amount:l(),direction:"outgoing"};default:return{title:e.transaction_name,subtitle:"Contractless transaction",amount:l(),direction:"outgoing"}}}async function cr(e,t){const n=c("activity-list");if(n.replaceChildren(),c("activity-count").textContent=`${e.length} shown`,e.length===0){const r=document.createElement("div");r.className="activity-empty";const i=document.createElement("strong");i.textContent="No wallet activity found";const a=document.createElement("span");a.textContent="New transactions will appear here after they reach the selected API.",r.append(i,a),n.append(r);return}const o=await Yt((g==null?void 0:g.selectedNetwork)??"testnet");e.forEach(r=>{let i;try{i=JSON.parse(o.decode_transaction(r.transaction_hex))}catch{const E=Number.parseInt(r.transaction_hex.slice(0,2),16);i={txtype:E,transaction_name:On[E]??`Transaction Type ${E}`,fields:{}}}const a=sr(i,(g==null?void 0:g.address)??""),l=ln(r.transaction_hex),d=ht(r.block_height,t){fn(r)}),n.append(u)})}async function mn(){if(!(g!=null&&g.unlocked))return;c("activity-count").textContent="Loading";const e=c("activity-list");e.replaceChildren();const t=document.createElement("div");t.className="activity-empty",t.textContent="Loading recent activity...",e.append(t);try{const n=await $("wallet-activity");await cr(Array.isArray(n.transactions)?n.transactions:[],Number(n.current_height??0))}catch(n){e.replaceChildren();const o=document.createElement("div");o.className="activity-empty activity-error",o.textContent=F(n),e.append(o),c("activity-count").textContent="Unavailable"}}function F(e){return e instanceof Error?e.message:String(e).replace(/^JsValue\((.*)\)$/,"$1")}async function U(e,t=!1){M.textContent="",t&&I("loading");try{await e()}catch(n){throw M.textContent=F(n),n}}function Q(e,t){if(!e.trim())throw new Error(t);return e}function lr(e){let t="";for(let o=0;oURL.revokeObjectURL(r),1e3)}async function gn(e,t){g=e,fe=t,ie=String(await $("export-private-key-image",{password:t,imagePassword:t})),c("wallet-image-preview").src=`data:image/png;base64,${ie}`;const n=c("backup-confirmed");n.disabled=!0,n.checked=!1,c("open-wallet").disabled=!0,I("backup-screen")}document.querySelectorAll(".navigation").forEach(e=>{e.addEventListener("click",()=>{const t=String(e.dataset.screen);if(t==="image-screen"&&en){chrome.tabs.create({url:chrome.runtime.getURL("import-wallet.html"),active:!0});return}I(t)})});c("account-menu-button").addEventListener("click",()=>z(!0));c("close-account-drawer").addEventListener("click",()=>z(!1));c("drawer-backdrop").addEventListener("click",()=>z(!1));function hn(e){z(!1),c("cancel-wallet-add").hidden=!1,I(e)}c("menu-create-wallet").addEventListener("click",()=>{hn("create-screen")});c("menu-import-wallet").addEventListener("click",()=>{z(!1),en?chrome.tabs.create({url:chrome.runtime.getURL("import-wallet.html"),active:!0}):hn("image-screen")});c("cancel-wallet-add").addEventListener("click",()=>{c("cancel-wallet-add").hidden=!0,g&&V(g)});c("menu-settings").addEventListener("click",()=>{z(!1),gt(),g&&(c("settings-testnet-api-url").value=g.testnetApiUrl,c("settings-mainnet-api-url").value=g.mainnetApiUrl,c("settings-lock-timeout").value=String(g.autoLockMinutes)),I("settings-screen")});c("close-settings").addEventListener("click",()=>{gt(),g&&V(g)});c("drawer-lock-wallet").addEventListener("click",()=>{U(async()=>{z(!1),fe="",ie="",V(await $("lock"))})});c("open-receive").addEventListener("click",()=>{U(er)});c("open-send").addEventListener("click",()=>{U(async()=>nr())});c("close-send").addEventListener("click",()=>{G=void 0,P?(xe(P),I("nft-detail-screen")):g&&V(g)});c("send-asset").addEventListener("change",Re);c("send-amount").addEventListener("input",Re);c("send-form").addEventListener("submit",e=>{e.preventDefault(),U(or)});c("send-form").addEventListener("keydown",e=>{!(e instanceof KeyboardEvent)||e.key!=="Enter"||e.target instanceof HTMLInputElement&&(e.preventDefault(),c("send-form").requestSubmit())});c("edit-send").addEventListener("click",()=>{I("send-screen")});c("sign-broadcast-transfer").addEventListener("click",()=>{U(async()=>{if(!G)throw new Error("The reviewed transfer is unavailable.");ve=(await $("wallet-broadcast-transfer",{transaction:G.transaction})).txid,c("send-result-txid").textContent=ve,G=void 0,I("send-success-screen")})});c("copy-send-txid").addEventListener("click",()=>{ve&&navigator.clipboard.writeText(ve).then(()=>{M.textContent="Transaction ID copied."}).catch(e=>{M.textContent=F(e)})});c("finish-send").addEventListener("click",()=>{ve="";const e=!!P;P=void 0,g&&(Be(),e?(Z=void 0,I("nfts-screen"),pt(!0)):V(g))});c("open-activity").addEventListener("click",()=>{I("activity-screen"),mn()});c("open-nfts").addEventListener("click",()=>{K=1,Z=void 0,I("nfts-screen"),pt(!0)});c("close-nfts").addEventListener("click",()=>{ae+=1,g&&V(g)});c("refresh-nfts").addEventListener("click",()=>{pt(!0)});c("close-nft-detail").addEventListener("click",()=>{Z=void 0,I("nfts-screen")});c("transfer-nft").addEventListener("click",()=>{U(async()=>{if(!Z)throw new Error("No NFT or RWA is selected.");rr(Z)})});c("nfts-previous").addEventListener("click",()=>{if(K<=1)return;K-=1;const e=++ae;Ce(),mt(e)});c("nfts-next").addEventListener("click",()=>{const e=Math.max(1,Math.ceil(H.length/we));if(K>=e)return;K+=1;const t=++ae;Ce(),mt(t)});c("close-activity").addEventListener("click",()=>{g&&V(g)});c("close-transaction").addEventListener("click",()=>{tn==="nft"&&Z?(xe(Z),I("nft-detail-screen")):I("activity-screen")});c("copy-transaction-txid").addEventListener("click",()=>{const e=_e==null?void 0:_e.txid;e&&navigator.clipboard.writeText(e).then(()=>{M.textContent="Transaction ID copied."}).catch(t=>{M.textContent=F(t)})});c("refresh-activity").addEventListener("click",()=>{mn()});c("refresh-balances").addEventListener("click",()=>{Be()});document.querySelectorAll("[data-token-filter]").forEach(e=>{e.addEventListener("click",()=>{Te=e.dataset.tokenFilter==="all"?"all":"held",document.querySelectorAll("[data-token-filter]").forEach(t=>{t.classList.toggle("active",t===e)}),st()})});c("close-receive").addEventListener("click",()=>{g&&V(g)});c("copy-receive-address").addEventListener("click",()=>{var t;const e=(t=c("receive-address-value").textContent)==null?void 0:t.trim();if(!e){M.textContent="No active wallet address is available.";return}navigator.clipboard.writeText(e).then(()=>{M.textContent="Wallet address copied."}).catch(n=>{M.textContent=F(n)})});c("save-wallet-settings").addEventListener("click",()=>{U(async()=>{const e=c("settings-testnet-api-url").value.replace(/\/+$/,""),t=c("settings-mainnet-api-url").value.replace(/\/+$/,"");if(!e)throw new Error("A testnet API URL is required.");for(const n of[e,t].filter(Boolean)){const o=new URL(n);if(o.protocol!=="https:"&&o.hostname!=="localhost"&&o.hostname!=="127.0.0.1")throw new Error("Contractless API URLs must use HTTPS.");const r=`${o.origin}/*`;if(!await chrome.permissions.contains({origins:[r]})&&!await chrome.permissions.request({origins:[r]}))throw new Error("Permission to contact this API endpoint was denied.")}g=await $("save-settings",{testnetApiUrl:e,mainnetApiUrl:t,autoLockMinutes:c("settings-lock-timeout").value}),M.textContent="Settings saved."})});document.querySelectorAll("[data-select-network]").forEach(e=>{e.addEventListener("click",()=>{const t=e.dataset.selectNetwork;!t||t===(g==null?void 0:g.selectedNetwork)||U(async()=>{z(!1),V(await $("switch-network",{network:t}))})})});document.querySelectorAll("[data-backup-action]").forEach(e=>{e.addEventListener("click",()=>{dt=e.dataset.backupAction,Ee(!0)})});c("close-backup-key").addEventListener("click",()=>Ee(!1));c("backup-key-backdrop").addEventListener("click",()=>Ee(!1));c("confirm-backup-key").addEventListener("click",()=>{const e=dt,t=c("backup-key-input").value;if(e){if(!t){M.textContent="Enter the active wallet encryption key.";return}U(async()=>{var r;if(e==="image"){const i=String(await $("export-private-key-image",{password:t,imagePassword:t})),a=((r=g==null?void 0:g.address)==null?void 0:r.split(".")[0])??"contractless";pn(i,`${a}.wallet.png`),Ee(!1),M.textContent="Wallet image downloaded.";return}const n=await $("reveal-private-key",{password:t});if(Ee(!1),e==="private"){c("revealed-private-key").value=n.privateKey,c("private-key-result").hidden=!1,c("private-key-result").scrollIntoView({block:"nearest"});return}const o=crypto.randomUUID();await chrome.storage.session.set({[`print-backup:${o}`]:{label:n.label,address:n.address,privateKey:n.privateKey,encryptionKey:t}}),await chrome.windows.create({url:chrome.runtime.getURL(`print.html?id=${encodeURIComponent(o)}`),type:"popup",width:820,height:760})})}});c("clear-private-key").addEventListener("click",gt);c("copy-private-key").addEventListener("click",()=>{const e=c("revealed-private-key").value;e&&navigator.clipboard.writeText(e).then(()=>{M.textContent="Private key copied."}).catch(t=>{M.textContent=F(t)})});document.querySelectorAll(".reveal").forEach(e=>{e.addEventListener("click",()=>{const t=c(String(e.dataset.input)),n=t.type==="text";t.type=n?"password":"text",e.textContent=n?"Show":"Hide"})});c("continue-create").addEventListener("click",()=>{try{Le=Q(c("new-password").value,"Enter an encryption key."),c("verify-password").value="",I("verify-screen")}catch(e){M.textContent=F(e)}});c("create-wallet").addEventListener("click",()=>{if(c("verify-password").value!==Le){M.textContent="The encryption keys do not match.";return}U(async()=>{const t=Le,n=await $("create-wallet",{password:t});Le="",c("new-password").value="",c("verify-password").value="",await gn(n,t)},!0).catch(()=>I("verify-screen",!1))});const ct=c("wallet-image");ct.addEventListener("change",()=>{var t;const e=(t=ct.files)==null?void 0:t[0];ge="",c("image-file-name").textContent=(e==null?void 0:e.name)??"Choose wallet image",e&&e.arrayBuffer().then(n=>{ge=lr(new Uint8Array(n))}).catch(n=>{M.textContent=F(n)})});c("import-image-form").addEventListener("submit",e=>{e.preventDefault();const t=c("image-password").value;try{Q(t,"Enter the wallet image encryption key."),Q(ge,"Choose a wallet image.")}catch(n){M.textContent=F(n);return}U(async()=>{const n=await $("import-wallet-image",{imageBase64:ge,imagePassword:t,password:t});fe=t,ge="",ct.value="",c("image-password").value="",V(n)},!0).catch(()=>I("image-screen",!1))});c("import-private").addEventListener("click",()=>{const e=c("private-key").value.trim(),t=c("private-password").value;try{Q(e,"Paste the private key."),Q(t,"Enter a new encryption key.")}catch(n){M.textContent=F(n);return}U(async()=>{const n=await $("import-private-key",{privateKey:e,password:t});c("private-key").value="",c("private-password").value="",await gn(n,t)},!0).catch(()=>I("private-screen",!1))});c("save-image").addEventListener("click",()=>{U(async()=>{var n;if(!ie)throw new Error("The wallet image is not ready.");const e=((n=g==null?void 0:g.address)==null?void 0:n.split(".")[0])??"contractless";pn(ie,`${e}.wallet.png`);const t=c("backup-confirmed");t.disabled=!1,t.checked=!1,c("open-wallet").disabled=!0})});c("backup-confirmed").addEventListener("change",e=>{c("open-wallet").disabled=!e.currentTarget.checked});c("open-wallet").addEventListener("click",()=>{fe="",ie="",c("wallet-image-preview").removeAttribute("src"),g&&V(g)});c("unlock-form").addEventListener("submit",e=>{e.preventDefault();const t=c("unlock-password").value;try{Q(t,"Enter your encryption key.")}catch(n){M.textContent=F(n);return}U(async()=>{const n=await $("unlock",{password:t});fe=t,c("unlock-password").value="",V(n)},!0).catch(()=>I("locked-screen",!1))});c("approve-provider-approval").addEventListener("click",()=>{oe&&(ye("approve"),U(async()=>{const e=await $("resolve-provider-approval",{id:oe,approved:!0});oe="",await nn(e.remaining??0,!0)}).catch(()=>ye(null)))});c("reject-provider-approval").addEventListener("click",()=>{oe&&(ye("reject"),U(async()=>{const e=await $("resolve-provider-approval",{id:oe,approved:!1});oe="",await nn(e.remaining??0,!1)}).catch(()=>ye(null)))});$("status").then(e=>V(e)).catch(e=>{I("create-screen"),M.textContent=F(e)}); diff --git a/extension/dist/firefox/print.html b/extension/dist/firefox/print.html new file mode 100644 index 0000000..7eeccfa --- /dev/null +++ b/extension/dist/firefox/print.html @@ -0,0 +1,44 @@ + + + + + + Contractless Wallet Backup + + + + + +
+ +
+

Contractless Wallet Backup

+

Private recovery information

+
+
+ +

+ Keep this page private. Anyone with the private key and encryption key can + control this wallet. +

+ +
+

Wallet

+

+    
+
+

Address

+

+    
+
+

Private Key

+

+    
+
+

Encryption Key

+

+    
+ + + + diff --git a/extension/dist/firefox/print.js b/extension/dist/firefox/print.js new file mode 100644 index 0000000..c86b6e5 --- /dev/null +++ b/extension/dist/firefox/print.js @@ -0,0 +1 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";async function a(){const e=new URLSearchParams(location.search).get("id");if(!e)throw new Error("Missing printable wallet backup.");const n=`print-backup:${e}`,o=await chrome.storage.session.get(n);await chrome.storage.session.remove(n);const t=o[n];if(!t)throw new Error("The printable wallet backup has expired.");document.getElementById("wallet-label").textContent=t.label,document.getElementById("wallet-address").textContent=t.address,document.getElementById("wallet-private-key").textContent=t.privateKey,document.getElementById("wallet-encryption-key").textContent=t.encryptionKey,setTimeout(()=>{window.focus(),window.print()},150)}a().catch(e=>{document.getElementById("print-error").textContent=e instanceof Error?e.message:String(e)}); diff --git a/extension/dist/firefox/provider.js b/extension/dist/firefox/provider.js new file mode 100644 index 0000000..47444f0 --- /dev/null +++ b/extension/dist/firefox/provider.js @@ -0,0 +1 @@ +const a=new Map;window.addEventListener("message",e=>{var s,t,o;if(e.source!==window||((s=e.data)==null?void 0:s.source)!=="contractless-wallet")return;const r=a.get(e.data.id);r&&(a.delete(e.data.id),(t=e.data.response)!=null&&t.ok?r.resolve(e.data.response.result):r.reject(new Error(((o=e.data.response)==null?void 0:o.error)??"Wallet request failed.")))});const n=Object.freeze({isContractless:!0,request({method:e,params:r}){const s=crypto.randomUUID();return new Promise((t,o)=>{a.set(s,{resolve:t,reject:o}),window.postMessage({source:"contractless-page",id:s,method:e,params:r},window.location.origin)})}});Object.defineProperty(window,"contractless",{value:n,writable:!1,configurable:!1}); diff --git a/extension/dist/manifest.json b/extension/dist/manifest.json new file mode 100644 index 0000000..c8137d2 --- /dev/null +++ b/extension/dist/manifest.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 3, + "name": "Contractless Wallet", + "version": "0.1.1", + "description": "A local-signing Web3 wallet for Contractless.", + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "permissions": ["storage", "alarms", "activeTab", "clipboardWrite"], + "host_permissions": ["https://api.contractless.dev/*"], + "optional_host_permissions": ["https://*/"], + "action": { + "default_popup": "popup.html", + "default_title": "Contractless Wallet", + "default_icon": { + "16": "contractless-logo.png", + "32": "contractless-logo.png", + "48": "contractless-logo.png", + "128": "contractless-logo.png" + } + }, + "icons": { + "16": "contractless-logo.png", + "32": "contractless-logo.png", + "48": "contractless-logo.png", + "128": "contractless-logo.png" + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"], + "js": ["content.js"], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": ["provider.js"], + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"] + } + ] +} diff --git a/extension/dist/popup.html b/extension/dist/popup.html new file mode 100644 index 0000000..77a3b93 --- /dev/null +++ b/extension/dist/popup.html @@ -0,0 +1,635 @@ + + + + + + Contractless Wallet + + + + + + +
+
+
+ Contractless +
+
+

Opening wallet

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/extension/dist/popup.js b/extension/dist/popup.js new file mode 100644 index 0000000..6f113ab --- /dev/null +++ b/extension/dist/popup.js @@ -0,0 +1,8 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";import{c as Yt}from"./chunks/core-CvwXoH-U.js";function yn(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var de={},De,Ct;function Cn(){return Ct||(Ct=1,De=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}),De}var Ue={},ee={},Et;function se(){if(Et)return ee;Et=1;let e;const t=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];return ee.getSymbolSize=function(o){if(!o)throw new Error('"version" cannot be null or undefined');if(o<1||o>40)throw new Error('"version" should be in range from 1 to 40');return o*4+17},ee.getSymbolTotalCodewords=function(o){return t[o]},ee.getBCHDigit=function(n){let o=0;for(;n!==0;)o++,n>>>=1;return o},ee.setToSJISFunction=function(o){if(typeof o!="function")throw new Error('"toSJISFunc" is not a valid function.');e=o},ee.isKanjiModeEnabled=function(){return typeof e<"u"},ee.toSJIS=function(o){return e(o)},ee}var Fe={},bt;function lt(){return bt||(bt=1,(function(e){e.L={bit:1},e.M={bit:0},e.Q={bit:3},e.H={bit:2};function t(n){if(typeof n!="string")throw new Error("Param is not a string");switch(n.toLowerCase()){case"l":case"low":return e.L;case"m":case"medium":return e.M;case"q":case"quartile":return e.Q;case"h":case"high":return e.H;default:throw new Error("Unknown EC Level: "+n)}}e.isValid=function(o){return o&&typeof o.bit<"u"&&o.bit>=0&&o.bit<4},e.from=function(o,r){if(e.isValid(o))return o;try{return t(o)}catch{return r}}})(Fe)),Fe}var qe,kt;function En(){if(kt)return qe;kt=1;function e(){this.buffer=[],this.length=0}return e.prototype={get:function(t){const n=Math.floor(t/8);return(this.buffer[n]>>>7-t%8&1)===1},put:function(t,n){for(let o=0;o>>n-o-1&1)===1)},getLengthInBits:function(){return this.length},putBit:function(t){const n=Math.floor(this.length/8);this.buffer.length<=n&&this.buffer.push(0),t&&(this.buffer[n]|=128>>>this.length%8),this.length++}},qe=e,qe}var Ve,At;function bn(){if(At)return Ve;At=1;function e(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}return e.prototype.set=function(t,n,o,r){const i=t*this.size+n;this.data[i]=o,r&&(this.reservedBit[i]=!0)},e.prototype.get=function(t,n){return this.data[t*this.size+n]},e.prototype.xor=function(t,n,o){this.data[t*this.size+n]^=o},e.prototype.isReserved=function(t,n){return this.reservedBit[t*this.size+n]},Ve=e,Ve}var Ke={},Lt;function kn(){return Lt||(Lt=1,(function(e){const t=se().getSymbolSize;e.getRowColCoords=function(o){if(o===1)return[];const r=Math.floor(o/7)+2,i=t(o),a=i===145?26:Math.ceil((i-13)/(2*r-2))*2,l=[i-7];for(let c=1;c=0&&r<=7},e.from=function(r){return e.isValid(r)?parseInt(r,10):void 0},e.getPenaltyN1=function(r){const i=r.size;let a=0,l=0,c=0,d=null,u=null;for(let f=0;f=5&&(a+=t.N1+(l-5)),d=m,l=1),m=r.get(p,f),m===u?c++:(c>=5&&(a+=t.N1+(c-5)),u=m,c=1)}l>=5&&(a+=t.N1+(l-5)),c>=5&&(a+=t.N1+(c-5))}return a},e.getPenaltyN2=function(r){const i=r.size;let a=0;for(let l=0;l=10&&(l===1488||l===93)&&a++,c=c<<1&2047|r.get(u,d),u>=10&&(c===1488||c===93)&&a++}return a*t.N3},e.getPenaltyN4=function(r){let i=0;const a=r.data.length;for(let c=0;c=0;){const a=i[0];for(let c=0;c0){const l=new Uint8Array(this.degree);return l.set(i,a),l}return i},We=t,We}var Je={},He={},Ye={},Rt;function Qt(){return Rt||(Rt=1,Ye.isValid=function(t){return!isNaN(t)&&t>=1&&t<=40}),Ye}var z={},It;function Zt(){if(It)return z;It=1;const e="[0-9]+",t="[A-Z $%*+\\-./:]+";let n="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";n=n.replace(/u/g,"\\u");const o="(?:(?![A-Z0-9 $%*+\\-./:]|"+n+`)(?:.|[\r +]))+`;z.KANJI=new RegExp(n,"g"),z.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),z.BYTE=new RegExp(o,"g"),z.NUMERIC=new RegExp(e,"g"),z.ALPHANUMERIC=new RegExp(t,"g");const r=new RegExp("^"+n+"$"),i=new RegExp("^"+e+"$"),a=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");return z.testKanji=function(c){return r.test(c)},z.testNumeric=function(c){return i.test(c)},z.testAlphanumeric=function(c){return a.test(c)},z}var Pt;function ce(){return Pt||(Pt=1,(function(e){const t=Qt(),n=Zt();e.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},e.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},e.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},e.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},e.MIXED={bit:-1},e.getCharCountIndicator=function(i,a){if(!i.ccBits)throw new Error("Invalid mode: "+i);if(!t.isValid(a))throw new Error("Invalid version: "+a);return a>=1&&a<10?i.ccBits[0]:a<27?i.ccBits[1]:i.ccBits[2]},e.getBestModeForData=function(i){return n.testNumeric(i)?e.NUMERIC:n.testAlphanumeric(i)?e.ALPHANUMERIC:n.testKanji(i)?e.KANJI:e.BYTE},e.toString=function(i){if(i&&i.id)return i.id;throw new Error("Invalid mode")},e.isValid=function(i){return i&&i.bit&&i.ccBits};function o(r){if(typeof r!="string")throw new Error("Param is not a string");switch(r.toLowerCase()){case"numeric":return e.NUMERIC;case"alphanumeric":return e.ALPHANUMERIC;case"kanji":return e.KANJI;case"byte":return e.BYTE;default:throw new Error("Unknown mode: "+r)}}e.from=function(i,a){if(e.isValid(i))return i;try{return o(i)}catch{return a}}})(He)),He}var Mt;function _n(){return Mt||(Mt=1,(function(e){const t=se(),n=Gt(),o=lt(),r=ce(),i=Qt(),a=7973,l=t.getBCHDigit(a);function c(p,m,g){for(let v=1;v<=40;v++)if(m<=e.getCapacity(v,g,p))return v}function d(p,m){return r.getCharCountIndicator(p,m)+4}function u(p,m){let g=0;return p.forEach(function(v){const R=d(v.mode,m);g+=R+v.getBitsLength()}),g}function f(p,m){for(let g=1;g<=40;g++)if(u(p,g)<=e.getCapacity(g,m,r.MIXED))return g}e.from=function(m,g){return i.isValid(m)?parseInt(m,10):g},e.getCapacity=function(m,g,v){if(!i.isValid(m))throw new Error("Invalid QR Code version");typeof v>"u"&&(v=r.BYTE);const R=t.getSymbolTotalCodewords(m),C=n.getTotalCodewordsCount(m,g),_=(R-C)*8;if(v===r.MIXED)return _;const E=_-d(v,m);switch(v){case r.NUMERIC:return Math.floor(E/10*3);case r.ALPHANUMERIC:return Math.floor(E/11*2);case r.KANJI:return Math.floor(E/13);case r.BYTE:default:return Math.floor(E/8)}},e.getBestVersionForData=function(m,g){let v;const R=o.from(g,o.M);if(Array.isArray(m)){if(m.length>1)return f(m,R);if(m.length===0)return 1;v=m[0]}else v=m;return c(v.mode,v.getLength(),R)},e.getEncodedBits=function(m){if(!i.isValid(m)||m<7)throw new Error("Invalid QR Code version");let g=m<<12;for(;t.getBCHDigit(g)-l>=0;)g^=a<=0;)c^=t<0&&(i=this.data.substr(r),a=parseInt(i,10),o.put(a,l*3+1))},Ze=t,Ze}var Xe,Ut;function Rn(){if(Ut)return Xe;Ut=1;const e=ce(),t=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function n(o){this.mode=e.ALPHANUMERIC,this.data=o}return n.getBitsLength=function(r){return 11*Math.floor(r/2)+6*(r%2)},n.prototype.getLength=function(){return this.data.length},n.prototype.getBitsLength=function(){return n.getBitsLength(this.data.length)},n.prototype.write=function(r){let i;for(i=0;i+2<=this.data.length;i+=2){let a=t.indexOf(this.data[i])*45;a+=t.indexOf(this.data[i+1]),r.put(a,11)}this.data.length%2&&r.put(t.indexOf(this.data[i]),6)},Xe=n,Xe}var et,Ft;function In(){if(Ft)return et;Ft=1;const e=ce();function t(n){this.mode=e.BYTE,typeof n=="string"?this.data=new TextEncoder().encode(n):this.data=new Uint8Array(n)}return t.getBitsLength=function(o){return o*8},t.prototype.getLength=function(){return this.data.length},t.prototype.getBitsLength=function(){return t.getBitsLength(this.data.length)},t.prototype.write=function(n){for(let o=0,r=this.data.length;o=33088&&i<=40956)i-=33088;else if(i>=57408&&i<=60351)i-=49472;else throw new Error("Invalid SJIS character: "+this.data[r]+` +Make sure your charset is UTF-8`);i=(i>>>8&255)*192+(i&255),o.put(i,13)}},tt=n,tt}var nt={exports:{}},Vt;function Mn(){return Vt||(Vt=1,(function(e){var t={single_source_shortest_paths:function(n,o,r){var i={},a={};a[o]=0;var l=t.PriorityQueue.make();l.push(o,0);for(var c,d,u,f,p,m,g,v,R;!l.empty();){c=l.pop(),d=c.value,f=c.cost,p=n[d]||{};for(u in p)p.hasOwnProperty(u)&&(m=p[u],g=f+m,v=a[u],R=typeof a[u]>"u",(R||v>g)&&(a[u]=g,l.push(u,g),i[u]=d))}if(typeof r<"u"&&typeof a[r]>"u"){var C=["Could not find a path from ",o," to ",r,"."].join("");throw new Error(C)}return i},extract_shortest_path_from_predecessor_list:function(n,o){for(var r=[],i=o;i;)r.push(i),n[i],i=n[i];return r.reverse(),r},find_path:function(n,o,r){var i=t.single_source_shortest_paths(n,o,r);return t.extract_shortest_path_from_predecessor_list(i,r)},PriorityQueue:{make:function(n){var o=t.PriorityQueue,r={},i;n=n||{};for(i in o)o.hasOwnProperty(i)&&(r[i]=o[i]);return r.queue=[],r.sorter=n.sorter||o.default_sorter,r},default_sorter:function(n,o){return n.cost-o.cost},push:function(n,o){var r={value:n,cost:o};this.queue.push(r),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};e.exports=t})(nt)),nt.exports}var Kt;function $n(){return Kt||(Kt=1,(function(e){const t=ce(),n=xn(),o=Rn(),r=In(),i=Pn(),a=Zt(),l=se(),c=Mn();function d(C){return unescape(encodeURIComponent(C)).length}function u(C,_,E){const N=[];let D;for(;(D=C.exec(E))!==null;)N.push({data:D[0],index:D.index,mode:_,length:D[0].length});return N}function f(C){const _=u(a.NUMERIC,t.NUMERIC,C),E=u(a.ALPHANUMERIC,t.ALPHANUMERIC,C);let N,D;return l.isKanjiModeEnabled()?(N=u(a.BYTE,t.BYTE,C),D=u(a.KANJI,t.KANJI,C)):(N=u(a.BYTE_KANJI,t.BYTE,C),D=[]),_.concat(E,N,D).sort(function(L,A){return L.index-A.index}).map(function(L){return{data:L.data,mode:L.mode,length:L.length}})}function p(C,_){switch(_){case t.NUMERIC:return n.getBitsLength(C);case t.ALPHANUMERIC:return o.getBitsLength(C);case t.KANJI:return i.getBitsLength(C);case t.BYTE:return r.getBitsLength(C)}}function m(C){return C.reduce(function(_,E){const N=_.length-1>=0?_[_.length-1]:null;return N&&N.mode===E.mode?(_[_.length-1].data+=E.data,_):(_.push(E),_)},[])}function g(C){const _=[];for(let E=0;E=0&&k<=6&&(B===0||B===6)||B>=0&&B<=6&&(k===0||k===6)||k>=2&&k<=4&&B>=2&&B<=4?y.set(b+k,S+B,!0,!0):y.set(b+k,S+B,!1,!0))}}function g(y){const L=y.size;for(let A=8;A>k&1)===1,y.set(T,b,S,!0),y.set(b,T,S,!0)}function C(y,L,A){const w=y.size,T=u.getEncodedBits(L,A);let b,S;for(b=0;b<15;b++)S=(T>>b&1)===1,b<6?y.set(b,8,S,!0):b<8?y.set(b+1,8,S,!0):y.set(w-15+b,8,S,!0),b<8?y.set(8,w-b-1,S,!0):b<9?y.set(8,15-b-1+1,S,!0):y.set(8,15-b-1,S,!0);y.set(w-8,8,1,!0)}function _(y,L){const A=y.size;let w=-1,T=A-1,b=7,S=0;for(let k=A-1;k>0;k-=2)for(k===6&&k--;;){for(let B=0;B<2;B++)if(!y.isReserved(T,k-B)){let X=!1;S>>b&1)===1),y.set(T,k-B,X),b--,b===-1&&(S++,b=7)}if(T+=w,T<0||A<=T){T-=w,w=-w;break}}}function E(y,L,A){const w=new n;A.forEach(function(B){w.put(B.mode.bit,4),w.put(B.getLength(),f.getCharCountIndicator(B.mode,y)),B.write(w)});const T=e.getSymbolTotalCodewords(y),b=l.getTotalCodewordsCount(y,L),S=(T-b)*8;for(w.getLengthInBits()+4<=S&&w.put(0,4);w.getLengthInBits()%8!==0;)w.putBit(0);const k=(S-w.getLengthInBits())/8;for(let B=0;B=7&&R(B,L),_(B,S),isNaN(w)&&(w=a.getBestMask(B,C.bind(null,B,A))),a.applyMask(w,B),C(B,A,w),{modules:B,version:L,errorCorrectionLevel:A,maskPattern:w,segments:T}}return Ue.create=function(L,A){if(typeof L>"u"||L==="")throw new Error("No input text");let w=t.M,T,b;return typeof A<"u"&&(w=t.from(A.errorCorrectionLevel,t.M),T=d.from(A.version),b=a.from(A.maskPattern),A.toSJISFunc&&e.setToSJISFunction(A.toSJISFunc)),D(L,T,w,b)},Ue}var rt={},ot={},zt;function Xt(){return zt||(zt=1,(function(e){function t(n){if(typeof n=="number"&&(n=n.toString()),typeof n!="string")throw new Error("Color should be defined as hex string");let o=n.slice().replace("#","").split("");if(o.length<3||o.length===5||o.length>8)throw new Error("Invalid hex color: "+n);(o.length===3||o.length===4)&&(o=Array.prototype.concat.apply([],o.map(function(i){return[i,i]}))),o.length===6&&o.push("F","F");const r=parseInt(o.join(""),16);return{r:r>>24&255,g:r>>16&255,b:r>>8&255,a:r&255,hex:"#"+o.slice(0,6).join("")}}e.getOptions=function(o){o||(o={}),o.color||(o.color={});const r=typeof o.margin>"u"||o.margin===null||o.margin<0?4:o.margin,i=o.width&&o.width>=21?o.width:void 0,a=o.scale||4;return{width:i,scale:i?4:a,margin:r,color:{dark:t(o.color.dark||"#000000ff"),light:t(o.color.light||"#ffffffff")},type:o.type,rendererOpts:o.rendererOpts||{}}},e.getScale=function(o,r){return r.width&&r.width>=o+r.margin*2?r.width/(o+r.margin*2):r.scale},e.getImageWidth=function(o,r){const i=e.getScale(o,r);return Math.floor((o+r.margin*2)*i)},e.qrToImageData=function(o,r,i){const a=r.modules.size,l=r.modules.data,c=e.getScale(a,i),d=Math.floor((a+i.margin*2)*c),u=i.margin*c,f=[i.color.light,i.color.dark];for(let p=0;p=u&&m>=u&&p"u"&&(!a||!a.getContext)&&(c=a,a=void 0),a||(d=o()),c=t.getOptions(c);const u=t.getImageWidth(i.modules.size,c),f=d.getContext("2d"),p=f.createImageData(u,u);return t.qrToImageData(p.data,i,c),n(f,d,u),f.putImageData(p,0,0),d},e.renderToDataURL=function(i,a,l){let c=l;typeof c>"u"&&(!a||!a.getContext)&&(c=a,a=void 0),c||(c={});const d=e.render(i,a,c),u=c.type||"image/png",f=c.rendererOpts||{};return d.toDataURL(u,f.quality)}})(rt)),rt}var it={},Wt;function Fn(){if(Wt)return it;Wt=1;const e=Xt();function t(r,i){const a=r.a/255,l=i+'="'+r.hex+'"';return a<1?l+" "+i+'-opacity="'+a.toFixed(2).slice(1)+'"':l}function n(r,i,a){let l=r+i;return typeof a<"u"&&(l+=" "+a),l}function o(r,i,a){let l="",c=0,d=!1,u=0;for(let f=0;f0&&p>0&&r[f-1]||(l+=d?n("M",p+a,.5+m+a):n("m",c,0),c=0,d=!1),p+1':"",m="',g='viewBox="0 0 '+f+" "+f+'"',R=''+p+m+` +`;return typeof l=="function"&&l(null,R),R},it}var Jt;function qn(){if(Jt)return de;Jt=1;const e=Cn(),t=Dn(),n=Un(),o=Fn();function r(i,a,l,c,d){const u=[].slice.call(arguments,1),f=u.length,p=typeof u[f-1]=="function";if(!p&&!e())throw new Error("Callback required as last argument");if(p){if(f<2)throw new Error("Too few arguments provided");f===2?(d=l,l=a,a=c=void 0):f===3&&(a.getContext&&typeof d>"u"?(d=c,c=void 0):(d=c,c=l,l=a,a=void 0))}else{if(f<1)throw new Error("Too few arguments provided");return f===1?(l=a,a=c=void 0):f===2&&!a.getContext&&(c=l,l=a,a=void 0),new Promise(function(m,g){try{const v=t.create(l,c);m(i(v,a,c))}catch(v){g(v)}})}try{const m=t.create(l,c);d(null,i(m,a,c))}catch(m){d(m)}}return de.create=t.create,de.toCanvas=r.bind(null,n.render),de.toDataURL=r.bind(null,n.renderToDataURL),de.toString=r.bind(null,function(i,a,l){return o.render(i,l)}),de}var Vn=qn();const Kn=yn(Vn),s=e=>document.getElementById(e),jn=Array.from(document.querySelectorAll(".screen")),M=s("notice");let Ne="",fe="",ge="",ie="",h,dt,Te="held",K=1,ae=0;const ve=8;let x,J=[],Z,P,_e,en="activity",Y,we="",oe="";const he=20,zn={1:"Mining Reward",2:"Transfer",3:"Token Creation",4:"NFT/RWA Creation",5:"Marketing",6:"Swap",7:"Loan",8:"Loan Payment",9:"Collateral Claim",10:"Asset Burn",11:"Token Issuance",12:"Vanity Address",100:"Storage Key",101:"Data Storage Bool",102:"Data Storage U8",103:"Data Storage U16",104:"Data Storage U32",105:"Data Storage U64",106:"Data Storage U128",107:"Data Storage String",108:"Data Storage I8",109:"Data Storage I16",110:"Data Storage I32",111:"Data Storage I64",112:"Data Storage I128",113:"Delete Stored Data",200:"Proposal",201:"Proposal Vote",202:"Activation Vote"};async function $(e,t={}){const n=await chrome.runtime.sendMessage({type:e,...t});if(!(n!=null&&n.ok))throw new Error((n==null?void 0:n.error)??"Wallet request failed.");return n.result}function I(e,t=!0){jn.forEach(n=>{n.hidden=n.id!==e}),t&&(M.textContent="")}function V(e){var i;h=e;const t=e.address??((i=e.wallets.find(a=>a.active))==null?void 0:i.address),n=(t==null?void 0:t.endsWith(".cltc"))??!0,o=n?"Testnet":"Mainnet",r=n?"CLTC":"CLC";document.querySelectorAll("[data-network-name]").forEach(a=>{a.textContent=o}),document.querySelectorAll("[data-network-symbol]").forEach(a=>{a.textContent=r}),s("base-asset-name").textContent=`Contractless ${o}`,s("wallet-address").textContent=e.address??"",s("wallet-address-short").textContent=e.address?`${e.address.slice(0,6)}...${e.address.slice(-6)}`:"Account",s("settings-api-url").value=e.apiUrl,s("settings-lock-timeout").value=String(e.autoLockMinutes),Zn(e),e.exists?e.unlocked?at().then(a=>{a||(I("dashboard-screen"),Be())}):I("locked-screen"):I("create-screen")}function On(e,t){return e==="contractless_connect"?"Connect application":e==="contractless_signMessage"?"Sign text message":e==="contractless_sendTransaction"||e==="contractless_signDualTransaction"?`Sign ${String(t.transaction_type??"transaction")}`:"Review request"}function j(e,t,n){const o=document.createElement("dt");o.textContent=t.replaceAll("_"," ");const r=document.createElement("dd");r.textContent=typeof n=="object"?JSON.stringify(n,null,2):String(n),e.append(o,r)}function te(e,t,n,o){const r=re(e[t]),i=String(e[n]??"").trim().toUpperCase(),a=Number(e[o]??0);return`${r} ${i}${a>0?` #${a}`:""}`}function Ht(e,t,n){var i;const o=n===1?2:1,r=(i=h==null?void 0:h.address)!=null&&i.endsWith(".cltc")?"CLTC":"CLC";j(e,"Your miner tip",te(t,`tip${n}`,`ticker${n}`,`nft_series${n}`)),j(e,"Your transaction fee",`${re(t[`txfee${n}`])} ${r}`),j(e,"Other party miner tip",te(t,`tip${o}`,`ticker${o}`,`nft_series${o}`)),j(e,"Other party transaction fee",`${re(t[`txfee${o}`])} ${r}`)}function Wn(e,t){var a;const n=((a=h==null?void 0:h.address)==null?void 0:a.trim().toLowerCase())??"",o=String(t.sender1??"").trim().toLowerCase(),r=String(t.sender2??"").trim().toLowerCase(),i=n===o?1:n===r?2:0;if(i===1){j(e,"Sending",te(t,"value1","ticker1","nft_series1")),j(e,"Receiving",te(t,"value2","ticker2","nft_series2")),Ht(e,t,1);return}if(i===2){j(e,"Sending",te(t,"value2","ticker2","nft_series2")),j(e,"Receiving",te(t,"value1","ticker1","nft_series1")),Ht(e,t,2);return}j(e,"First party sending",te(t,"value1","ticker1","nft_series1")),j(e,"Second party sending",te(t,"value2","ticker2","nft_series2"))}function ye(e){const t=s("approve-provider-approval"),n=s("reject-provider-approval"),o=e!==null;t.disabled=o,n.disabled=o,t.classList.toggle("is-processing",e==="approve"),n.classList.toggle("is-processing",e==="reject"),t.textContent=e==="approve"?"Processing...":"Approve",n.textContent=e==="reject"?"Rejecting...":"Reject"}async function at(){const e=await $("pending-provider-approval");if(!(e!=null&&e.request))return!1;const t=e.request;ye(null),oe=t.id;const n=(e.total??0)>1?` · Request ${e.position??1} of ${e.total}`:"";s("provider-approval-origin").textContent=`${t.origin}${n}`;const o=s("provider-approval-risk");o.className=`risk-label risk-${t.risk}`,o.textContent=t.risk==="safe"?"Safe":t.risk==="low"?"Low risk":"High risk";const r=t.display??{},i=t.method==="contractless_sendTransaction"||t.method==="contractless_signDualTransaction";s("provider-approval-title").textContent=On(t.method,r),s("provider-approval-intro").textContent=i?"Review every transaction field before signing.":"Review this request before approving it.";const a=s("provider-approval-details");return a.replaceChildren(),Object.entries(r).forEach(([l,c])=>{if(l==="transaction_fields"&&c&&typeof c=="object"){const d=c,u=Number(d.txtype??0),f=new Set(["ticker1","nft_series1","value1","ticker2","nft_series2","value2","tip1","tip2","txfee1","txfee2"]);u===6&&Wn(a,d),Object.entries(d).forEach(([p,m])=>{u===6&&f.has(p)||j(a,p==="txtype"?"transaction_type_id":p,ln(u,p,m))});return}j(a,l,c)}),I("provider-approval-screen"),!0}async function tn(e,t){if(!(e>0&&await at())){if(t){const n=Date.now()+1500;for(;Date.now()setTimeout(o,75))}}window.close()}}function Jn(e){return e.replace(/[.0]/g,"").length>0}function st(){var c;const e=s("token-list");if(e.replaceChildren(),!x){const d=document.createElement("div");d.className="empty-assets";const u=document.createElement("strong");u.textContent="Loading token balances...";const f=document.createElement("span");f.textContent="Balances are retrieved from the selected Contractless API.",d.append(u,f),e.append(d);return}const t=(c=h==null?void 0:h.address)!=null&&c.endsWith(".cltc")?"CLTC":"CLC",n=new Set(x.tokens.map(d=>d.token.trim().toUpperCase())),o=new Set(x.nfts.map(d=>`${d.name.trim().toUpperCase()}|${Number(d.series||0)}`)),r=x.balances.filter(d=>{const u=d.asset.trim().toUpperCase(),f=`${u}|${Number(d.nft_series||0)}`;return d.nft_series===0&&u!=="CLTC"&&u!=="CLC"&&n.has(u)&&!o.has(f)}),i=new Map(r.map(d=>[d.asset.trim().toUpperCase(),d])),a=Te==="all"?x.tokens.map(d=>d.token):r.filter(d=>Jn(d.balance)).map(d=>d.asset),l=[...new Set(a.map(d=>d.trim().toUpperCase()))].filter(d=>d!==t&&!o.has(`${d}|0`)).sort((d,u)=>d.localeCompare(u));if(l.length===0){const d=document.createElement("div");d.className="empty-assets";const u=document.createElement("strong");u.textContent=Te==="held"?"No token balances found":"No tokens found";const f=document.createElement("span");f.textContent=Te==="held"?"Switch to All tokens to view the complete network token list.":x.tokenCatalogError?`Token catalog unavailable: ${x.tokenCatalogError}`:"The selected API did not return any fungible tokens.",d.append(u,f),e.append(d);return}l.forEach(d=>{const u=i.get(d),f=document.createElement("div");f.className="asset-row";const p=document.createElement("div");p.className="asset-symbol token-symbol",p.textContent=d.slice(0,3);const m=document.createElement("div");m.className="asset-name";const g=document.createElement("strong");g.textContent=d;const v=document.createElement("span");v.textContent="Contractless token",m.append(g,v);const R=document.createElement("div");R.className="asset-value";const C=document.createElement("strong");C.textContent=(u==null?void 0:u.balance)??"0.00000000";const _=document.createElement("span");_.textContent=d,R.append(C,_),f.append(p,m,R),e.append(f)})}async function Be(){var e;if(h!=null&&h.unlocked){x=void 0,s("base-balance").textContent="--",s("base-asset-balance").textContent="--",st();try{const t=await $("wallet-balances");x={balances:Array.isArray(t.balances)?t.balances:[],tokens:Array.isArray(t.tokens)?t.tokens:[],nfts:Array.isArray(t.nfts)?t.nfts:[],tokenCatalogError:t.tokenCatalogError,nftCatalogError:t.nftCatalogError};const n=(e=h.address)!=null&&e.endsWith(".cltc")?"CLTC":"CLC",o=x.balances.find(i=>i.nft_series===0&&i.asset.toUpperCase()===n),r=(o==null?void 0:o.balance)??"0.00000000";s("base-balance").textContent=r,s("base-asset-balance").textContent=r,st()}catch(t){const n=s("token-list");n.replaceChildren();const o=document.createElement("div");o.className="empty-assets balance-error";const r=document.createElement("strong");r.textContent="Unable to load balances";const i=document.createElement("span");i.textContent=q(t),o.append(r,i),n.append(o),M.textContent=q(t)}}}function Le(e,t){return`${e.trim().toUpperCase()}|${t}`}function Hn(e){try{return H(e,"Balance")>0n}catch{return!1}}function nn(e){if(e.ownership_type!==1)return"Complete ownership";try{const t=H(e.balance,"Balance"),n=H(e.supply,"Supply");if(n<=0n)return"0% ownership";const o=t*100000000n/n,r=o/1000000n,i=(o%1000000n).toString().padStart(6,"0").replace(/0+$/,"");return`${r}${i?`.${i}`:""}% ownership`}catch{return"Fractional ownership"}}function ut(e){var t;return(t=e.details)!=null&&t.metadata&&typeof e.details.metadata=="object"?e.details.metadata:{}}function ft(e){const t=ut(e).name;return typeof t=="string"&&t.trim()?t.trim():e.series>0?`${e.name} #${e.series}`:e.name}function rn(e){const n=ut(e).contractless;return n&&typeof n=="object"&&String(n.metadata_type??"").toLowerCase()==="rwa"?"RWA":"NFT"}function be(e){return e.series>0?`${e.name}_${e.series}`:e.name}function Yn(e,t=7,n=7){return e.length>t+n+3?`${e.slice(0,t)}...${e.slice(-n)}`:e}function Gn(e,t){var r;const n=document.createElement("div");if(n.className=t,(r=e.details)!=null&&r.image_data_url){const i=document.createElement("img");return i.src=e.details.image_data_url,i.alt=ft(e),n.append(i),n}const o=document.createElement("span");return o.textContent=e.loading?"...":e.name.slice(0,2).toUpperCase(),n.append(o),n}function on(){const e=(K-1)*ve;return J.slice(e,e+ve)}function Ce(){const e=s("nfts-list");e.replaceChildren();const t=Math.max(1,Math.ceil(J.length/ve));if(K=Math.min(Math.max(1,K),t),s("nfts-count").textContent=J.length===1?"1 owned asset":`${J.length} owned assets`,s("nfts-page").textContent=String(K),s("nfts-pages").textContent=String(t),s("nfts-pagination").hidden=t<=1,s("nfts-previous").disabled=K<=1,s("nfts-next").disabled=K>=t,!J.length){const n=document.createElement("div");n.className="nfts-empty";const o=document.createElement("strong");o.textContent="No NFTs or RWAs found";const r=document.createElement("span");r.textContent=x!=null&&x.nftCatalogError?`NFT catalog unavailable: ${x.nftCatalogError}`:"Assets owned by this wallet will appear here.",n.append(o,r),e.append(n);return}on().forEach(n=>{const o=document.createElement("button");o.type="button",o.className="nft-card",o.append(Gn(n,"nft-card-media"));const r=document.createElement("div");r.className="nft-card-identity";const i=document.createElement("span");i.textContent=rn(n);const a=document.createElement("strong");a.textContent=ft(n);const l=document.createElement("small");l.textContent=be(n);const c=document.createElement("b");if(c.textContent=nn(n),r.append(i,a,l,c),n.error){const d=document.createElement("em");d.textContent="Media unavailable",r.append(d)}o.append(r),o.addEventListener("click",()=>Qn(n)),e.append(o)})}function Se(e,t,n){const o=document.createElement("div"),r=document.createElement("dt");r.textContent=t;const i=document.createElement("dd");i.textContent=n||"--",o.append(r,i),e.append(o)}function xe(e){Z=e;const t=e.details,n=ut(e),o=rn(e),r=ft(e);s("nft-detail-title").textContent=r,s("nft-detail-kind").textContent=`${o} details`,s("nft-detail-badge").textContent=o,s("nft-detail-name").textContent=r,s("nft-detail-ownership").textContent=nn(e);const i=s("nft-detail-image");i.src=(t==null?void 0:t.image_data_url)??"",i.alt=r,i.hidden=!(t!=null&&t.image_data_url);const a=n.description??n.summary;s("nft-detail-description").textContent=typeof a=="string"&&a.trim()?a.trim():t?"No description was included in the NFT metadata.":"Loading NFT metadata and provenance...";const l=s("nft-detail-attributes");l.replaceChildren(),(Array.isArray(n.attributes)?n.attributes.slice(0,12):[]).forEach(f=>{if(!f||typeof f!="object")return;const p=f,m=document.createElement("div"),g=document.createElement("span");g.textContent=String(p.trait_type??p.type??"Attribute");const v=document.createElement("strong");v.textContent=String(p.value??""),m.append(g,v),l.append(m)}),l.hidden=l.childElementCount===0;const d=s("nft-detail-fields");d.replaceChildren(),Se(d,"Asset",be(e)),Se(d,"Creator",(t==null?void 0:t.creator)??"--"),Se(d,"IPFS CID",(t==null?void 0:t.item_ipfs)??"--"),Se(d,"Origin transaction",(t==null?void 0:t.genesis_txid)??e.origin_txid);const u=s("nft-detail-history");if(u.replaceChildren(),t)if(t.history.length)t.history.slice().reverse().forEach(f=>{const p=document.createElement("button");p.type="button",p.className="nft-history-row",p.title=`View full transaction ${f.txid}`;const m=document.createElement("strong");m.textContent=f.action;const g=document.createElement("span");g.textContent=`Block ${Number(f.block).toLocaleString()}`;const v=document.createElement("small");v.textContent=Yn(f.txid),p.append(m,g,v),p.addEventListener("click",()=>{un({txid:f.txid,block_height:Number(f.block),transaction_hex:""},"nft")}),u.append(p)});else{const f=document.createElement("p");f.textContent="No provenance records were returned.",u.append(f)}else{const f=document.createElement("p");f.textContent="Loading provenance...",u.append(f)}}function Qn(e){xe(e),I("nft-detail-screen")}async function mt(e){const t=on().filter(r=>!r.details&&!r.loading);let n=0;const o=async()=>{for(;n[Le(r.name,r.series),r]));J=x.nfts.map(r=>{const i=x==null?void 0:x.balances.find(l=>Le(l.asset,Number(l.nft_series||0))===Le(r.name,Number(r.series||0)));if(!i||!Hn(i.balance))return null;const a=n.get(Le(r.name,r.series));return{name:r.name.trim(),series:Number(r.series||0),origin_txid:r.origin_txid,ownership_type:Number(r.ownership_type||0),supply:r.supply,balance:i.balance,details:a==null?void 0:a.details,error:a==null?void 0:a.error}}).filter(r=>r!==null).sort((r,i)=>r.name.localeCompare(i.name)||r.series-i.series);const o=Math.max(1,Math.ceil(J.length/ve));K=Math.min(K,o),Ce(),await mt(t)}function Zn(e){const t=s("known-addresses");t.replaceChildren(),e.wallets.forEach(n=>{const o=document.createElement("div");o.className=`known-address${n.active?" active-address":""}`;const r=document.createElement("button");r.type="button",r.className="address-select",r.dataset.walletId=n.id;const i=document.createElement("span");i.className="address-avatar";const a=document.createElement("img");a.src=chrome.runtime.getURL("contractless-logo.png"),a.alt="",i.append(a);const l=document.createElement("span");l.className="address-details";const c=document.createElement("strong");c.textContent=n.label;const d=document.createElement("small");d.textContent=n.address,l.append(c,d);const u=document.createElement("span");u.className="selected-mark",u.textContent=n.active?"✓":"",r.append(i,l,u),r.addEventListener("click",()=>{if(n.active){G(!1);return}U(async()=>{G(!1),fe="",ie="",V(await $("switch-wallet",{walletId:n.id}))})});const f=document.createElement("button");f.type="button",f.className="rename-address",f.title="Rename wallet",f.setAttribute("aria-label",`Rename ${n.label}`),f.textContent="✎",f.addEventListener("click",()=>{const p=window.prompt("Wallet label",n.label);p===null||p.trim()===n.label||U(async()=>{const m=await $("rename-wallet",{walletId:n.id,label:p});V(m),G(!0)})}),o.append(r,f),t.append(o)})}function G(e){const t=s("account-drawer"),n=s("drawer-backdrop"),o=s("account-menu-button");t.classList.toggle("open",e),t.setAttribute("aria-hidden",String(!e)),o.setAttribute("aria-expanded",String(e)),n.hidden=!e}function gt(){s("revealed-private-key").value="",s("private-key-result").hidden=!0}function Ee(e){if(s("backup-key-modal").hidden=!e,s("backup-key-backdrop").hidden=!e,e){const t=s("backup-key-input");t.value="",t.focus()}else s("backup-key-input").value="",dt=void 0}async function Xn(){const e=await $("wallet-receive-address"),t=String(e.receiveAddress??"");if(!t)throw new Error("No active wallet address is available.");s("receive-address-label").textContent=e.vanity?"Vanity address":"Wallet address",s("receive-address-value").textContent=t;const n=s("receive-qr");await Kn.toCanvas(n,t,{width:220,margin:1,errorCorrectionLevel:"M",color:{dark:"#071016",light:"#ffffff"}}),I("receive-screen")}function H(e,t){const n=e.trim();if(!/^(?:0|[1-9]\d*)(?:\.\d{1,8})?$/.test(n))throw new Error(`${t} must be a positive number with no more than 8 decimal places.`);const[o,r=""]=n.split(".");return BigInt(o)*100000000n+BigInt(r.padEnd(8,"0"))}function ne(e){const t=e/100000000n,n=(e%100000000n).toString().padStart(8,"0");return`${t}.${n}`}function an(){if(P)return P.balance;const e=s("send-asset").value,t=x==null?void 0:x.balances.find(n=>n.nft_series===0&&n.asset.trim().toUpperCase()===e);return(t==null?void 0:t.balance)??"0.00000000"}function sn(){var o;const e=s("send-asset").value,t=(o=h==null?void 0:h.address)!=null&&o.endsWith(".cltc")?"CLTC":"CLC";if(e!==t)return 100000000n;const n=s("send-amount").value.trim();if(!n)return 0n;try{return(H(n,"Amount")+99n)/100n}catch{return 0n}}function Re(){var r;const e=(P==null?void 0:P.name)??s("send-asset").value,t=(r=h==null?void 0:h.address)!=null&&r.endsWith(".cltc")?"CLTC":"CLC",n=P?be(P):e;s("send-amount-symbol").textContent=n,s("send-available").textContent=`Available: ${an()} ${n}`;const o=sn();s("send-fee").value=ne(o),s("send-fee-note").textContent=!P&&e===t?"Minimum fee: 1% of the transfer amount.":`Minimum fee: 1 ${t} for asset transfers.`}function er(){var o;const e=s("send-asset");e.replaceChildren();const t=(o=h==null?void 0:h.address)!=null&&o.endsWith(".cltc")?"CLTC":"CLC",n=new Set([t]);x==null||x.tokens.forEach(r=>{const i=r.token.trim().toUpperCase();(x==null?void 0:x.balances.find(l=>l.nft_series===0&&l.asset.trim().toUpperCase()===i&&H(l.balance,"Balance")>0n))&&n.add(i)}),n.forEach(r=>{const i=document.createElement("option");i.value=r,i.textContent=r,e.append(i)}),Re()}function tr(){if(!x)throw new Error("Wallet balances are still loading.");P=void 0,Y=void 0,s("send-form").reset(),s("send-asset").disabled=!1,s("send-amount").disabled=!1,er(),I("send-screen")}function nr(e){if(!x)throw new Error("Wallet balances are still loading.");P=e,Y=void 0,s("send-form").reset();const t=s("send-asset");t.replaceChildren();const n=document.createElement("option");n.value=e.name,n.textContent=be(e),t.append(n),t.disabled=!0;const o=s("send-amount");e.ownership_type===0?(o.value="1.00000000",o.disabled=!0):(o.value="",o.disabled=!1),Re(),I("send-screen")}async function rr(){var v;if(!(h!=null&&h.address))throw new Error("No active wallet is available.");const e=(P==null?void 0:P.name)??s("send-asset").value,t=P?be(P):e,n=Q(s("send-recipient").value,"Enter a receiving address."),o=Q(s("send-amount").value,"Enter an amount to send."),r=Q(s("send-fee").value,"Enter a transaction fee."),i=H(o,"Amount"),a=H(r,"Transaction fee");if(i===0n)throw new Error("The transfer amount must be greater than zero.");if((P==null?void 0:P.ownership_type)===0&&i!==100000000n)throw new Error("An indivisible NFT must be transferred as one complete asset.");if((P==null?void 0:P.ownership_type)===1&&i>100000000n)throw new Error("A fractional NFT/RWA transfer cannot exceed one complete asset.");const l=sn();if(ac)throw new Error(`Insufficient ${e} balance.`);const d=h.address.endsWith(".cltc")?"CLTC":"CLC",u=((v=x==null?void 0:x.balances.find(R=>R.nft_series===0&&R.asset.trim().toUpperCase()===d))==null?void 0:v.balance)??"0.00000000",f=H(u,"Base balance");if((e===d?i+a:a)>f)throw new Error(`Insufficient ${d} balance including the fee.`);const m=await $("wallet-prepare-transfer",{receiver:n,asset:e,nftSeries:(P==null?void 0:P.series)??0,valueAtomic:i.toString(),feeAtomic:a.toString()}),g=e===d?`${ne(i+a)} ${d}`:`${ne(i)} ${t} + ${ne(a)} ${d}`;Y={transaction:m.transaction,receiverInput:m.receiverInput,asset:e,amount:ne(i),fee:ne(a),total:g},s("review-amount").textContent=Y.amount,s("review-asset").textContent=t,s("review-sender").textContent=h.address,s("review-recipient").textContent=m.receiverInput,s("review-resolved-recipient").textContent=String(m.review.receiver??""),s("review-fee").textContent=Y.fee,s("review-total").textContent=g,I("send-review-screen")}function cn(e){if(!/^[0-9a-f]+$/i.test(e)||e.length<10)return;const t=e.slice(2,10).match(/../g);if(!t||t.length!==4)return;const n=t.reduce((r,i,a)=>r+Number.parseInt(i,16)*2**(a*8),0),o=new Date(n*1e3);return Number.isNaN(o.getTime())?void 0:o}function or(e){return e.split("_").map(t=>t&&`${t[0].toUpperCase()}${t.slice(1)}`).join(" ")}function re(e){try{const t=ne(BigInt(String(e??"0"))),[n,o]=t.split("."),r=o.replace(/0+$/,"");return r?`${n}.${r}`:`${n}.0`}catch{return String(e??"")}}function ir(e,t){var o;return t.startsWith("txfee")?!0:((o={1:new Set(["value"]),2:new Set(["value"]),3:new Set(["number"]),6:new Set(["value1","value2","tip1","tip2"]),7:new Set(["loan_amount","collateral_amount","payment_amount","max_late_value"]),8:new Set(["payback_amount","tip"]),10:new Set(["value"]),11:new Set(["number"])}[e])==null?void 0:o.has(t))??!1}function ln(e,t,n){if(typeof n=="boolean")return n?"True":"False";if(ir(e,t))return re(n);if(t==="time"||t==="timestamp"||t==="offer_expiration"){const o=Number(n);if(Number.isFinite(o))return`${new Date(o*1e3).toLocaleString()} (${o})`}return t==="payment_period"?{d:"Daily",w:"Weekly",m:"Monthly"}[String(n).trim().toLowerCase()]??String(n??""):e===3&&t==="hard_limit"?Number(n)===1?"Yes":"No":e===4&&t==="series"?Number(n)===1?"Series":"Single NFT/RWA":e===4&&t==="ownership_type"?Number(n)===1?"Fractional ownership":"Complete ownership":(e===201||e===202)&&t==="vote"?Number(n)===1?"Approve":"Reject":(t==="previous"||t.endsWith("_hash"))&&/^0+$/.test(String(n))?"None":e===5&&(t==="impression_value"||t==="click_value")?(Number(n)/100).toFixed(2):typeof n=="string"?n.trim():String(n??"")}function F(e,t,n,o=!1,r=0){const i=document.createElement("div");i.className=`transaction-field${o?" technical-field":""}`;const a=document.createElement("span");a.textContent=or(t);const l=document.createElement("strong");l.textContent=o?String(n??""):ln(r,t,n),i.append(a,l),e.append(i)}function ht(e,t){return!e||!t||t=he?`${n} confirmations`:`${n}/${he} confirmations`:`Pending · 0/${he} confirmations`}async function un(e,t="activity"){_e=e,en=t;const n=s("close-transaction");n.title=t==="nft"?"Back to NFT/RWA details":"Back to activity",n.setAttribute("aria-label",t==="nft"?"Back to NFT/RWA details":"Back to activity"),I("transaction-screen"),s("transaction-title").textContent="Loading transaction",s("transaction-header-status").textContent="",s("transaction-full-txid").textContent=e.txid,s("transaction-overview").replaceChildren(),s("transaction-fields").replaceChildren(),s("transaction-technical-fields").replaceChildren(),s("transaction-miner-earnings").replaceChildren(),s("miner-earnings-section").hidden=!0;try{const o=await $("wallet-transaction",{txid:e.txid}),r=await Yt(),i=JSON.parse(r.decode_transaction(o.transaction_hex)),l=ht(o.block_height,o.current_height){F(u,m,g,!1,i.txtype)});const f=e.miner_earnings??[];if(f.length>0){const m=s("transaction-miner-earnings");f.forEach((g,v)=>{F(m,`earning_${v+1}_type`,g.type),F(m,`earning_${v+1}_asset`,g.asset),F(m,`earning_${v+1}_nft_series`,g.nft_series),F(m,`earning_${v+1}_amount`,`${re(g.amount_atomic)} ${g.asset.trim()}`)}),s("miner-earnings-section").hidden=!1}const p=s("transaction-technical-fields");i.stored_hash&&F(p,"stored_hash",i.stored_hash,!0),i.signature&&F(p,"signature",i.signature,!0),i.signature1&&F(p,"signature_1",i.signature1,!0),i.signature2&&F(p,"signature_2",i.signature2,!0),F(p,"raw_transaction_bytes",o.transaction_hex,!0)}catch(o){s("transaction-title").textContent="Transaction unavailable";const r=s("transaction-fields");r.replaceChildren(),F(r,"error",q(o))}}function ue(e){const t=String(e??"").trim();return t.length>18?`${t.slice(0,8)}...${t.slice(-7)}`:t}function ar(e,t){const n=e.fields,o=t.trim().toLowerCase(),r=o.endsWith(".cltc")?"CLTC":"CLC",i=c=>String(n[c]??"").trim(),a=(c,d,u="")=>`${u}${re(n[c])} ${d}`,l=()=>`-${re(n.txfee??0)} ${r}`;switch(e.txtype){case 1:return{title:"Mining reward",subtitle:"From network",amount:a("value",r,"+"),direction:"incoming"};case 2:{const c=i("coin"),d=String(n.sender??"").trim().toLowerCase()===o;return{title:d?`Sent ${c}`:`Received ${c}`,subtitle:d?`To: ${ue(n.receiver)}`:`From: ${ue(n.sender)}`,amount:a("value",c,d?"-":"+"),direction:d?"outgoing":"incoming"}}case 3:{const c=i("ticker");return{title:`Created ${c}`,subtitle:"Token creation",amount:a("number",c,"+"),secondary:l(),direction:"incoming"}}case 4:{const c=String(n.count??"1");return{title:`Created ${i("nft_name")}`,subtitle:Number(n.series??0)===0?"NFT/RWA":"NFT/RWA collection",amount:`+${c} ${Number(c)===1?"item":"items"}`,secondary:l(),direction:"incoming"}}case 6:{const c=String(n.sender1??"").trim().toLowerCase()===o,d=i(c?"ticker1":"ticker2"),u=i(c?"ticker2":"ticker1");return{title:"Completed swap",subtitle:`With: ${ue(n[c?"sender2":"sender1"])}`,amount:a(c?"value1":"value2",d,"-"),secondary:a(c?"value2":"value1",u,"+"),direction:"neutral"}}case 7:{const c=String(n.lender??"").trim().toLowerCase()===o;return{title:c?"Loan funded":"Loan received",subtitle:c?`To: ${ue(n.borrower)}`:`From: ${ue(n.lender)}`,amount:a("loan_amount",i("loan_coin"),c?"-":"+"),direction:c?"outgoing":"incoming"}}case 8:return{title:"Loan payment",subtitle:`Contract: ${ue(n.contract_hash)}`,amount:`-${re(n.payback_amount)}`,direction:"outgoing"};case 10:return{title:`Burned ${i("coin")}`,subtitle:"Removed from supply",amount:a("value",i("coin"),"-"),direction:"outgoing"};case 11:{const c=i("ticker");return{title:`Issued ${c}`,subtitle:"Additional token supply",amount:a("number",c,"+"),secondary:l(),direction:"incoming"}}case 12:return{title:"Created vanity address",subtitle:String(n.vanity_address??"").trim(),amount:l(),direction:"outgoing"};default:return{title:e.transaction_name,subtitle:"Contractless transaction",amount:l(),direction:"outgoing"}}}async function sr(e,t){const n=s("activity-list");if(n.replaceChildren(),s("activity-count").textContent=`${e.length} shown`,e.length===0){const r=document.createElement("div");r.className="activity-empty";const i=document.createElement("strong");i.textContent="No wallet activity found";const a=document.createElement("span");a.textContent="New transactions will appear here after they reach the selected API.",r.append(i,a),n.append(r);return}const o=await Yt();e.forEach(r=>{let i;try{i=JSON.parse(o.decode_transaction(r.transaction_hex))}catch{const E=Number.parseInt(r.transaction_hex.slice(0,2),16);i={txtype:E,transaction_name:zn[E]??`Transaction Type ${E}`,fields:{}}}const a=ar(i,(h==null?void 0:h.address)??""),l=cn(r.transaction_hex),d=ht(r.block_height,t){un(r)}),n.append(u)})}async function fn(){if(!(h!=null&&h.unlocked))return;s("activity-count").textContent="Loading";const e=s("activity-list");e.replaceChildren();const t=document.createElement("div");t.className="activity-empty",t.textContent="Loading recent activity...",e.append(t);try{const n=await $("wallet-activity");await sr(Array.isArray(n.transactions)?n.transactions:[],Number(n.current_height??0))}catch(n){e.replaceChildren();const o=document.createElement("div");o.className="activity-empty activity-error",o.textContent=q(n),e.append(o),s("activity-count").textContent="Unavailable"}}function q(e){return e instanceof Error?e.message:String(e).replace(/^JsValue\((.*)\)$/,"$1")}async function U(e,t=!1){M.textContent="",t&&I("loading");try{await e()}catch(n){throw M.textContent=q(n),n}}function Q(e,t){if(!e.trim())throw new Error(t);return e}function cr(e){let t="";for(let o=0;oURL.revokeObjectURL(r),1e3)}async function pn(e,t){h=e,fe=t,ie=String(await $("export-private-key-image",{password:t,imagePassword:t})),s("wallet-image-preview").src=`data:image/png;base64,${ie}`;const n=s("backup-confirmed");n.disabled=!0,n.checked=!1,s("open-wallet").disabled=!0,I("backup-screen")}document.querySelectorAll(".navigation").forEach(e=>{e.addEventListener("click",()=>I(String(e.dataset.screen)))});s("account-menu-button").addEventListener("click",()=>G(!0));s("close-account-drawer").addEventListener("click",()=>G(!1));s("drawer-backdrop").addEventListener("click",()=>G(!1));function gn(e){G(!1),s("cancel-wallet-add").hidden=!1,I(e)}s("menu-create-wallet").addEventListener("click",()=>{gn("create-screen")});s("menu-import-wallet").addEventListener("click",()=>{gn("image-screen")});s("cancel-wallet-add").addEventListener("click",()=>{s("cancel-wallet-add").hidden=!0,h&&V(h)});s("menu-settings").addEventListener("click",()=>{G(!1),gt(),h&&(s("settings-api-url").value=h.apiUrl,s("settings-lock-timeout").value=String(h.autoLockMinutes)),I("settings-screen")});s("close-settings").addEventListener("click",()=>{gt(),h&&V(h)});s("drawer-lock-wallet").addEventListener("click",()=>{U(async()=>{G(!1),fe="",ie="",V(await $("lock"))})});s("open-receive").addEventListener("click",()=>{U(Xn)});s("open-send").addEventListener("click",()=>{U(async()=>tr())});s("close-send").addEventListener("click",()=>{Y=void 0,P?(xe(P),I("nft-detail-screen")):h&&V(h)});s("send-asset").addEventListener("change",Re);s("send-amount").addEventListener("input",Re);s("send-form").addEventListener("submit",e=>{e.preventDefault(),U(rr)});s("send-form").addEventListener("keydown",e=>{!(e instanceof KeyboardEvent)||e.key!=="Enter"||e.target instanceof HTMLInputElement&&(e.preventDefault(),s("send-form").requestSubmit())});s("edit-send").addEventListener("click",()=>{I("send-screen")});s("sign-broadcast-transfer").addEventListener("click",()=>{U(async()=>{if(!Y)throw new Error("The reviewed transfer is unavailable.");we=(await $("wallet-broadcast-transfer",{transaction:Y.transaction})).txid,s("send-result-txid").textContent=we,Y=void 0,I("send-success-screen")})});s("copy-send-txid").addEventListener("click",()=>{we&&navigator.clipboard.writeText(we).then(()=>{M.textContent="Transaction ID copied."}).catch(e=>{M.textContent=q(e)})});s("finish-send").addEventListener("click",()=>{we="";const e=!!P;P=void 0,h&&(Be(),e?(Z=void 0,I("nfts-screen"),pt(!0)):V(h))});s("open-activity").addEventListener("click",()=>{I("activity-screen"),fn()});s("open-nfts").addEventListener("click",()=>{K=1,Z=void 0,I("nfts-screen"),pt(!0)});s("close-nfts").addEventListener("click",()=>{ae+=1,h&&V(h)});s("refresh-nfts").addEventListener("click",()=>{pt(!0)});s("close-nft-detail").addEventListener("click",()=>{Z=void 0,I("nfts-screen")});s("transfer-nft").addEventListener("click",()=>{U(async()=>{if(!Z)throw new Error("No NFT or RWA is selected.");nr(Z)})});s("nfts-previous").addEventListener("click",()=>{if(K<=1)return;K-=1;const e=++ae;Ce(),mt(e)});s("nfts-next").addEventListener("click",()=>{const e=Math.max(1,Math.ceil(J.length/ve));if(K>=e)return;K+=1;const t=++ae;Ce(),mt(t)});s("close-activity").addEventListener("click",()=>{h&&V(h)});s("close-transaction").addEventListener("click",()=>{en==="nft"&&Z?(xe(Z),I("nft-detail-screen")):I("activity-screen")});s("copy-transaction-txid").addEventListener("click",()=>{const e=_e==null?void 0:_e.txid;e&&navigator.clipboard.writeText(e).then(()=>{M.textContent="Transaction ID copied."}).catch(t=>{M.textContent=q(t)})});s("refresh-activity").addEventListener("click",()=>{fn()});s("refresh-balances").addEventListener("click",()=>{Be()});document.querySelectorAll("[data-token-filter]").forEach(e=>{e.addEventListener("click",()=>{Te=e.dataset.tokenFilter==="all"?"all":"held",document.querySelectorAll("[data-token-filter]").forEach(t=>{t.classList.toggle("active",t===e)}),st()})});s("close-receive").addEventListener("click",()=>{h&&V(h)});s("copy-receive-address").addEventListener("click",()=>{var t;const e=(t=s("receive-address-value").textContent)==null?void 0:t.trim();if(!e){M.textContent="No active wallet address is available.";return}navigator.clipboard.writeText(e).then(()=>{M.textContent="Wallet address copied."}).catch(n=>{M.textContent=q(n)})});s("save-wallet-settings").addEventListener("click",()=>{U(async()=>{const e=s("settings-api-url").value.replace(/\/+$/,""),t=new URL(e);if(t.protocol!=="https:"&&t.hostname!=="localhost"&&t.hostname!=="127.0.0.1")throw new Error("Contractless API URLs must use HTTPS.");const n=`${t.origin}/*`;if(!await chrome.permissions.contains({origins:[n]})&&!await chrome.permissions.request({origins:[n]}))throw new Error("Permission to contact this API endpoint was denied.");h=await $("save-settings",{apiUrl:e,autoLockMinutes:s("settings-lock-timeout").value}),M.textContent="Settings saved."})});document.querySelectorAll("[data-backup-action]").forEach(e=>{e.addEventListener("click",()=>{dt=e.dataset.backupAction,Ee(!0)})});s("close-backup-key").addEventListener("click",()=>Ee(!1));s("backup-key-backdrop").addEventListener("click",()=>Ee(!1));s("confirm-backup-key").addEventListener("click",()=>{const e=dt,t=s("backup-key-input").value;if(e){if(!t){M.textContent="Enter the active wallet encryption key.";return}U(async()=>{var r;if(e==="image"){const i=String(await $("export-private-key-image",{password:t,imagePassword:t})),a=((r=h==null?void 0:h.address)==null?void 0:r.split(".")[0])??"contractless";mn(i,`${a}.wallet.png`),Ee(!1),M.textContent="Wallet image downloaded.";return}const n=await $("reveal-private-key",{password:t});if(Ee(!1),e==="private"){s("revealed-private-key").value=n.privateKey,s("private-key-result").hidden=!1,s("private-key-result").scrollIntoView({block:"nearest"});return}const o=crypto.randomUUID();await chrome.storage.session.set({[`print-backup:${o}`]:{label:n.label,address:n.address,privateKey:n.privateKey,encryptionKey:t}}),await chrome.windows.create({url:chrome.runtime.getURL(`print.html?id=${encodeURIComponent(o)}`),type:"popup",width:820,height:760})})}});s("clear-private-key").addEventListener("click",gt);s("copy-private-key").addEventListener("click",()=>{const e=s("revealed-private-key").value;e&&navigator.clipboard.writeText(e).then(()=>{M.textContent="Private key copied."}).catch(t=>{M.textContent=q(t)})});document.querySelectorAll(".reveal").forEach(e=>{e.addEventListener("click",()=>{const t=s(String(e.dataset.input)),n=t.type==="text";t.type=n?"password":"text",e.textContent=n?"Show":"Hide"})});s("continue-create").addEventListener("click",()=>{try{Ne=Q(s("new-password").value,"Enter an encryption key."),s("verify-password").value="",I("verify-screen")}catch(e){M.textContent=q(e)}});s("create-wallet").addEventListener("click",()=>{if(s("verify-password").value!==Ne){M.textContent="The encryption keys do not match.";return}U(async()=>{const t=Ne,n=await $("create-wallet",{password:t});Ne="",s("new-password").value="",s("verify-password").value="",await pn(n,t)},!0).catch(()=>I("verify-screen",!1))});const ct=s("wallet-image");ct.addEventListener("change",()=>{var t;const e=(t=ct.files)==null?void 0:t[0];ge="",s("image-file-name").textContent=(e==null?void 0:e.name)??"Choose wallet image",e&&e.arrayBuffer().then(n=>{ge=cr(new Uint8Array(n))}).catch(n=>{M.textContent=q(n)})});s("import-image").addEventListener("click",()=>{const e=s("image-password").value;try{Q(e,"Enter the wallet image encryption key."),Q(ge,"Choose a wallet image.")}catch(t){M.textContent=q(t);return}U(async()=>{const t=await $("import-wallet-image",{imageBase64:ge,imagePassword:e,password:e});fe=e,ge="",ct.value="",s("image-password").value="",V(t)},!0).catch(()=>I("image-screen",!1))});s("import-private").addEventListener("click",()=>{const e=s("private-key").value.trim(),t=s("private-password").value;try{Q(e,"Paste the private key."),Q(t,"Enter a new encryption key.")}catch(n){M.textContent=q(n);return}U(async()=>{const n=await $("import-private-key",{privateKey:e,password:t});s("private-key").value="",s("private-password").value="",await pn(n,t)},!0).catch(()=>I("private-screen",!1))});s("save-image").addEventListener("click",()=>{U(async()=>{var n;if(!ie)throw new Error("The wallet image is not ready.");const e=((n=h==null?void 0:h.address)==null?void 0:n.split(".")[0])??"contractless";mn(ie,`${e}.wallet.png`);const t=s("backup-confirmed");t.disabled=!1,t.checked=!1,s("open-wallet").disabled=!0})});s("backup-confirmed").addEventListener("change",e=>{s("open-wallet").disabled=!e.currentTarget.checked});s("open-wallet").addEventListener("click",()=>{fe="",ie="",s("wallet-image-preview").removeAttribute("src"),h&&V(h)});s("unlock-form").addEventListener("submit",e=>{e.preventDefault();const t=s("unlock-password").value;try{Q(t,"Enter your encryption key.")}catch(n){M.textContent=q(n);return}U(async()=>{const n=await $("unlock",{password:t});fe=t,s("unlock-password").value="",V(n)},!0).catch(()=>I("locked-screen",!1))});s("approve-provider-approval").addEventListener("click",()=>{oe&&(ye("approve"),U(async()=>{const e=await $("resolve-provider-approval",{id:oe,approved:!0});oe="",await tn(e.remaining??0,!0)}).catch(()=>ye(null)))});s("reject-provider-approval").addEventListener("click",()=>{oe&&(ye("reject"),U(async()=>{const e=await $("resolve-provider-approval",{id:oe,approved:!1});oe="",await tn(e.remaining??0,!1)}).catch(()=>ye(null)))});$("status").then(e=>V(e)).catch(e=>{I("create-screen"),M.textContent=q(e)}); diff --git a/extension/dist/print.html b/extension/dist/print.html new file mode 100644 index 0000000..7eeccfa --- /dev/null +++ b/extension/dist/print.html @@ -0,0 +1,44 @@ + + + + + + Contractless Wallet Backup + + + + + +
+ +
+

Contractless Wallet Backup

+

Private recovery information

+
+
+ +

+ Keep this page private. Anyone with the private key and encryption key can + control this wallet. +

+ +
+

Wallet

+

+    
+
+

Address

+

+    
+
+

Private Key

+

+    
+
+

Encryption Key

+

+    
+ + + + diff --git a/extension/dist/print.js b/extension/dist/print.js new file mode 100644 index 0000000..c86b6e5 --- /dev/null +++ b/extension/dist/print.js @@ -0,0 +1 @@ +import"./chunks/modulepreload-polyfill-B5Qt9EMX.js";async function a(){const e=new URLSearchParams(location.search).get("id");if(!e)throw new Error("Missing printable wallet backup.");const n=`print-backup:${e}`,o=await chrome.storage.session.get(n);await chrome.storage.session.remove(n);const t=o[n];if(!t)throw new Error("The printable wallet backup has expired.");document.getElementById("wallet-label").textContent=t.label,document.getElementById("wallet-address").textContent=t.address,document.getElementById("wallet-private-key").textContent=t.privateKey,document.getElementById("wallet-encryption-key").textContent=t.encryptionKey,setTimeout(()=>{window.focus(),window.print()},150)}a().catch(e=>{document.getElementById("print-error").textContent=e instanceof Error?e.message:String(e)}); diff --git a/extension/dist/provider.js b/extension/dist/provider.js new file mode 100644 index 0000000..47444f0 --- /dev/null +++ b/extension/dist/provider.js @@ -0,0 +1 @@ +const a=new Map;window.addEventListener("message",e=>{var s,t,o;if(e.source!==window||((s=e.data)==null?void 0:s.source)!=="contractless-wallet")return;const r=a.get(e.data.id);r&&(a.delete(e.data.id),(t=e.data.response)!=null&&t.ok?r.resolve(e.data.response.result):r.reject(new Error(((o=e.data.response)==null?void 0:o.error)??"Wallet request failed.")))});const n=Object.freeze({isContractless:!0,request({method:e,params:r}){const s=crypto.randomUUID();return new Promise((t,o)=>{a.set(s,{resolve:t,reject:o}),window.postMessage({source:"contractless-page",id:s,method:e,params:r},window.location.origin)})}});Object.defineProperty(window,"contractless",{value:n,writable:!1,configurable:!1}); diff --git a/extension/package.json b/extension/package.json new file mode 100644 index 0000000..17df9f1 --- /dev/null +++ b/extension/package.json @@ -0,0 +1,29 @@ +{ + "name": "contractless-browser-wallet", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "build:wasm:testnet": "wasm-pack build ../core --target web --out-dir ../extension/src/wasm/testnet --out-name contractless_browser_core --no-default-features --features testnet", + "build:wasm:mainnet": "wasm-pack build ../core --target web --out-dir ../extension/src/wasm/mainnet --out-name contractless_browser_core --no-default-features --features mainnet", + "build:wasm": "pnpm run build:wasm:testnet && pnpm run build:wasm:mainnet", + "build:chrome": "pnpm run build:wasm && vite build --mode chrome", + "build:firefox": "pnpm run build:wasm && vite build --mode firefox", + "build": "pnpm run build:wasm && vite build --mode chrome && vite build --mode firefox", + "check": "tsc --noEmit", + "lint:firefox": "web-ext lint --source-dir dist/firefox", + "package:firefox": "web-ext build --source-dir dist/firefox --artifacts-dir web-ext-artifacts --overwrite-dest" + }, + "devDependencies": { + "@types/chrome": "^0.0.326", + "@types/node": "^22.15.3", + "@types/qrcode": "^1.5.6", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vite-plugin-static-copy": "^2.3.1", + "web-ext": "^10.5.0" + }, + "dependencies": { + "qrcode": "^1.5.4" + } +} diff --git a/extension/pnpm-lock.yaml b/extension/pnpm-lock.yaml new file mode 100644 index 0000000..e4a9b24 --- /dev/null +++ b/extension/pnpm-lock.yaml @@ -0,0 +1,3418 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + devDependencies: + '@types/chrome': + specifier: ^0.0.326 + version: 0.0.326 + '@types/node': + specifier: ^22.15.3 + version: 22.20.1 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^6.3.5 + version: 6.4.3(@types/node@22.20.1) + vite-plugin-static-copy: + specifier: ^2.3.1 + version: 2.3.2(vite@6.4.3(@types/node@22.20.1)) + web-ext: + specifier: ^10.5.0 + version: 10.5.0 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@8.0.0': + resolution: {integrity: sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==} + + '@devicefarmer/adbkit-logcat@2.1.3': + resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} + engines: {node: '>= 4'} + + '@devicefarmer/adbkit-monkey@1.2.1': + resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} + engines: {node: '>= 0.10.4'} + + '@devicefarmer/adbkit@3.3.8': + resolution: {integrity: sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==} + engines: {node: '>= 0.10.4'} + hasBin: true + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@fluent/syntax@0.19.0': + resolution: {integrity: sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==} + engines: {node: '>=14.0.0', npm: '>=7.0.0'} + + '@fregante/relaxed-json@2.0.0': + resolution: {integrity: sha512-PyUXQWB42s4jBli435TDiYuVsadwRHnMc27YaLouINktvTWsL3FcKrRMGawTayFk46X+n5bE23RjUTWQwrukWw==} + engines: {node: '>= 0.10.0'} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@mdn/browser-compat-data@8.0.4': + resolution: {integrity: sha512-up6DsNsaPt3jq5d2TGx4UOXIqBKU0D86dxWwcVmmOOEYqP1rFaV1XcAcwHxMvb6bbtMfY5JpjUCszzy/aVc4yA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@3.0.3': + resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} + engines: {node: '>=12'} + + '@rollup/rollup-android-arm-eabi@4.62.3': + resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.3': + resolution: {integrity: sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.3': + resolution: {integrity: sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.3': + resolution: {integrity: sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.3': + resolution: {integrity: sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.3': + resolution: {integrity: sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + resolution: {integrity: sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + resolution: {integrity: sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.3': + resolution: {integrity: sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.3': + resolution: {integrity: sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.3': + resolution: {integrity: sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.3': + resolution: {integrity: sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + resolution: {integrity: sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.3': + resolution: {integrity: sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + resolution: {integrity: sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.3': + resolution: {integrity: sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.3': + resolution: {integrity: sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.3': + resolution: {integrity: sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.3': + resolution: {integrity: sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.3': + resolution: {integrity: sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.3': + resolution: {integrity: sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.3': + resolution: {integrity: sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.3': + resolution: {integrity: sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.3': + resolution: {integrity: sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.3': + resolution: {integrity: sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==} + cpu: [x64] + os: [win32] + + '@types/chrome@0.0.326': + resolution: {integrity: sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/har-format@1.2.16': + resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + addons-linter@10.8.0: + resolution: {integrity: sha512-AotpGWlp5FrmUIH8kjyYjQvCmthnSqPLpw7kOEl7DfoAWEndOCV/ONuekAVM8QObiei802RyyIClEaekA51n5g==} + engines: {node: '>=20.0.0'} + hasBin: true + + addons-moz-compare@1.3.0: + resolution: {integrity: sha512-/rXpQeaY0nOKhNx00pmZXdk5Mu+KhVlL3/pSBuAYwrxRrNiTvI/9xfQI8Lmm7DMMl+PDhtfAHY/0ibTpdeoQQQ==} + + addons-scanner-utils@15.4.0: + resolution: {integrity: sha512-i3PnJx9YLZi96o4t4JWArP6JimC01949YVAcMJKHfyQ5qmUBaH21qNS8f12/RsloejW8IcCEljhh3Fq3gxFNsg==} + peerDependencies: + express: 5.2.1 + safe-compare: 1.1.4 + peerDependenciesMeta: + express: + optional: true + safe-compare: + optional: true + + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} + engines: {node: '>=12.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-differ@4.0.0: + resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + array-union@3.0.1: + resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + atomically@2.1.1: + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chrome-launcher@1.2.0: + resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} + engines: {node: '>=12.13.0'} + hasBin: true + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + columnify@1.6.0: + resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==} + engines: {node: '>=8.0.0'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@7.1.0: + resolution: {integrity: sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==} + engines: {node: '>=18'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@6.0.1: + resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-no-unsanitized@4.1.5: + resolution: {integrity: sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + firefox-profile@4.7.0: + resolution: {integrity: sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ==} + engines: {node: '>=18'} + hasBin: true + + first-chunk-stream@3.0.0: + resolution: {integrity: sha512-LNRvR4hr/S8cXXkIY5pTgVP7L3tq6LlYWcg9nWBuW7o1NMxKZo6oOVa/6GIekMGI0Iw7uC+HWimMe9u/VAeKqw==} + engines: {node: '>=8'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fx-runner@1.5.0: + resolution: {integrity: sha512-EstfdRQu04tM+SXR3pBmc0+GYKZj9FMHAvA8XAbKPzTvUeHP0v1/F3aQ10bG7EluR6phvFqsvn5Z7SvYJsEgNw==} + hasBin: true + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ci@1.0.0: + resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} + engines: {node: '>=18'} + hasBin: true + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + + is-npm@6.1.0: + resolution: {integrity: sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-merge-patch@1.0.2: + resolution: {integrity: sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + ky@1.14.3: + resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} + engines: {node: '>=18'} + + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multimatch@6.0.0: + resolution: {integrity: sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@7.0.6: + resolution: {integrity: sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==} + engines: {node: '>=18'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss@8.5.24: + resolution: {integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + promise-toolbox@0.21.0: + resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==} + engines: {node: '>=6'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pupa@3.3.0: + resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} + engines: {node: '>=12.20'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.62.3: + resolution: {integrity: sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom-buf@2.0.0: + resolution: {integrity: sha512-gLFNHucd6gzb8jMsl5QmZ3QgnUJmp7qn4uUSHNwEXumAp7YizoGYw19ZUVfuq4aBOQUtyn2k8X/CwzWB73W2lQ==} + engines: {node: '>=8'} + + strip-bom-stream@4.0.0: + resolution: {integrity: sha512-0ApK3iAkHv6WbgLICw/J4nhwHeDZsBxIIsOD+gHgZICL6SeJ0S9f/WZqemka9cjkTyMN5geId6e8U5WGFAn3cQ==} + engines: {node: '>=8'} + + strip-bom@5.0.0: + resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + upath@3.0.7: + resolution: {integrity: sha512-VjBBquch25nUGMuVBpOb2Cj3gc8Kb7lJBqbsXR/0anZ/5uJsL14Kpth9JKfnBsckxCfgIp6hPvcvvmZ97R9X7g==} + engines: {node: '>=20'} + + update-notifier@7.3.1: + resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} + engines: {node: '>=18'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-plugin-static-copy@2.3.2: + resolution: {integrity: sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-ext@10.5.0: + resolution: {integrity: sha512-tveUHhZHu5A2Qrs7u+hbh2S+7ZPuKVH/ILzqkRwpdivFOnVmgPhDLtmtGNKT3a8N9PZbW5uP6INe6Byj5qePVQ==} + engines: {node: '>=20.0.0', npm: '>=8.0.0'} + hasBin: true + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + winreg@0.0.12: + resolution: {integrity: sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zip-dir@2.0.0: + resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/runtime@8.0.0': {} + + '@devicefarmer/adbkit-logcat@2.1.3': {} + + '@devicefarmer/adbkit-monkey@1.2.1': {} + + '@devicefarmer/adbkit@3.3.8': + dependencies: + '@devicefarmer/adbkit-logcat': 2.1.3 + '@devicefarmer/adbkit-monkey': 1.2.1 + bluebird: 3.7.2 + commander: 9.5.0 + debug: 4.3.7 + node-forge: 1.4.0 + split: 1.0.1 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@fluent/syntax@0.19.0': {} + + '@fregante/relaxed-json@2.0.0': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@mdn/browser-compat-data@8.0.4': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@pinojs/redact@0.4.0': {} + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@3.0.3': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@rollup/rollup-android-arm-eabi@4.62.3': + optional: true + + '@rollup/rollup-android-arm64@4.62.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.3': + optional: true + + '@rollup/rollup-darwin-x64@4.62.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.3': + optional: true + + '@types/chrome@0.0.326': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + + '@types/estree@1.0.9': {} + + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/har-format@1.2.16': {} + + '@types/json-schema@7.0.15': {} + + '@types/minimatch@3.0.5': {} + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 22.20.1 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + addons-linter@10.8.0: + dependencies: + '@fluent/syntax': 0.19.0 + '@fregante/relaxed-json': 2.0.0 + '@mdn/browser-compat-data': 8.0.4 + addons-moz-compare: 1.3.0 + addons-scanner-utils: 15.4.0 + ajv: 8.20.0 + cheerio: 1.2.0 + columnify: 1.6.0 + common-tags: 1.8.2 + css-tree: 3.2.1 + deepmerge: 4.3.1 + eslint: 9.39.4 + eslint-plugin-no-unsanitized: 4.1.5(eslint@9.39.4) + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esprima: 4.0.1 + fast-json-patch: 3.1.1 + image-size: 2.0.2 + json-merge-patch: 1.0.2 + pino: 10.3.1 + semver: 7.8.5 + source-map-support: 0.5.21 + upath: 3.0.7 + yargs: 17.7.2 + yauzl: 3.4.0 + transitivePeerDependencies: + - express + - jiti + - safe-compare + - supports-color + + addons-moz-compare@1.3.0: {} + + addons-scanner-utils@15.4.0: + dependencies: + common-tags: 1.8.2 + first-chunk-stream: 3.0.0 + jsonwebtoken: 9.0.3 + strip-bom-stream: 4.0.0 + upath: 3.0.7 + yauzl: 3.4.0 + + adm-zip@0.5.18: {} + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@2.0.1: {} + + array-differ@4.0.0: {} + + array-union@3.0.1: {} + + async@3.2.6: {} + + atomic-sleep@1.0.0: {} + + atomically@2.1.1: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.6.2 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.41.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.2 + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@8.0.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.29.0 + whatwg-mimetype: 4.0.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chrome-launcher@1.2.0: + dependencies: + '@types/node': 22.20.1 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + + cli-boxes@3.0.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + columnify@1.6.0: + dependencies: + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + commander@12.1.0: {} + + commander@9.5.0: {} + + common-tags@1.8.2: {} + + concat-map@0.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@7.1.0: + dependencies: + atomically: 2.1.1 + dot-prop: 9.0.0 + graceful-fs: 4.2.11 + xdg-basedir: 5.1.0 + + core-util-is@1.0.3: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + debounce@1.2.1: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decamelize@6.0.1: {} + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@3.0.0: {} + + dijkstrajs@1.0.3: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@9.0.0: + dependencies: + type-fest: 4.41.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + es6-error@4.1.1: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + escalade@3.2.0: {} + + escape-goat@4.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-no-unsanitized@4.1.5(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-patch@3.1.1: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.5: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + firefox-profile@4.7.0: + dependencies: + adm-zip: 0.5.18 + fs-extra: 11.4.0 + ini: 4.1.3 + minimist: 1.2.8 + xml2js: 0.6.2 + + first-chunk-stream@3.0.0: {} + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + fx-runner@1.5.0: + dependencies: + commander: 12.1.0 + shell-quote: 1.8.4 + which: 4.0.0 + winreg: 0.0.12 + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + globals@14.0.0: {} + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + image-size@2.0.2: {} + + immediate@3.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + index-to-position@1.2.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + ini@4.1.3: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ci@1.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + + is-npm@6.1.0: {} + + is-number@7.0.0: {} + + is-path-inside@4.0.0: {} + + is-utf8@0.2.1: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + jose@5.9.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-merge-patch@1.0.2: + dependencies: + fast-deep-equal: 3.1.3 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + ky@1.14.3: {} + + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.3 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + make-error@1.3.6: {} + + marky@1.3.0: {} + + mdn-data@2.27.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + multimatch@6.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 4.0.0 + array-union: 3.0.1 + minimatch: 3.1.5 + + nanoid@3.3.16: {} + + natural-compare@1.4.0: {} + + node-forge@1.4.0: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + on-exit-leak-free@2.1.2: {} + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@7.0.6: {} + + p-try@2.2.0: {} + + package-json@10.0.1: + dependencies: + ky: 1.14.3 + registry-auth-token: 5.1.1 + registry-url: 6.0.1 + semver: 7.8.5 + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pend@1.2.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + pngjs@5.0.0: {} + + postcss@8.5.24: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + process-nextick-args@2.0.1: {} + + process-warning@5.1.0: {} + + promise-toolbox@0.21.0: + dependencies: + make-error: 1.3.6 + + proto-list@1.2.4: {} + + punycode@2.3.1: {} + + pupa@3.3.0: + dependencies: + escape-goat: 4.0.0 + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + registry-auth-token@5.1.1: + dependencies: + '@pnpm/npm-conf': 3.0.3 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rollup@4.62.3: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.3 + '@rollup/rollup-android-arm64': 4.62.3 + '@rollup/rollup-darwin-arm64': 4.62.3 + '@rollup/rollup-darwin-x64': 4.62.3 + '@rollup/rollup-freebsd-arm64': 4.62.3 + '@rollup/rollup-freebsd-x64': 4.62.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.3 + '@rollup/rollup-linux-arm-musleabihf': 4.62.3 + '@rollup/rollup-linux-arm64-gnu': 4.62.3 + '@rollup/rollup-linux-arm64-musl': 4.62.3 + '@rollup/rollup-linux-loong64-gnu': 4.62.3 + '@rollup/rollup-linux-loong64-musl': 4.62.3 + '@rollup/rollup-linux-ppc64-gnu': 4.62.3 + '@rollup/rollup-linux-ppc64-musl': 4.62.3 + '@rollup/rollup-linux-riscv64-gnu': 4.62.3 + '@rollup/rollup-linux-riscv64-musl': 4.62.3 + '@rollup/rollup-linux-s390x-gnu': 4.62.3 + '@rollup/rollup-linux-x64-gnu': 4.62.3 + '@rollup/rollup-linux-x64-musl': 4.62.3 + '@rollup/rollup-openbsd-x64': 4.62.3 + '@rollup/rollup-openharmony-arm64': 4.62.3 + '@rollup/rollup-win32-arm64-msvc': 4.62.3 + '@rollup/rollup-win32-ia32-msvc': 4.62.3 + '@rollup/rollup-win32-x64-gnu': 4.62.3 + '@rollup/rollup-win32-x64-msvc': 4.62.3 + fsevents: 2.3.3 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sax@1.6.1: {} + + semver@7.8.5: {} + + set-blocking@2.0.0: {} + + setimmediate@1.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.4: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + split@1.0.1: + dependencies: + through: 2.3.8 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom-buf@2.0.0: + dependencies: + is-utf8: 0.2.1 + + strip-bom-stream@4.0.0: + dependencies: + first-chunk-stream: 3.0.0 + strip-bom-buf: 2.0.0 + + strip-bom@5.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.3: {} + + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + through@2.3.8: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tmp@0.2.7: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici@7.29.0: {} + + universalify@2.0.1: {} + + upath@3.0.7: {} + + update-notifier@7.3.1: + dependencies: + boxen: 8.0.1 + chalk: 5.6.2 + configstore: 7.1.0 + is-in-ci: 1.0.0 + is-installed-globally: 1.0.0 + is-npm: 6.1.0 + latest-version: 9.0.0 + pupa: 3.3.0 + semver: 7.8.5 + xdg-basedir: 5.1.0 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite-plugin-static-copy@2.3.2(vite@6.4.3(@types/node@22.20.1)): + dependencies: + chokidar: 3.6.0 + fast-glob: 3.3.3 + fs-extra: 11.4.0 + p-map: 7.0.6 + picocolors: 1.1.1 + vite: 6.4.3(@types/node@22.20.1) + + vite@6.4.3(@types/node@22.20.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.24 + rollup: 4.62.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.20.1 + fsevents: 2.3.3 + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-ext@10.5.0: + dependencies: + '@babel/runtime': 8.0.0 + '@devicefarmer/adbkit': 3.3.8 + addons-linter: 10.8.0 + camelcase: 8.0.0 + chrome-launcher: 1.2.0 + debounce: 1.2.1 + decamelize: 6.0.1 + es6-error: 4.1.1 + firefox-profile: 4.7.0 + fx-runner: 1.5.0 + https-proxy-agent: 7.0.6 + jose: 5.9.6 + jszip: 3.10.1 + multimatch: 6.0.0 + open: 11.0.0 + parse-json: 8.3.0 + pino: 10.3.1 + promise-toolbox: 0.21.0 + source-map-support: 0.5.21 + strip-bom: 5.0.0 + strip-json-comments: 5.0.3 + tmp: 0.2.7 + update-notifier: 7.3.1 + watchpack: 2.5.2 + yargs: 17.7.2 + zip-dir: 2.0.0 + transitivePeerDependencies: + - express + - jiti + - safe-compare + - supports-color + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + when-exit@2.1.5: {} + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + winreg@0.0.12: {} + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xdg-basedir@5.1.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.6.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + + yocto-queue@0.1.0: {} + + zip-dir@2.0.0: + dependencies: + async: 3.2.6 + jszip: 3.10.1 diff --git a/extension/pnpm-workspace.yaml b/extension/pnpm-workspace.yaml new file mode 100644 index 0000000..09a02ca --- /dev/null +++ b/extension/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + esbuild: true +onlyBuiltDependencies: + - esbuild diff --git a/extension/src/assets/contractless-logo-128.png b/extension/src/assets/contractless-logo-128.png new file mode 100644 index 0000000..86681b0 Binary files /dev/null and b/extension/src/assets/contractless-logo-128.png differ diff --git a/extension/src/assets/contractless-logo-16.png b/extension/src/assets/contractless-logo-16.png new file mode 100644 index 0000000..6d69407 Binary files /dev/null and b/extension/src/assets/contractless-logo-16.png differ diff --git a/extension/src/assets/contractless-logo-32.png b/extension/src/assets/contractless-logo-32.png new file mode 100644 index 0000000..9d949e9 Binary files /dev/null and b/extension/src/assets/contractless-logo-32.png differ diff --git a/extension/src/assets/contractless-logo-48.png b/extension/src/assets/contractless-logo-48.png new file mode 100644 index 0000000..3697789 Binary files /dev/null and b/extension/src/assets/contractless-logo-48.png differ diff --git a/extension/src/assets/contractless-logo.png b/extension/src/assets/contractless-logo.png new file mode 100644 index 0000000..729e4d7 Binary files /dev/null and b/extension/src/assets/contractless-logo.png differ diff --git a/extension/src/background.ts b/extension/src/background.ts new file mode 100644 index 0000000..b668581 --- /dev/null +++ b/extension/src/background.ts @@ -0,0 +1,1655 @@ +import { core } from "./core"; +import type { ApprovalRecord, ContractlessNetwork, StoredSettings, WalletStatus } from "./types"; + +const WALLET_KEY = "contractlessWallet"; +const WALLETS_KEY = "contractlessWallets"; +const SETTINGS_KEY = "contractlessSettings"; +const LOCK_ALARM = "contractlessAutoLock"; +const UNLOCKED_SESSION_KEY = "contractlessUnlockedWallet"; +const APP_SESSIONS_KEY = "contractlessApplicationSessions"; +const APPROVAL_TIMEOUT_MS = 120_000; +const MAX_PENDING_APPROVALS = 10; +const DEFAULT_SETTINGS: StoredSettings = { + selectedNetwork: "testnet", + testnetApiUrl: "https://api.contractless.dev", + mainnetApiUrl: "", + apiUrl: "https://api.contractless.dev", + autoLockMinutes: 60 +}; + +type UnlockedWallet = { + walletId: string; + sessionId: string; + short_address: string; + public_key: string; + private_key: string; + network: ContractlessNetwork; + expiresAt: number; +}; + +let unlocked: UnlockedWallet | undefined; +const PENDING_APPROVAL_KEY = "pending-provider-approvals"; + +type ApplicationSession = { + origin: string; + appId: string; + accessKey: string; + walletId: string; + unlockSessionId: string; + createdAt: number; +}; + +type StoredWallet = { + id: string; + label: string; + address: string; + publicKey: string; + walletJson: string; + network: ContractlessNetwork; +}; + +type StoredWalletCollection = { + version: 2; + activeWalletIds: Partial>; + wallets: StoredWallet[]; +}; + +async function settings(): Promise { + const stored = await chrome.storage.local.get(SETTINGS_KEY); + const previous = (stored[SETTINGS_KEY] ?? {}) as Partial; + const selectedNetwork = previous.selectedNetwork === "mainnet" ? "mainnet" : "testnet"; + const testnetApiUrl = previous.testnetApiUrl ?? previous.apiUrl ?? DEFAULT_SETTINGS.testnetApiUrl; + const mainnetApiUrl = previous.mainnetApiUrl ?? ""; + return { + selectedNetwork, + testnetApiUrl, + mainnetApiUrl, + apiUrl: selectedNetwork === "mainnet" ? mainnetApiUrl : testnetApiUrl, + autoLockMinutes: previous.autoLockMinutes ?? DEFAULT_SETTINGS.autoLockMinutes + }; +} + +function requireApiUrl(configured: StoredSettings): string { + if (!configured.apiUrl) { + throw new Error(`No ${configured.selectedNetwork} API is configured.`); + } + return configured.apiUrl; +} + +function networkFromAddress(address: string): ContractlessNetwork { + if (address.endsWith(".cltc")) return "testnet"; + if (address.endsWith(".clc")) return "mainnet"; + throw new Error("The wallet address does not identify a supported Contractless network."); +} + +function walletMetadata(walletJson: string): { address: string; publicKey: string; network: ContractlessNetwork } { + const wallet = JSON.parse(walletJson) as { + short_address?: string; + public_key?: string; + }; + if (typeof wallet.short_address !== "string" || typeof wallet.public_key !== "string") { + throw new Error("The encrypted wallet metadata is invalid."); + } + return { + address: wallet.short_address, + publicKey: wallet.public_key, + network: networkFromAddress(wallet.short_address) + }; +} + +async function walletCollection(): Promise { + const stored = await chrome.storage.local.get([WALLETS_KEY, WALLET_KEY]); + const existing = stored[WALLETS_KEY] as ( + StoredWalletCollection + | { version: 1; activeWalletId?: string; wallets: Array> } + | undefined + ); + if (existing?.version === 2 && Array.isArray(existing.wallets)) { + return existing; + } + if (existing?.version === 1 && Array.isArray(existing.wallets)) { + const old = existing; + const wallets = old.wallets.map((wallet) => ({ + ...wallet, + network: networkFromAddress(wallet.address) + })); + const active = wallets.find((wallet) => wallet.id === old.activeWalletId); + const migrated: StoredWalletCollection = { + version: 2, + activeWalletIds: active ? { [active.network]: active.id } : {}, + wallets + }; + await saveWalletCollection(migrated); + return migrated; + } + + const legacyWallet = stored[WALLET_KEY]; + if (typeof legacyWallet !== "string") { + return { version: 2, activeWalletIds: {}, wallets: [] }; + } + + const metadata = walletMetadata(legacyWallet); + const migrated: StoredWallet = { + id: crypto.randomUUID(), + label: "Contractless Account 1", + address: metadata.address, + publicKey: metadata.publicKey, + walletJson: legacyWallet, + network: metadata.network + }; + const collection: StoredWalletCollection = { + version: 2, + activeWalletIds: { [metadata.network]: migrated.id }, + wallets: [migrated] + }; + await chrome.storage.local.set({ [WALLETS_KEY]: collection }); + await chrome.storage.local.remove(WALLET_KEY); + return collection; +} + +async function saveWalletCollection(collection: StoredWalletCollection): Promise { + await chrome.storage.local.set({ [WALLETS_KEY]: collection }); +} + +function activeStoredWallet( + collection: StoredWalletCollection, + network: ContractlessNetwork +): StoredWallet | undefined { + const candidates = collection.wallets.filter((wallet) => wallet.network === network); + return candidates.find((wallet) => wallet.id === collection.activeWalletIds[network]) + ?? candidates[0]; +} + +async function addWallet(walletJson: string): Promise { + const collection = await walletCollection(); + const metadata = walletMetadata(walletJson); + const duplicate = collection.wallets.find((wallet) => wallet.address === metadata.address); + + if (duplicate) { + duplicate.walletJson = walletJson; + duplicate.publicKey = metadata.publicKey; + collection.activeWalletIds[metadata.network] = duplicate.id; + await saveWalletCollection(collection); + return duplicate; + } + + const wallet: StoredWallet = { + id: crypto.randomUUID(), + label: `Contractless Account ${collection.wallets.filter((candidate) => candidate.network === metadata.network).length + 1}`, + address: metadata.address, + publicKey: metadata.publicKey, + walletJson, + network: metadata.network + }; + collection.wallets.push(wallet); + collection.activeWalletIds[metadata.network] = wallet.id; + await saveWalletCollection(collection); + return wallet; +} + +async function status(): Promise { + await restoreUnlockedWallet(); + const collection = await walletCollection(); + const configured = await settings(); + const active = activeStoredWallet(collection, configured.selectedNetwork); + const wallets = collection.wallets.filter((wallet) => wallet.network === configured.selectedNetwork); + return { + exists: wallets.length > 0, + unlocked: Boolean(unlocked && unlocked.walletId === active?.id && unlocked.network === configured.selectedNetwork), + activeWalletId: active?.id, + activeWalletLabel: active?.label, + address: unlocked?.short_address, + publicKey: unlocked?.public_key, + wallets: wallets.map((wallet) => ({ + id: wallet.id, + label: wallet.label, + address: wallet.address, + publicKey: wallet.publicKey, + network: wallet.network, + active: wallet.id === active?.id + })), + selectedNetwork: configured.selectedNetwork, + networkName: configured.selectedNetwork === "testnet" ? "Testnet" : "Mainnet", + networkSymbol: configured.selectedNetwork === "testnet" ? "CLTC" : "CLC", + testnetApiUrl: configured.testnetApiUrl, + mainnetApiUrl: configured.mainnetApiUrl, + apiUrl: configured.apiUrl, + autoLockMinutes: configured.autoLockMinutes + }; +} + +async function resetLockAlarm(): Promise { + if (!unlocked) return; + const configured = await settings(); + unlocked.expiresAt = Date.now() + (configured.autoLockMinutes * 60_000); + await chrome.storage.session.set({ [UNLOCKED_SESSION_KEY]: unlocked }); + await chrome.alarms.clear(LOCK_ALARM); + await chrome.alarms.create(LOCK_ALARM, { + when: unlocked.expiresAt + }); +} + +function arrayBufferDataUrl(buffer: ArrayBuffer, contentType: string): string { + const bytes = new Uint8Array(buffer); + let binary = ""; + const chunkSize = 0x8000; + for (let offset = 0; offset < bytes.length; offset += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); + } + return `data:${contentType};base64,${btoa(binary)}`; +} + +async function rememberUnlockedWallet( + wallet: Omit +): Promise { + await clearApplicationSessions(); + unlocked = { + ...wallet, + sessionId: crypto.randomUUID(), + expiresAt: 0 + }; + await resetLockAlarm(); +} + +async function registerWalletKeys( + wasm: Awaited>, + wallet: Pick +): Promise { + const registration = JSON.parse( + wasm.create_wallet_registration_from_keys( + wallet.short_address, + wallet.public_key, + wallet.private_key + ) + ); + const handshakeSignature = wasm.create_handshake_proof( + wallet.public_key, + wallet.private_key + ); + const configured = await settings(); + if (!configured.apiUrl) { + throw new Error(`No ${configured.selectedNetwork} API is configured.`); + } + const response = await fetch(`${requireApiUrl(configured)}/api/v1/addresses/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Contractless-Address": wallet.short_address, + "X-Contractless-Public-Key": wallet.public_key, + "X-Contractless-Signature": handshakeSignature + }, + body: JSON.stringify({ + address: registration.short_address, + public_key: registration.public_key, + signature: registration.signature + }) + }); + const result = await response.json().catch(() => undefined); + if (!response.ok || result?.success !== true) { + throw new Error(result?.error ?? `Wallet registration failed with HTTP ${response.status}.`); + } + return result.data; +} + +async function ensureWalletRegistered( + wasm: Awaited>, + wallet: Pick +): Promise { + const configured = await settings(); + if (!configured.apiUrl) return; + const handshakeSignature = wasm.create_handshake_proof( + wallet.public_key, + wallet.private_key + ); + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/addresses/registration?address=${encodeURIComponent(wallet.short_address)}`, + { + headers: { + "X-Contractless-Address": wallet.short_address, + "X-Contractless-Public-Key": wallet.public_key, + "X-Contractless-Signature": handshakeSignature + } + } + ); + const result = await response.json().catch(() => undefined); + if (!response.ok || result?.success !== true) { + throw new Error( + result?.error ?? `Wallet registration lookup failed with HTTP ${response.status}.` + ); + } + if (result.data?.registered !== true) { + await registerWalletKeys(wasm, wallet); + } +} + +async function restoreUnlockedWallet(): Promise { + const configured = await settings(); + if (unlocked) { + if (unlocked.expiresAt > Date.now() && unlocked.network === configured.selectedNetwork) return; + await lock(); + return; + } + + const stored = await chrome.storage.session.get(UNLOCKED_SESSION_KEY); + const candidate = stored[UNLOCKED_SESSION_KEY] as UnlockedWallet | undefined; + if (!candidate || candidate.expiresAt <= Date.now()) { + await chrome.storage.session.remove(UNLOCKED_SESSION_KEY); + return; + } + if (candidate.network !== configured.selectedNetwork) { + await chrome.storage.session.remove(UNLOCKED_SESSION_KEY); + await clearApplicationSessions(); + return; + } + if (!candidate.sessionId) { + candidate.sessionId = crypto.randomUUID(); + await clearApplicationSessions(); + await chrome.storage.session.set({ [UNLOCKED_SESSION_KEY]: candidate }); + } + unlocked = candidate; +} + +async function lock(): Promise { + if (unlocked) { + unlocked.private_key = ""; + unlocked = undefined; + } + await chrome.storage.session.remove([ + UNLOCKED_SESSION_KEY, + APP_SESSIONS_KEY + ]); + await chrome.alarms.clear(LOCK_ALARM); +} + +async function applicationSessions(): Promise { + const stored = await chrome.storage.session.get(APP_SESSIONS_KEY); + const sessions = stored[APP_SESSIONS_KEY]; + return Array.isArray(sessions) ? sessions as ApplicationSession[] : []; +} + +async function saveApplicationSessions( + sessions: ApplicationSession[] +): Promise { + await chrome.storage.session.set({ [APP_SESSIONS_KEY]: sessions }); +} + +async function clearApplicationSessions(): Promise { + await chrome.storage.session.remove(APP_SESSIONS_KEY); +} + +function randomHex(byteLength: number): string { + const bytes = crypto.getRandomValues(new Uint8Array(byteLength)); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +async function sha256Hex(value: string): Promise { + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(value) + ); + return Array.from( + new Uint8Array(digest), + (byte) => byte.toString(16).padStart(2, "0") + ).join(""); +} + +function providerParameters(request: ApprovalRecord): Record { + if (!request.params || typeof request.params !== "object") { + return {}; + } + return request.params as Record; +} + +function validatedApplicationId(value: unknown): string { + const appId = String(value ?? "").trim(); + if (!/^[A-Za-z0-9_-]{5,32}$/.test(appId)) { + throw new Error( + "The application ID must contain 5 to 32 URL-safe characters." + ); + } + return appId; +} + +function validateClaimedOrigin( + params: Record, + actualOrigin: string +): void { + if (String(params.origin ?? "") !== actualOrigin) { + throw new Error("The application origin does not match the requesting website."); + } +} + +function connectionTimestamp(value: unknown): number { + const timestamp = Number(value); + const now = Math.floor(Date.now() / 1_000); + if ( + !Number.isInteger(timestamp) + || Math.abs(now - timestamp) > 300 + ) { + throw new Error( + "The application connection timestamp must be within five minutes." + ); + } + return timestamp; +} + +function connectionNonce(value: unknown): string { + const nonce = String(value ?? "").trim().toLowerCase(); + if (!/^[0-9a-f]{32,128}$/.test(nonce) || nonce.length % 2 !== 0) { + throw new Error( + "The application nonce must contain 16 to 64 random hexadecimal bytes." + ); + } + return nonce; +} + +function connectionMessage(value: unknown): string { + const message = String(value ?? "").trim(); + if (!message || new TextEncoder().encode(message).length > 75) { + throw new Error( + "The application connection message must contain 1 to 75 bytes." + ); + } + return message; +} + +function signedTextMessage(value: unknown): string { + const message = String(value ?? "").trim(); + if (!message || new TextEncoder().encode(message).length > 75) { + throw new Error("A signed website message must contain 1 to 75 bytes."); + } + return message; +} + +function approvalRisk( + method: string, + params: Record +): ApprovalRecord["risk"] { + if ( + method === "contractless_connect" + || method === "contractless_signMessage" + ) { + return "safe"; + } + const transaction = params.transaction; + if (!transaction || typeof transaction !== "object") return "high"; + const txtype = Number((transaction as Record).txtype); + return Number.isInteger(txtype) && txtype >= 100 && txtype <= 113 + ? "low" + : "high"; +} + +function websiteTransaction(value: unknown): Record { + if (!value || typeof value !== "object") { + throw new Error("A complete transaction object is required."); + } + const transaction = value as Record; + const txtype = Number(transaction.txtype); + if (!Number.isInteger(txtype)) { + throw new Error("The transaction type is invalid."); + } + if (txtype === 6 || txtype === 7) { + throw new Error( + "Swaps and loan creation require a staged signing workflow." + ); + } + return transaction; +} + +function websiteDualTransaction(value: unknown): Record { + if (!value || typeof value !== "object") { + throw new Error("A complete or partially signed transaction object is required."); + } + const transaction = value as Record; + const unsigned = transaction.unsigned_swap + ?? transaction.unsigned_loan_contract + ?? transaction; + if (!unsigned || typeof unsigned !== "object") { + throw new Error("The unsigned transaction fields are missing."); + } + const txtype = Number((unsigned as Record).txtype); + if (txtype !== 6 && txtype !== 7) { + throw new Error("Staged signing is only available for swaps and loan creation."); + } + return transaction; +} + +async function ensureRegisteredTransactionParticipants( + wasm: Awaited>, + transaction: Record +): Promise { + if (!unlocked) throw new Error("Wallet is locked."); + const unsignedValue = transaction.unsigned_swap + ?? transaction.unsigned_loan_contract + ?? transaction; + if (!unsignedValue || typeof unsignedValue !== "object") return; + const unsigned = unsignedValue as Record; + const txtype = Number(unsigned.txtype); + const participants: Array<[string, string]> = txtype === 2 + ? [["Receiving", String(unsigned.receiver ?? "")]] + : txtype === 6 + ? [ + ["First swap participant", String(unsigned.sender1 ?? "")], + ["Second swap participant", String(unsigned.sender2 ?? "")] + ] + : txtype === 7 + ? [ + ["Lender", String(unsigned.lender ?? "")], + ["Borrower", String(unsigned.borrower ?? "")] + ] + : []; + if (participants.length === 0) return; + + // Inspection performs canonical field validation before any API request. + wasm.inspect_transaction_request(JSON.stringify(transaction)); + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const headers = { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + }; + const checked = new Map(); + for (const [label, address] of participants) { + const canonical = address.trim().toLowerCase(); + let registered = checked.get(canonical); + if (registered === undefined) { + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/addresses/registration?address=${encodeURIComponent(canonical)}`, + { headers } + ); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Address registration lookup failed with HTTP ${response.status}.` + ); + } + registered = reply.data?.registered === true; + checked.set(canonical, registered); + } + if (!registered) throw new Error(`${label} wallet is not registered.`); + } +} + +async function broadcastSignedTransaction( + wasm: Awaited>, + signed: Record +) { + if ( + signed.complete !== true + || typeof signed.bytes_hex !== "string" + || typeof signed.hash !== "string" + ) { + throw new Error("The transaction could not be completed."); + } + if (!unlocked) throw new Error("Wallet is locked."); + + const configured = await settings(); + const handshakeSignature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/transactions/broadcast`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": handshakeSignature + }, + body: JSON.stringify({ transaction_hex: signed.bytes_hex }) + } + ); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Transaction broadcast failed with HTTP ${response.status}.` + ); + } + await resetLockAlarm(); + return { + txid: signed.hash, + bytes: signed.bytes_hex, + broadcast: reply.data + }; +} + +async function authorizedApplication( + params: Record, + actualOrigin: string +): Promise { + if (!unlocked) throw new Error("Wallet is locked."); + validateClaimedOrigin(params, actualOrigin); + const appId = validatedApplicationId(params.appId); + const accessKey = String(params.accessKey ?? ""); + const session = (await applicationSessions()).find((candidate) => + candidate.origin === actualOrigin + && candidate.appId === appId + && candidate.accessKey === accessKey + && candidate.walletId === unlocked?.walletId + && candidate.unlockSessionId === unlocked?.sessionId + ); + if (!session) { + throw new Error( + "The application session is invalid or has expired. Reconnect the wallet." + ); + } + return session; +} + +async function approvalQueue(): Promise { + const stored = await chrome.storage.session.get(PENDING_APPROVAL_KEY); + const value = stored[PENDING_APPROVAL_KEY]; + const ids = Array.isArray(value) + ? value.filter((id): id is string => typeof id === "string" && id.length > 0) + : typeof value === "string" && value.length > 0 + ? [value] + : []; + if (ids.length === 0) return []; + + const approvalKeys = ids.map((id) => `approval:${id}`); + const approvals = await chrome.storage.session.get(approvalKeys); + const now = Date.now(); + const active: string[] = []; + const expiredKeys: string[] = []; + for (const id of ids) { + const key = `approval:${id}`; + const request = approvals[key] as ApprovalRecord | undefined; + if (!request || now - request.createdAt >= APPROVAL_TIMEOUT_MS) { + expiredKeys.push(key); + } else { + active.push(id); + } + } + if (expiredKeys.length > 0 || active.length !== ids.length) { + await chrome.storage.session.set({ [PENDING_APPROVAL_KEY]: active }); + if (expiredKeys.length > 0) await chrome.storage.session.remove(expiredKeys); + } + return active; +} + +async function removeApprovalFromQueue(id: string): Promise { + const remaining = (await approvalQueue()).filter((candidate) => candidate !== id); + await chrome.storage.session.set({ [PENDING_APPROVAL_KEY]: remaining }); + return remaining; +} + +async function openApproval(request: ApprovalRecord): Promise { + const queue = await approvalQueue(); + const queuedRecords = await chrome.storage.session.get( + queue.map((id) => `approval:${id}`) + ); + const duplicateConnection = request.method === "contractless_connect" + && queue.some((id) => { + const candidate = queuedRecords[`approval:${id}`] as ApprovalRecord | undefined; + if (!candidate || candidate.method !== "contractless_connect") return false; + const candidateParams = providerParameters(candidate); + const requestParams = providerParameters(request); + return candidate.origin === request.origin + && candidateParams.appId === requestParams.appId; + }); + if (duplicateConnection) { + throw new Error("This application already has a wallet connection request pending."); + } + if (queue.length >= MAX_PENDING_APPROVALS) { + throw new Error("The wallet approval queue is full. Resolve a pending request first."); + } + await chrome.storage.session.set({ + [PENDING_APPROVAL_KEY]: [...queue, request.id], + [`approval:${request.id}`]: request + }); + try { + await chrome.action.openPopup(); + } catch { + // Browsers that cannot open the action automatically retain the request + // so the user can open the wallet from the toolbar and approve it. + } +} + +async function waitForApproval(id: string): Promise { + const resultKey = `approval-result:${id}`; + const expiresAt = Date.now() + APPROVAL_TIMEOUT_MS; + while (Date.now() < expiresAt) { + const stored = await chrome.storage.session.get(resultKey); + if (stored[resultKey]) { + await chrome.storage.session.remove(resultKey); + return stored[resultKey].approved === true; + } + await new Promise((resolve) => setTimeout(resolve, 150)); + } + await chrome.storage.session.remove([ + `approval:${id}`, + resultKey + ]); + await removeApprovalFromQueue(id); + throw new Error("Wallet approval request expired."); +} + +function requestOrigin(sender: chrome.runtime.MessageSender): string { + const candidate = sender.url ?? sender.origin; + if (!candidate) throw new Error("The requesting website origin is unavailable."); + const origin = new URL(candidate).origin; + if (origin === "null") throw new Error("Requests from opaque origins are not supported."); + return origin; +} + +async function executeApprovedRequest( + wasm: Awaited>, + request: ApprovalRecord +) { + if (!unlocked) throw new Error("Wallet is locked."); + await resetLockAlarm(); + + switch (request.method) { + case "contractless_connect": { + const params = providerParameters(request); + validateClaimedOrigin(params, request.origin); + const requestedSessionId = String(params.walletSessionId ?? ""); + if (requestedSessionId && requestedSessionId !== unlocked.sessionId) { + throw new Error( + "The active wallet changed while the connection was being approved." + ); + } + const appId = validatedApplicationId(params.appId); + const requestedAt = connectionTimestamp(params.timestamp); + const nonce = connectionNonce(params.nonce); + const message = connectionMessage(params.message); + const accessKey = randomHex(32); + const accessKeyHash = await sha256Hex(accessKey); + const now = Math.floor(Date.now() / 1_000); + const proofExpiresAt = Math.min( + now + 15 * 60, + Math.floor(unlocked.expiresAt / 1_000) + ); + const signedMessage = [ + "Contractless application connection", + `Application ID: ${appId}`, + `Origin: ${request.origin}`, + `Requested timestamp: ${requestedAt}`, + `Message: ${message}`, + `Session key hash: ${accessKeyHash}` + ].join("\n"); + const proof = JSON.parse( + wasm.sign_message_request( + JSON.stringify({ + origin: request.origin, + message: signedMessage, + nonce, + issued_at: now, + expires_at: proofExpiresAt + }), + unlocked.private_key, + unlocked.public_key + ) + ) as Record; + const session: ApplicationSession = { + origin: request.origin, + appId, + accessKey, + walletId: unlocked.walletId, + unlockSessionId: unlocked.sessionId, + createdAt: Date.now() + }; + const sessions = await applicationSessions(); + sessions.push(session); + await saveApplicationSessions(sessions); + return { + address: unlocked.short_address, + publicKey: unlocked.public_key, + signature: proof.signature, + proof, + accessKey, + appId, + origin: request.origin, + sessionExpiresAt: unlocked.expiresAt + }; + } + case "contractless_signMessage": { + const params = providerParameters(request); + await authorizedApplication(params, request.origin); + const now = Math.floor(Date.now() / 1000); + const expiresAt = Number(params.expires_at ?? now + 300); + const appId = validatedApplicationId(params.appId); + const messageRequest = { + origin: request.origin, + message: [ + "Contractless application message", + `Application ID: ${appId}`, + `Message: ${signedTextMessage(params.message)}` + ].join("\n"), + nonce: String(params.nonce ?? ""), + issued_at: now, + expires_at: expiresAt + }; + return JSON.parse( + wasm.sign_message_request( + JSON.stringify(messageRequest), + unlocked.private_key, + unlocked.public_key + ) + ); + } + case "contractless_sendTransaction": { + const params = providerParameters(request); + await authorizedApplication(params, request.origin); + const transaction = websiteTransaction(params.transaction); + const signed = JSON.parse( + wasm.sign_transaction_request( + JSON.stringify(transaction), + unlocked.private_key, + unlocked.public_key, + 1 + ) + ) as Record; + return broadcastSignedTransaction(wasm, signed); + } + case "contractless_signDualTransaction": { + const params = providerParameters(request); + await authorizedApplication(params, request.origin); + const transaction = websiteDualTransaction(params.transaction); + const signerSlot = Number(params.signerSlot); + if (signerSlot !== 1 && signerSlot !== 2) { + throw new Error("The signer slot must be 1 or 2."); + } + const signed = JSON.parse( + wasm.sign_transaction_request( + JSON.stringify(transaction), + unlocked.private_key, + unlocked.public_key, + signerSlot + ) + ) as Record; + return { + complete: false, + txid: signed.hash, + unsignedBytes: signed.unsigned_bytes_hex, + canonicalUnsignedJson: JSON.parse( + wasm.inspect_transaction_request(JSON.stringify(transaction)) + ).canonical_unsigned_json, + signedTransaction: signed.signed_transaction, + signature: signerSlot === 1 ? signed.signature1 : signed.signature2, + signature1: signed.signature1, + signature2: signed.signature2, + signerSlot, + signerPublicKey: unlocked.public_key + }; + } + default: + throw new Error("The website requested an unsupported wallet method."); + } +} + +async function handle(message: Record, sender: chrome.runtime.MessageSender) { + const configured = await settings(); + if (message.type === "switch-network") { + const selectedNetwork = message.network === "mainnet" ? "mainnet" : "testnet"; + await lock(); + const next = { + ...configured, + selectedNetwork, + apiUrl: selectedNetwork === "mainnet" + ? configured.mainnetApiUrl + : configured.testnetApiUrl + }; + await chrome.storage.local.set({ [SETTINGS_KEY]: next }); + return status(); + } + const wasm = await core(configured.selectedNetwork); + await restoreUnlockedWallet(); + switch (message.type) { + case "status": + return status(); + case "create-wallet": { + const password = String(message.password); + const walletJson = wasm.generate_browser_wallet(password); + const opened = JSON.parse(wasm.unlock_browser_wallet(walletJson, password)); + const storedWallet = await addWallet(walletJson); + await rememberUnlockedWallet({ + walletId: storedWallet.id, + network: configured.selectedNetwork, + ...opened + }); + if (configured.apiUrl) { + try { + await registerWalletKeys(wasm, opened); + } catch (error) { + console.warn("Automatic wallet registration failed during creation.", error); + } + } + return status(); + } + case "import-wallet": { + const walletJson = wasm.import_legacy_wallet( + String(message.walletJson), + String(message.legacyPassword), + String(message.password) + ); + const storedWallet = await addWallet(walletJson); + const opened = JSON.parse( + wasm.unlock_browser_wallet(walletJson, String(message.password)) + ); + await rememberUnlockedWallet({ walletId: storedWallet.id, network: configured.selectedNetwork, ...opened }); + return status(); + } + case "import-wallet-image": { + const password = String(message.password); + const walletJson = wasm.import_private_key_image( + String(message.imageBase64), + String(message.imagePassword), + password + ); + const storedWallet = await addWallet(walletJson); + const opened = JSON.parse(wasm.unlock_browser_wallet(walletJson, password)); + await rememberUnlockedWallet({ walletId: storedWallet.id, network: configured.selectedNetwork, ...opened }); + return status(); + } + case "import-private-key": { + const password = String(message.password); + const walletJson = wasm.import_private_key(String(message.privateKey), password); + const storedWallet = await addWallet(walletJson); + const opened = JSON.parse(wasm.unlock_browser_wallet(walletJson, password)); + await rememberUnlockedWallet({ walletId: storedWallet.id, network: configured.selectedNetwork, ...opened }); + return status(); + } + case "unlock": { + const collection = await walletCollection(); + const wallet = activeStoredWallet(collection, configured.selectedNetwork); + if (!wallet) throw new Error("No wallet exists."); + const opened = JSON.parse( + wasm.unlock_browser_wallet(wallet.walletJson, String(message.password)) + ); + await rememberUnlockedWallet({ walletId: wallet.id, network: configured.selectedNetwork, ...opened }); + try { + await ensureWalletRegistered(wasm, opened); + } catch (error) { + console.warn("Automatic wallet registration failed during unlock.", error); + } + return status(); + } + case "switch-wallet": { + const collection = await walletCollection(); + const walletId = String(message.walletId); + const selected = collection.wallets.find((wallet) => wallet.id === walletId); + if (!selected || selected.network !== configured.selectedNetwork) { + throw new Error("The selected wallet does not exist."); + } + await lock(); + collection.activeWalletIds[configured.selectedNetwork] = walletId; + await saveWalletCollection(collection); + return status(); + } + case "rename-wallet": { + const collection = await walletCollection(); + const wallet = collection.wallets.find( + (candidate) => candidate.id === String(message.walletId) + ); + if (!wallet) throw new Error("The selected wallet does not exist."); + const label = String(message.label).trim(); + if (label.length < 1 || label.length > 40) { + throw new Error("Wallet labels must contain between 1 and 40 characters."); + } + wallet.label = label; + await saveWalletCollection(collection); + return status(); + } + case "lock": + await lock(); + return status(); + case "save-settings": { + const testnetApiUrl = String(message.testnetApiUrl ?? configured.testnetApiUrl).replace(/\/+$/, ""); + const mainnetApiUrl = String(message.mainnetApiUrl ?? configured.mainnetApiUrl).replace(/\/+$/, ""); + const next: StoredSettings = { + selectedNetwork: configured.selectedNetwork, + testnetApiUrl, + mainnetApiUrl, + apiUrl: configured.selectedNetwork === "mainnet" ? mainnetApiUrl : testnetApiUrl, + autoLockMinutes: Math.max(1, Number(message.autoLockMinutes)) + }; + await chrome.storage.local.set({ [SETTINGS_KEY]: next }); + await resetLockAlarm(); + return status(); + } + case "register-wallet": { + if (!unlocked) throw new Error("Wallet is locked."); + const result = await registerWalletKeys(wasm, unlocked); + await resetLockAlarm(); + return result; + } + case "wallet-balances": { + if (!unlocked) throw new Error("Wallet is locked."); + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const headers = { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + }; + const address = encodeURIComponent(unlocked.short_address); + const [balancesResponse, tokensResponse, nftsResponse] = await Promise.all([ + fetch(`${requireApiUrl(configured)}/api/v1/balances?address=${address}`, { headers }), + fetch(`${requireApiUrl(configured)}/api/v1/tokens`, { headers }), + fetch(`${requireApiUrl(configured)}/api/v1/nfts`, { headers }) + ]); + const [balancesReply, tokensReply, nftsReply] = await Promise.all([ + balancesResponse.json().catch(() => undefined), + tokensResponse.json().catch(() => undefined), + nftsResponse.json().catch(() => undefined) + ]); + if (!balancesResponse.ok || balancesReply?.success !== true) { + throw new Error( + balancesReply?.error ?? `Balance lookup failed with HTTP ${balancesResponse.status}.` + ); + } + await resetLockAlarm(); + return { + address: unlocked.short_address, + balances: balancesReply.data?.balances ?? [], + tokens: tokensResponse.ok && tokensReply?.success === true + ? tokensReply.data?.tokens ?? [] + : [], + nfts: nftsResponse.ok && nftsReply?.success === true + ? nftsReply.data?.nfts ?? [] + : [], + tokenCatalogError: tokensResponse.ok && tokensReply?.success === true + ? undefined + : tokensReply?.error ?? `Token lookup failed with HTTP ${tokensResponse.status}.`, + nftCatalogError: nftsResponse.ok && nftsReply?.success === true + ? undefined + : nftsReply?.error ?? `NFT lookup failed with HTTP ${nftsResponse.status}.` + }; + } + case "wallet-nft-details": { + if (!unlocked) throw new Error("Wallet is locked."); + const name = String(message.name ?? "").trim(); + const series = Number(message.series ?? 0); + if ( + !name + || name.length > 15 + || !Number.isInteger(series) + || series < 0 + || series > 4_294_967_295 + ) { + throw new Error("A valid NFT or RWA identifier is required."); + } + + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const headers = { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + }; + const query = `name=${encodeURIComponent(name)}&series=${series}`; + const [detailsResponse, mediaResponse] = await Promise.all([ + fetch(`${requireApiUrl(configured)}/api/v1/nfts/details?${query}`, { headers }), + fetch(`${requireApiUrl(configured)}/api/v1/nfts/media?${query}`, { headers }) + ]); + const [detailsReply, mediaReply] = await Promise.all([ + detailsResponse.json().catch(() => undefined), + mediaResponse.json().catch(() => undefined) + ]); + if (!detailsResponse.ok || detailsReply?.success !== true) { + throw new Error( + detailsReply?.error + ?? `NFT details lookup failed with HTTP ${detailsResponse.status}.` + ); + } + + let imageDataUrl = ""; + let imageAvailable = false; + let metadata: Record = {}; + let mediaError: string | undefined; + if (mediaResponse.ok && mediaReply?.success === true) { + metadata = mediaReply.data?.metadata + && typeof mediaReply.data.metadata === "object" + ? mediaReply.data.metadata + : {}; + imageAvailable = mediaReply.data?.image_available === true; + const imagePath = String(mediaReply.data?.image_url ?? ""); + if (imagePath) { + try { + const imageResponse = await fetch( + new URL(imagePath, `${requireApiUrl(configured)}/`).toString(), + { headers } + ); + if (imageResponse.ok) { + imageDataUrl = arrayBufferDataUrl( + await imageResponse.arrayBuffer(), + imageResponse.headers.get("Content-Type") ?? "image/png" + ); + } else { + mediaError = `NFT image returned HTTP ${imageResponse.status}.`; + } + } catch { + mediaError = "NFT image could not be retrieved."; + } + } + } else { + mediaError = mediaReply?.error + ?? `NFT media lookup failed with HTTP ${mediaResponse.status}.`; + } + + await resetLockAlarm(); + return { + ...detailsReply.data, + metadata, + image_data_url: imageDataUrl, + image_available: imageAvailable, + media_error: mediaError + }; + } + case "wallet-receive-address": { + if (!unlocked) throw new Error("Wallet is locked."); + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const address = encodeURIComponent(unlocked.short_address); + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/addresses/vanity?address=${address}`, + { + headers: { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + } + } + ); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Vanity address lookup failed with HTTP ${response.status}.` + ); + } + await resetLockAlarm(); + const vanityAddress = + typeof reply.data?.vanity_address === "string" && + reply.data.vanity_address.trim() !== "" + ? reply.data.vanity_address + : undefined; + return { + shortAddress: unlocked.short_address, + receiveAddress: vanityAddress ?? unlocked.short_address, + vanity: Boolean(vanityAddress) + }; + } + case "wallet-prepare-transfer": { + if (!unlocked) throw new Error("Wallet is locked."); + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const headers = { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + }; + const receiverInput = String(message.receiver ?? "").trim().toLowerCase(); + const suffix = unlocked.short_address.endsWith(".cltc") ? "cltc" : "clc"; + let receiver = receiverInput; + + if (!new RegExp(`^[0-9a-f]{40}\\.${suffix}$`).test(receiverInput)) { + if (!new RegExp(`^[a-z]{1,20}\\.${suffix}$`).test(receiverInput)) { + throw new Error(`Enter a valid Contractless ${suffix.toUpperCase()} address.`); + } + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/addresses/vanity/resolve?address=${encodeURIComponent(receiverInput)}`, + { headers } + ); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Vanity address lookup failed with HTTP ${response.status}.` + ); + } + receiver = String(reply.data?.owner_address ?? ""); + if (!receiver) throw new Error("That vanity address is not registered."); + } else { + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/addresses/registration?address=${encodeURIComponent(receiver)}`, + { headers } + ); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Address lookup failed with HTTP ${response.status}.` + ); + } + if (reply.data?.registered !== true) { + throw new Error("The receiving wallet is not registered."); + } + } + + const nftSeries = Number(message.nftSeries ?? 0); + if ( + !Number.isInteger(nftSeries) + || nftSeries < 0 + || nftSeries > 4_294_967_295 + ) { + throw new Error("The NFT/RWA series number is invalid."); + } + const transaction = { + txtype: 2, + time: Math.floor(Date.now() / 1_000), + value: String(message.valueAtomic ?? ""), + coin: String(message.asset ?? "").trim().padEnd(15, " "), + nft_series: nftSeries, + sender: unlocked.short_address, + receiver, + txfee: String(message.feeAtomic ?? "") + }; + const inspection = JSON.parse( + wasm.inspect_transaction_request(JSON.stringify(transaction)) + ); + await resetLockAlarm(); + return { + transaction, + review: inspection.review, + hash: inspection.hash, + receiverInput + }; + } + case "wallet-broadcast-transfer": { + if (!unlocked) throw new Error("Wallet is locked."); + const transaction = message.transaction; + if (!transaction || typeof transaction !== "object") { + throw new Error("The reviewed transfer is unavailable."); + } + const signed = JSON.parse( + wasm.sign_transaction_request( + JSON.stringify(transaction), + unlocked.private_key, + unlocked.public_key, + 1 + ) + ); + return broadcastSignedTransaction( + wasm, + signed as Record + ); + } + case "wallet-activity": { + if (!unlocked) throw new Error("Wallet is locked."); + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const address = encodeURIComponent(unlocked.short_address); + const headers = { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + }; + const [response, heightResponse] = await Promise.all([ + fetch( + `${requireApiUrl(configured)}/api/v1/addresses/latest?address=${address}&limit=25`, + { headers } + ), + fetch(`${requireApiUrl(configured)}/api/v1/height`, { headers }) + ]); + const [reply, heightReply] = await Promise.all([ + response.json().catch(() => undefined), + heightResponse.json().catch(() => undefined) + ]); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Activity lookup failed with HTTP ${response.status}.` + ); + } + if (!heightResponse.ok || heightReply?.success !== true) { + throw new Error( + heightReply?.error ?? `Block height lookup failed with HTTP ${heightResponse.status}.` + ); + } + await resetLockAlarm(); + return { + address: unlocked.short_address, + current_height: Number(heightReply.data?.height ?? 0), + transactions: Array.isArray(reply.data?.transactions) + ? reply.data.transactions + : [] + }; + } + case "wallet-transaction": { + if (!unlocked) throw new Error("Wallet is locked."); + const txid = String(message.txid); + if (!/^[0-9a-f]{64}$/i.test(txid)) { + throw new Error("A valid transaction ID is required."); + } + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const headers = { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + }; + const [response, heightResponse] = await Promise.all([ + fetch( + `${requireApiUrl(configured)}/api/v1/transactions/lookup?txid=${encodeURIComponent(txid)}`, + { headers } + ), + fetch(`${requireApiUrl(configured)}/api/v1/height`, { headers }) + ]); + const [reply, heightReply] = await Promise.all([ + response.json().catch(() => undefined), + heightResponse.json().catch(() => undefined) + ]); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Transaction lookup failed with HTTP ${response.status}.` + ); + } + if (!heightResponse.ok || heightReply?.success !== true) { + throw new Error( + heightReply?.error ?? `Block height lookup failed with HTTP ${heightResponse.status}.` + ); + } + await resetLockAlarm(); + return { + ...reply.data, + current_height: Number(heightReply.data?.height ?? 0) + }; + } + case "export-wallet": { + const wallet = activeStoredWallet(await walletCollection(), configured.selectedNetwork); + if (!wallet) throw new Error("No wallet exists."); + return wallet.walletJson; + } + case "export-legacy-wallet": { + const wallet = activeStoredWallet(await walletCollection(), configured.selectedNetwork); + if (!wallet) throw new Error("No wallet exists."); + return wasm.export_legacy_wallet( + wallet.walletJson, + String(message.password), + String(message.legacyPassword) + ); + } + case "export-private-key-image": { + const wallet = activeStoredWallet(await walletCollection(), configured.selectedNetwork); + if (!wallet) throw new Error("No wallet exists."); + const legacy = JSON.parse( + wasm.export_legacy_wallet( + wallet.walletJson, + String(message.password), + String(message.imagePassword) + ) + ) as { private_key?: string }; + if (typeof legacy.private_key !== "string") { + throw new Error("The wallet image could not be created."); + } + return legacy.private_key; + } + case "reveal-private-key": { + const wallet = activeStoredWallet(await walletCollection(), configured.selectedNetwork); + if (!wallet) throw new Error("No wallet exists."); + const opened = JSON.parse( + wasm.unlock_browser_wallet(wallet.walletJson, String(message.password)) + ) as { private_key?: string }; + if (typeof opened.private_key !== "string") { + throw new Error("The private key could not be decrypted."); + } + await resetLockAlarm(); + return { + label: wallet.label, + address: wallet.address, + privateKey: opened.private_key + }; + } + case "provider-request": { + const origin = requestOrigin(sender); + const method = String(message.method); + const params = message.params && typeof message.params === "object" + ? message.params as Record + : {}; + + if (method === "contractless_detect") { + return { + installed: true, + providerVersion: "1.0.0" + }; + } + + if (method === "contractless_connect") { + validateClaimedOrigin(params, origin); + const appId = validatedApplicationId(params.appId); + const timestamp = connectionTimestamp(params.timestamp); + connectionNonce(params.nonce); + const appMessage = connectionMessage(params.message); + const request: ApprovalRecord = { + id: crypto.randomUUID(), + method, + params: { + ...params, + walletSessionId: unlocked?.sessionId + }, + origin, + createdAt: Date.now(), + risk: approvalRisk(method, params), + display: { + request: "Connect application", + application_id: appId, + website: origin, + wallet_address: unlocked?.short_address ?? "Unlock wallet to continue", + message: appMessage, + requested_timestamp: timestamp, + access: "Address access and future wallet requests until this wallet locks" + } + }; + await openApproval(request); + if (!(await waitForApproval(request.id))) { + throw new Error("The wallet connection request was rejected."); + } + return executeApprovedRequest(wasm, request); + } + + if (method === "contractless_accounts") { + try { + await authorizedApplication(params, origin); + return unlocked ? [unlocked.short_address] : []; + } catch { + return []; + } + } + + if (method === "contractless_balances") { + await authorizedApplication(params, origin); + if (!unlocked) throw new Error("Wallet is locked."); + const configured = await settings(); + const signature = wasm.create_handshake_proof( + unlocked.public_key, + unlocked.private_key + ); + const response = await fetch( + `${requireApiUrl(configured)}/api/v1/balances?address=${encodeURIComponent(unlocked.short_address)}`, + { + headers: { + "X-Contractless-Address": unlocked.short_address, + "X-Contractless-Public-Key": unlocked.public_key, + "X-Contractless-Signature": signature + } + } + ); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error( + reply?.error ?? `Balance lookup failed with HTTP ${response.status}.` + ); + } + await resetLockAlarm(); + return reply.data; + } + + if (method === "contractless_disconnect") { + const session = await authorizedApplication(params, origin); + const remaining = (await applicationSessions()).filter((candidate) => + !( + candidate.origin === session.origin + && candidate.appId === session.appId + && candidate.accessKey === session.accessKey + ) + ); + await saveApplicationSessions(remaining); + return { disconnected: true }; + } + + if (method === "contractless_completeDualTransaction") { + await authorizedApplication(params, origin); + const transaction = websiteDualTransaction(params.transaction); + return JSON.parse(wasm.complete_dual_transaction_request( + JSON.stringify(transaction), + String(params.signature1 ?? ""), + String(params.publicKey1 ?? ""), + String(params.signature2 ?? ""), + String(params.publicKey2 ?? "") + )); + } + + if (![ + "contractless_signMessage", + "contractless_sendTransaction", + "contractless_signDualTransaction" + ].includes(method)) { + throw new Error("The website requested an unsupported wallet method."); + } + await authorizedApplication(params, origin); + if (method === "contractless_sendTransaction") { + const transaction = websiteTransaction(params.transaction); + await ensureRegisteredTransactionParticipants(wasm, transaction); + } else if (method === "contractless_signDualTransaction") { + const transaction = websiteDualTransaction(params.transaction); + await ensureRegisteredTransactionParticipants(wasm, transaction); + } + const request: ApprovalRecord = { + id: crypto.randomUUID(), + method, + params, + origin, + createdAt: Date.now(), + risk: approvalRisk(method, params), + display: method === "contractless_signMessage" + ? { + request: "Sign a text message", + application_id: validatedApplicationId(params.appId), + website: origin, + message: signedTextMessage(params.message), + warning: "This is a text signature. It cannot move wallet assets." + } + : method === "contractless_signDualTransaction" + ? (() => { + const inspection = JSON.parse( + wasm.inspect_transaction_request(JSON.stringify(params.transaction)) + ) as Record; + return { + application_id: validatedApplicationId(params.appId), + website: origin, + transaction_type: inspection.transaction_name, + signer_slot: params.signerSlot, + transaction_fields: inspection.review, + action: "Sign and return this signature to the application without broadcasting." + }; + })() + : (() => { + const inspection = JSON.parse( + wasm.inspect_transaction_request(JSON.stringify(params.transaction)) + ) as Record; + return { + application_id: validatedApplicationId(params.appId), + website: origin, + transaction_type: inspection.transaction_name, + transaction_fields: inspection.review, + action: "Sign and broadcast this transaction." + }; + })() + }; + await openApproval(request); + if (!(await waitForApproval(request.id))) { + throw new Error("The wallet request was rejected."); + } + return executeApprovedRequest(wasm, request); + } + case "pending-provider-approval": { + const queue = await approvalQueue(); + const id = queue[0] ?? ""; + if (!id) return null; + const approvalKey = `approval:${id}`; + const approvalStored = await chrome.storage.session.get(approvalKey); + const request = approvalStored[approvalKey] as ApprovalRecord | undefined; + if (!request) { + await removeApprovalFromQueue(id); + return null; + } + if (!unlocked) return { locked: true }; + + if (request.method === "contractless_connect") { + request.params = { + ...(request.params as Record), + walletSessionId: unlocked.sessionId + }; + request.display = { + ...(request.display ?? {}), + wallet_address: unlocked.short_address + }; + await chrome.storage.session.set({ [approvalKey]: request }); + } + return { locked: false, request, position: 1, total: queue.length }; + } + case "resolve-provider-approval": { + if (!unlocked) throw new Error("Unlock the wallet before approving this request."); + const id = String(message.id ?? ""); + const queue = await approvalQueue(); + if (!id || queue[0] !== id) { + throw new Error("The wallet approval request has expired."); + } + const approvalKey = `approval:${id}`; + const approvalStored = await chrome.storage.session.get(approvalKey); + const request = approvalStored[approvalKey] as ApprovalRecord | undefined; + if (!request) throw new Error("The wallet approval request has expired."); + + if (request.method === "contractless_connect") { + request.params = { + ...(request.params as Record), + walletSessionId: unlocked.sessionId + }; + await chrome.storage.session.set({ [approvalKey]: request }); + } + await chrome.storage.session.set({ + [`approval-result:${id}`]: { approved: message.approved === true } + }); + await chrome.storage.session.remove(approvalKey); + const remaining = await removeApprovalFromQueue(id); + return { resolved: true, remaining: remaining.length }; + } + default: + throw new Error("Unsupported wallet request."); + } +} + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + handle(message, sender) + .then((result) => sendResponse({ ok: true, result })) + .catch((error: unknown) => + sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }) + ); + return true; +}); + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === LOCK_ALARM) void lock(); +}); diff --git a/extension/src/content.ts b/extension/src/content.ts new file mode 100644 index 0000000..c8903cf --- /dev/null +++ b/extension/src/content.ts @@ -0,0 +1,25 @@ +const script = document.createElement("script"); +script.src = chrome.runtime.getURL("provider.js"); +script.onload = () => script.remove(); +(document.head || document.documentElement).appendChild(script); + +window.addEventListener("message", (event) => { + if (event.source !== window || event.data?.source !== "contractless-page") return; + chrome.runtime.sendMessage( + { + type: "provider-request", + method: event.data.method, + params: event.data.params + }, + (response) => { + window.postMessage( + { + source: "contractless-wallet", + id: event.data.id, + response + }, + window.location.origin + ); + } + ); +}); diff --git a/extension/src/core.ts b/extension/src/core.ts new file mode 100644 index 0000000..fa0df01 --- /dev/null +++ b/extension/src/core.ts @@ -0,0 +1,12 @@ +import initTestnet, * as testnet from "./wasm/testnet/contractless_browser_core"; +import initMainnet, * as mainnet from "./wasm/mainnet/contractless_browser_core"; +import type { ContractlessNetwork } from "./types"; + +const initialized: Partial>> = {}; + +export async function core(network: ContractlessNetwork = "testnet") { + const module = network === "mainnet" ? mainnet : testnet; + initialized[network] ??= network === "mainnet" ? initMainnet() : initTestnet(); + await initialized[network]; + return module; +} diff --git a/extension/src/import-wallet.css b/extension/src/import-wallet.css new file mode 100644 index 0000000..6589be1 --- /dev/null +++ b/extension/src/import-wallet.css @@ -0,0 +1,200 @@ +:root { + font: 16px/1.5 Arial, Helvetica, sans-serif; + color: #fff; + background: #08131c; +} + +* { box-sizing: border-box; } + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: #08131c; +} + +button, +input { font: inherit; } + +button { cursor: pointer; } +button:disabled { cursor: wait; opacity: .72; } + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 76px; + padding: 0 32px; + border-bottom: 1px solid #2e4757; + background: #0d1d28; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; +} + +.brand img { + width: 39px; + filter: brightness(0) invert(1); +} + +.brand div { display: grid; } +.brand strong { font-size: 18px; } +.brand span, +.local-page { color: #b9cbd6; font-size: 13px; } + +main { + display: grid; + place-items: start center; + padding: 54px 20px; +} + +.import-panel, +.success-panel { + width: min(100%, 500px); + padding: 34px; + border: 1px solid #385666; + background: #10222e; +} + +.eyebrow { + color: #58d5e7; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +h1 { + margin: 7px 0 10px; + font-size: 31px; + line-height: 1.15; + letter-spacing: 0; +} + +.intro, +.success-panel p { margin: 0 0 26px; color: #c8d6de; } + +form { display: grid; } + +label { + margin: 21px 0 8px; + font-size: 13px; + font-weight: 700; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +.file-picker { + display: flex; + align-items: center; + gap: 14px; + min-height: 78px; + margin-top: 8px; + padding: 13px; + border: 1px solid #5ac9de; + background: #132d3b; + cursor: pointer; +} + +.file-picker > span:last-child { display: grid; gap: 3px; } +.file-picker small { color: #afc6d1; font-weight: 400; } + +.file-icon { + display: grid; + place-items: center; + width: 44px; + height: 44px; + border: 1px solid #62d7e9; + color: #62d7e9; + font-size: 25px; +} + +.input-row { + display: grid; + grid-template-columns: 1fr auto; +} + +.input-row input { + min-width: 0; + height: 50px; + padding: 0 14px; + border: 1px solid #5a7180; + border-right: 0; + background: #08151e; + color: white; +} + +.input-row button { + width: 68px; + border: 1px solid #5a7180; + background: #173345; + color: white; +} + +.security-note, +.address-result { + display: grid; + gap: 4px; + margin-top: 20px; + padding: 13px 14px; + border-left: 3px solid #57d0df; + background: #0a1922; +} + +.security-note span, +.address-result span { color: #b7cad4; font-size: 13px; } + +.address-result strong { overflow-wrap: anywhere; } + +.primary { + min-height: 50px; + margin-top: 24px; + border: 1px solid #67d5e4; + background: #236eaa; + color: white; + font-weight: 700; +} + +#notice { + margin: 14px 0 0; + padding: 10px 12px; + border-left: 3px solid #ff7168; + background: #321c20; + color: #ffc0bb; + font-size: 13px; +} + +#notice:empty { display: none; } + +.success-mark { + display: grid; + place-items: center; + width: 58px; + height: 58px; + margin-bottom: 25px; + border: 2px solid #62d7e9; + border-radius: 50%; + color: #62d7e9; + font-size: 31px; + font-weight: 700; +} + +.completion-message { margin-top: 24px !important; } + +@media (max-width: 560px) { + .site-header { padding: 0 18px; } + .local-page { display: none; } + main { padding: 24px 12px; } + .import-panel, + .success-panel { padding: 24px 20px; } + h1 { font-size: 27px; } +} diff --git a/extension/src/import-wallet.html b/extension/src/import-wallet.html new file mode 100644 index 0000000..fcf7ff6 --- /dev/null +++ b/extension/src/import-wallet.html @@ -0,0 +1,69 @@ + + + + + + Import Contractless Wallet + + + + + +
+
+ Restore Contractless wallet +

Import wallet image

+

+ Select your encrypted Contractless private-key image and enter the + encryption key originally used to create it. +

+ +
+ + + + +
+ + +
+ +
+ Processed locally by Contractless Wallet + The wallet image and encryption key are not sent to the website you were visiting. +
+ + + +
+
+ + +
+ + + + diff --git a/extension/src/import-wallet.ts b/extension/src/import-wallet.ts new file mode 100644 index 0000000..185cdcf --- /dev/null +++ b/extension/src/import-wallet.ts @@ -0,0 +1,78 @@ +import type { WalletStatus } from "./types"; + +const byId = (id: string) => document.getElementById(id) as T; +const form = byId("import-form"); +const imageInput = byId("wallet-image"); +const passwordInput = byId("image-password"); +const importButton = byId("import-button"); +const notice = byId("notice"); + +void send("status").then((status) => { + const walletStatus = status as WalletStatus; + byId("selected-network").textContent = walletStatus.networkName; +}).catch((error) => { + notice.textContent = errorText(error); +}); + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ""; + const chunkSize = 0x8000; + for (let offset = 0; offset < bytes.length; offset += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); + } + return btoa(binary); +} + +function errorText(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +async function send(type: string, values: Record = {}) { + const response = await chrome.runtime.sendMessage({ type, ...values }); + if (!response?.ok) throw new Error(response?.error ?? "Wallet request failed."); + return response.result; +} + +imageInput.addEventListener("change", () => { + byId("image-file-name").textContent = + imageInput.files?.[0]?.name ?? "Choose wallet image"; + notice.textContent = ""; +}); + +byId("reveal-password").addEventListener("click", (event) => { + const button = event.currentTarget as HTMLButtonElement; + const showing = passwordInput.type === "text"; + passwordInput.type = showing ? "password" : "text"; + button.textContent = showing ? "Show" : "Hide"; +}); + +form.addEventListener("submit", (event) => { + event.preventDefault(); + void (async () => { + const file = imageInput.files?.[0]; + if (!file) throw new Error("Choose a wallet image."); + if (!passwordInput.value.trim()) throw new Error("Enter the wallet image encryption key."); + + notice.textContent = ""; + importButton.disabled = true; + importButton.textContent = "Importing wallet..."; + + const imageBase64 = bytesToBase64(new Uint8Array(await file.arrayBuffer())); + const status = await send("import-wallet-image", { + imageBase64, + imagePassword: passwordInput.value, + password: passwordInput.value + }) as WalletStatus; + + passwordInput.value = ""; + byId("imported-address").textContent = status.address ?? "Imported wallet"; + byId("import-panel").hidden = true; + byId("success-panel").hidden = false; + })().catch((error) => { + notice.textContent = errorText(error); + importButton.disabled = false; + importButton.textContent = "Import wallet"; + }); +}); + +byId("close-page").addEventListener("click", () => window.close()); diff --git a/extension/src/manifest.chrome.json b/extension/src/manifest.chrome.json new file mode 100644 index 0000000..c42233e --- /dev/null +++ b/extension/src/manifest.chrome.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 3, + "name": "Contractless Wallet", + "version": "0.1.1", + "description": "A local-signing Web3 wallet for Contractless.", + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "permissions": ["storage", "alarms", "activeTab", "clipboardWrite"], + "host_permissions": ["https://api.contractless.dev/*"], + "optional_host_permissions": ["https://*/"], + "action": { + "default_popup": "popup.html", + "default_title": "Contractless Wallet", + "default_icon": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + } + }, + "icons": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"], + "js": ["content.js"], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": ["provider.js"], + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"] + } + ] +} diff --git a/extension/src/manifest.firefox.json b/extension/src/manifest.firefox.json new file mode 100644 index 0000000..389d071 --- /dev/null +++ b/extension/src/manifest.firefox.json @@ -0,0 +1,57 @@ +{ + "manifest_version": 3, + "name": "Contractless Wallet", + "version": "0.1.1", + "description": "A local-signing Web3 wallet for Contractless.", + "browser_specific_settings": { + "gecko": { + "id": "wallet@contractless.dev", + "strict_min_version": "149.0", + "data_collection_permissions": { + "required": ["none"] + } + }, + "gecko_android": { + "strict_min_version": "149.0" + } + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "permissions": ["storage", "alarms", "activeTab", "clipboardWrite"], + "host_permissions": ["https://api.contractless.dev/*"], + "optional_host_permissions": ["https://*/"], + "action": { + "default_popup": "popup.html", + "default_title": "Contractless Wallet", + "default_icon": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + } + }, + "icons": { + "16": "contractless-logo-16.png", + "32": "contractless-logo-32.png", + "48": "contractless-logo-48.png", + "128": "contractless-logo-128.png" + }, + "background": { + "scripts": ["background.js"], + "type": "module" + }, + "content_scripts": [ + { + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"], + "js": ["content.js"], + "run_at": "document_start" + } + ], + "web_accessible_resources": [ + { + "resources": ["provider.js"], + "matches": ["https://*/*", "http://localhost/*", "http://127.0.0.1/*"] + } + ] +} diff --git a/extension/src/popup.html b/extension/src/popup.html new file mode 100644 index 0000000..39295ad --- /dev/null +++ b/extension/src/popup.html @@ -0,0 +1,652 @@ + + + + + + Contractless Wallet + + + +
+
+
+ Contractless +
+
+

Opening wallet

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/extension/src/popup.ts b/extension/src/popup.ts new file mode 100644 index 0000000..4d82910 --- /dev/null +++ b/extension/src/popup.ts @@ -0,0 +1,2293 @@ +import type { WalletStatus } from "./types"; +import QRCode from "qrcode"; +import { core } from "./core"; + +const byId = (id: string) => document.getElementById(id) as T; +const screens = Array.from(document.querySelectorAll(".screen")); +const notice = byId("notice"); +const firefoxToolbarPopup = navigator.userAgent.includes("Firefox"); + +let pendingPassword = ""; +let currentPassword = ""; +let selectedImageBase64 = ""; +let backupImageBase64 = ""; +let walletStatus: WalletStatus | undefined; +let pendingBackupAction: "image" | "private" | "print" | undefined; +let tokenFilter: "held" | "all" = "held"; +let nftPage = 1; +let nftLoadId = 0; +const NFT_PAGE_SIZE = 8; +let walletBalances: + | { + balances: Array<{ + asset: string; + nft_series: number; + balance: string; + }>; + tokens: Array<{ + token: string; + origin_txid: string; + }>; + nfts: Array<{ + name: string; + series: number; + origin_txid: string; + ownership_type: number; + supply: string; + }>; + tokenCatalogError?: string; + nftCatalogError?: string; + } + | undefined; + +type ActivityRecord = { + txid: string; + block_height: number; + transaction_hex: string; + miner_earnings?: Array<{ + type: string; + asset: string; + nft_series: number; + amount_atomic: string; + }>; +}; + +type OwnedNft = { + name: string; + series: number; + origin_txid: string; + ownership_type: number; + supply: string; + balance: string; + details?: NftDetails; + loading?: boolean; + error?: string; +}; + +type NftDetails = { + nft_name: string; + series: number; + asset_name: string; + genesis_txid: string; + creator: string; + item_ipfs: string; + metadata_uri: string; + current_holder?: string | null; + history_count: number; + history: Array<{ + txid: string; + block: number; + transaction_type: number; + action: string; + from?: string | null; + to?: string | null; + received_asset?: string; + received_series?: number; + received_value?: string; + }>; + metadata: Record; + image_data_url: string; + image_available: boolean; + media_error?: string; +}; + +let ownedNfts: OwnedNft[] = []; +let selectedNft: OwnedNft | undefined; +let sendNft: OwnedNft | undefined; + +type DecodedTransaction = { + txtype: number; + transaction_name: string; + fields: Record; + stored_hash?: string | null; + signature?: string | null; + signature1?: string | null; + signature2?: string | null; +}; + +let selectedTransaction: ActivityRecord | undefined; +let transactionReturnScreen: "activity" | "nft" = "activity"; +let pendingTransfer: + | { + transaction: Record; + receiverInput: string; + asset: string; + amount: string; + fee: string; + total: string; + } + | undefined; +let lastSentTxid = ""; +let pendingProviderApprovalId = ""; +const CONFIRMED_AFTER_BLOCKS = 20; + +const TRANSACTION_NAMES: Record = { + 1: "Mining Reward", + 2: "Transfer", + 3: "Token Creation", + 4: "NFT/RWA Creation", + 5: "Marketing", + 6: "Swap", + 7: "Loan", + 8: "Loan Payment", + 9: "Collateral Claim", + 10: "Asset Burn", + 11: "Token Issuance", + 12: "Vanity Address", + 100: "Storage Key", + 101: "Data Storage Bool", + 102: "Data Storage U8", + 103: "Data Storage U16", + 104: "Data Storage U32", + 105: "Data Storage U64", + 106: "Data Storage U128", + 107: "Data Storage String", + 108: "Data Storage I8", + 109: "Data Storage I16", + 110: "Data Storage I32", + 111: "Data Storage I64", + 112: "Data Storage I128", + 113: "Delete Stored Data", + 200: "Proposal", + 201: "Proposal Vote", + 202: "Activation Vote" +}; + +async function send(type: string, values: Record = {}) { + const response = await chrome.runtime.sendMessage({ type, ...values }); + if (!response?.ok) throw new Error(response?.error ?? "Wallet request failed."); + return response.result; +} + +function showScreen(id: string, clearNotice = true): void { + screens.forEach((screen) => { + screen.hidden = screen.id !== id; + }); + if (clearNotice) notice.textContent = ""; +} + +function showStatus(status: WalletStatus): void { + walletStatus = status; + const networkName = status.networkName; + const networkSymbol = status.networkSymbol; + document.querySelectorAll("[data-network-name]").forEach((element) => { + element.textContent = networkName; + }); + document.querySelectorAll("[data-network-symbol]").forEach((element) => { + element.textContent = networkSymbol; + }); + byId("base-asset-name").textContent = `Contractless ${networkName}`; + byId("wallet-address").textContent = status.address ?? ""; + byId("wallet-address-short").textContent = status.address + ? `${status.address.slice(0, 6)}...${status.address.slice(-6)}` + : "Account"; + byId("settings-testnet-api-url").value = status.testnetApiUrl; + byId("settings-mainnet-api-url").value = status.mainnetApiUrl; + document.querySelectorAll("[data-select-network]").forEach((button) => { + const active = button.dataset.selectNetwork === status.selectedNetwork; + button.classList.toggle("active", active); + button.setAttribute("aria-pressed", String(active)); + }); + byId("settings-lock-timeout").value = String(status.autoLockMinutes); + renderKnownAddresses(status); + if (!status.exists) { + showScreen("create-screen"); + } else if (!status.unlocked) { + showScreen("locked-screen"); + } else { + void showPendingProviderApproval().then((shown) => { + if (!shown) { + showScreen("dashboard-screen"); + void loadWalletBalances(); + } + }); + } +} + +function approvalTitle(method: string, display: Record): string { + if (method === "contractless_connect") return "Connect application"; + if (method === "contractless_signMessage") return "Sign text message"; + if ( + method === "contractless_sendTransaction" + || method === "contractless_signDualTransaction" + ) { + return `Sign ${String(display.transaction_type ?? "transaction")}`; + } + return "Review request"; +} + +function appendApprovalValue( + container: HTMLElement, + label: string, + value: unknown +): void { + const term = document.createElement("dt"); + term.textContent = label.replaceAll("_", " "); + const description = document.createElement("dd"); + description.textContent = typeof value === "object" + ? JSON.stringify(value, null, 2) + : String(value); + container.append(term, description); +} + +function swapApprovalAmount( + fields: Record, + valueField: "value1" | "value2" | "tip1" | "tip2", + tickerField: "ticker1" | "ticker2", + seriesField: "nft_series1" | "nft_series2" +): string { + const amount = formatAtomicDisplay(fields[valueField]); + const ticker = String(fields[tickerField] ?? "").trim().toUpperCase(); + const series = Number(fields[seriesField] ?? 0); + return `${amount} ${ticker}${series > 0 ? ` #${series}` : ""}`; +} + +function appendSwapApprovalCosts( + container: HTMLElement, + fields: Record, + signerSlot: 1 | 2 +): void { + const otherSlot = signerSlot === 1 ? 2 : 1; + const baseSymbol = walletStatus?.networkSymbol ?? "CLTC"; + appendApprovalValue( + container, + "Your miner tip", + swapApprovalAmount( + fields, + `tip${signerSlot}`, + `ticker${signerSlot}`, + `nft_series${signerSlot}` + ) + ); + appendApprovalValue( + container, + "Your transaction fee", + `${formatAtomicDisplay(fields[`txfee${signerSlot}`])} ${baseSymbol}` + ); + appendApprovalValue( + container, + "Other party miner tip", + swapApprovalAmount( + fields, + `tip${otherSlot}`, + `ticker${otherSlot}`, + `nft_series${otherSlot}` + ) + ); + appendApprovalValue( + container, + "Other party transaction fee", + `${formatAtomicDisplay(fields[`txfee${otherSlot}`])} ${baseSymbol}` + ); +} + +function appendSwapApprovalSummary( + container: HTMLElement, + fields: Record +): void { + const activeAddress = walletStatus?.address?.trim().toLowerCase() ?? ""; + const sender1 = String(fields.sender1 ?? "").trim().toLowerCase(); + const sender2 = String(fields.sender2 ?? "").trim().toLowerCase(); + const signerSlot = activeAddress === sender1 ? 1 : activeAddress === sender2 ? 2 : 0; + + if (signerSlot === 1) { + appendApprovalValue( + container, + "Sending", + swapApprovalAmount(fields, "value1", "ticker1", "nft_series1") + ); + appendApprovalValue( + container, + "Receiving", + swapApprovalAmount(fields, "value2", "ticker2", "nft_series2") + ); + appendSwapApprovalCosts(container, fields, 1); + return; + } + + if (signerSlot === 2) { + appendApprovalValue( + container, + "Sending", + swapApprovalAmount(fields, "value2", "ticker2", "nft_series2") + ); + appendApprovalValue( + container, + "Receiving", + swapApprovalAmount(fields, "value1", "ticker1", "nft_series1") + ); + appendSwapApprovalCosts(container, fields, 2); + return; + } + + appendApprovalValue( + container, + "First party sending", + swapApprovalAmount(fields, "value1", "ticker1", "nft_series1") + ); + appendApprovalValue( + container, + "Second party sending", + swapApprovalAmount(fields, "value2", "ticker2", "nft_series2") + ); +} + +function setApprovalProcessing(action: "approve" | "reject" | null): void { + const approve = byId("approve-provider-approval"); + const reject = byId("reject-provider-approval"); + const processing = action !== null; + approve.disabled = processing; + reject.disabled = processing; + approve.classList.toggle("is-processing", action === "approve"); + reject.classList.toggle("is-processing", action === "reject"); + approve.textContent = action === "approve" ? "Processing..." : "Approve"; + reject.textContent = action === "reject" ? "Rejecting..." : "Reject"; +} + +async function showPendingProviderApproval(): Promise { + const pending = await send("pending-provider-approval") as { + locked?: boolean; + position?: number; + total?: number; + request?: { + id: string; + method: string; + origin: string; + risk: "safe" | "low" | "high"; + display?: Record; + }; + } | null; + if (!pending?.request) return false; + + const request = pending.request; + setApprovalProcessing(null); + pendingProviderApprovalId = request.id; + const queueLabel = (pending.total ?? 0) > 1 + ? ` · Request ${pending.position ?? 1} of ${pending.total}` + : ""; + byId("provider-approval-origin").textContent = `${request.origin}${queueLabel}`; + const risk = byId("provider-approval-risk"); + risk.className = `risk-label risk-${request.risk}`; + risk.textContent = request.risk === "safe" + ? "Safe" + : request.risk === "low" + ? "Low risk" + : "High risk"; + const display = request.display ?? {}; + const transactionRequest = request.method === "contractless_sendTransaction" + || request.method === "contractless_signDualTransaction"; + byId("provider-approval-title").textContent = approvalTitle(request.method, display); + byId("provider-approval-intro").textContent = transactionRequest + ? "Review every transaction field before signing." + : "Review this request before approving it."; + const details = byId("provider-approval-details"); + details.replaceChildren(); + Object.entries(display).forEach(([label, value]) => { + if (label === "transaction_fields" && value && typeof value === "object") { + const fields = value as Record; + const txtype = Number(fields.txtype ?? 0); + const swapAmountFields = new Set([ + "ticker1", + "nft_series1", + "value1", + "ticker2", + "nft_series2", + "value2", + "tip1", + "tip2", + "txfee1", + "txfee2" + ]); + if (txtype === 6) appendSwapApprovalSummary(details, fields); + Object.entries(fields).forEach(([field, fieldValue]) => { + if (txtype === 6 && swapAmountFields.has(field)) return; + appendApprovalValue( + details, + field === "txtype" ? "transaction_type_id" : field, + transactionFieldDisplay(txtype, field, fieldValue) + ); + }); + return; + } + appendApprovalValue(details, label, value); + }); + showScreen("provider-approval-screen"); + return true; +} + +async function showNextProviderApproval( + remaining: number, + waitForFollowup: boolean +): Promise { + if (remaining > 0 && await showPendingProviderApproval()) return; + + if (waitForFollowup) { + const expiresAt = Date.now() + 1_500; + while (Date.now() < expiresAt) { + if (await showPendingProviderApproval()) return; + await new Promise((resolve) => setTimeout(resolve, 75)); + } + } + + window.close(); +} + +function decimalHasValue(value: string): boolean { + return value.replace(/[.0]/g, "").length > 0; +} + +function renderTokenBalances(): void { + const list = byId("token-list"); + list.replaceChildren(); + if (!walletBalances) { + const empty = document.createElement("div"); + empty.className = "empty-assets"; + const title = document.createElement("strong"); + title.textContent = "Loading token balances..."; + const description = document.createElement("span"); + description.textContent = "Balances are retrieved from the selected Contractless API."; + empty.append(title, description); + list.append(empty); + return; + } + + const baseSymbol = walletStatus?.networkSymbol ?? "CLTC"; + const tokenNames = new Set( + walletBalances.tokens.map((token) => token.token.trim().toUpperCase()) + ); + const nftKeys = new Set( + walletBalances.nfts.map( + (nft) => `${nft.name.trim().toUpperCase()}|${Number(nft.series || 0)}` + ) + ); + const fungibleBalances = walletBalances.balances.filter((balance) => { + const asset = balance.asset.trim().toUpperCase(); + const key = `${asset}|${Number(balance.nft_series || 0)}`; + return ( + balance.nft_series === 0 + && asset !== "CLTC" + && asset !== "CLC" + && tokenNames.has(asset) + && !nftKeys.has(key) + ); + }); + const balancesByAsset = new Map( + fungibleBalances.map((balance) => [balance.asset.trim().toUpperCase(), balance]) + ); + + const displayedTokenNames = tokenFilter === "all" + ? walletBalances.tokens.map((token) => token.token) + : fungibleBalances + .filter((balance) => decimalHasValue(balance.balance)) + .map((balance) => balance.asset); + const uniqueNames = [...new Set(displayedTokenNames.map((name) => name.trim().toUpperCase()))] + .filter((name) => name !== baseSymbol && !nftKeys.has(`${name}|0`)) + .sort((left, right) => left.localeCompare(right)); + + if (uniqueNames.length === 0) { + const empty = document.createElement("div"); + empty.className = "empty-assets"; + const title = document.createElement("strong"); + title.textContent = tokenFilter === "held" + ? "No token balances found" + : "No tokens found"; + const description = document.createElement("span"); + description.textContent = tokenFilter === "held" + ? "Switch to All tokens to view the complete network token list." + : walletBalances.tokenCatalogError + ? `Token catalog unavailable: ${walletBalances.tokenCatalogError}` + : "The selected API did not return any fungible tokens."; + empty.append(title, description); + list.append(empty); + return; + } + + uniqueNames.forEach((name) => { + const balance = balancesByAsset.get(name); + const row = document.createElement("div"); + row.className = "asset-row"; + + const symbol = document.createElement("div"); + symbol.className = "asset-symbol token-symbol"; + symbol.textContent = name.slice(0, 3); + + const identity = document.createElement("div"); + identity.className = "asset-name"; + const title = document.createElement("strong"); + title.textContent = name; + const subtitle = document.createElement("span"); + subtitle.textContent = "Contractless token"; + identity.append(title, subtitle); + + const value = document.createElement("div"); + value.className = "asset-value"; + const amount = document.createElement("strong"); + amount.textContent = balance?.balance ?? "0.00000000"; + const ticker = document.createElement("span"); + ticker.textContent = name; + value.append(amount, ticker); + + row.append(symbol, identity, value); + list.append(row); + }); +} + +async function loadWalletBalances(): Promise { + if (!walletStatus?.unlocked) return; + walletBalances = undefined; + byId("base-balance").textContent = "--"; + byId("base-asset-balance").textContent = "--"; + renderTokenBalances(); + try { + if (!walletStatus.apiUrl) { + throw new Error(`No ${walletStatus.networkName} API is configured.`); + } + const result = await send("wallet-balances") as { + balances: Array<{ asset: string; nft_series: number; balance: string }>; + tokens: Array<{ token: string; origin_txid: string }>; + nfts: Array<{ + name: string; + series: number; + origin_txid: string; + ownership_type: number; + supply: string; + }>; + tokenCatalogError?: string; + nftCatalogError?: string; + }; + walletBalances = { + balances: Array.isArray(result.balances) ? result.balances : [], + tokens: Array.isArray(result.tokens) ? result.tokens : [], + nfts: Array.isArray(result.nfts) ? result.nfts : [], + tokenCatalogError: result.tokenCatalogError, + nftCatalogError: result.nftCatalogError + }; + const baseSymbol = walletStatus.networkSymbol; + const base = walletBalances.balances.find( + (balance) => + balance.nft_series === 0 && balance.asset.toUpperCase() === baseSymbol + ); + const baseBalance = base?.balance ?? "0.00000000"; + byId("base-balance").textContent = baseBalance; + byId("base-asset-balance").textContent = baseBalance; + renderTokenBalances(); + } catch (error) { + const list = byId("token-list"); + list.replaceChildren(); + const empty = document.createElement("div"); + empty.className = "empty-assets balance-error"; + const title = document.createElement("strong"); + title.textContent = "Unable to load balances"; + const description = document.createElement("span"); + description.textContent = errorText(error); + empty.append(title, description); + list.append(empty); + notice.textContent = errorText(error); + } +} + +function nftKey(name: string, series: number): string { + return `${name.trim().toUpperCase()}|${series}`; +} + +function positiveDecimal(value: string): boolean { + try { + return decimalToAtomic(value, "Balance") > 0n; + } catch { + return false; + } +} + +function nftOwnership(nft: OwnedNft): string { + if (nft.ownership_type !== 1) return "Complete ownership"; + try { + const balance = decimalToAtomic(nft.balance, "Balance"); + const supply = decimalToAtomic(nft.supply, "Supply"); + if (supply <= 0n) return "0% ownership"; + const scaled = balance * 100_000_000n / supply; + const whole = scaled / 1_000_000n; + const fraction = (scaled % 1_000_000n) + .toString() + .padStart(6, "0") + .replace(/0+$/, ""); + return `${whole}${fraction ? `.${fraction}` : ""}% ownership`; + } catch { + return "Fractional ownership"; + } +} + +function nftMetadata(nft: OwnedNft): Record { + return nft.details?.metadata && typeof nft.details.metadata === "object" + ? nft.details.metadata + : {}; +} + +function nftDisplayName(nft: OwnedNft): string { + const metadataName = nftMetadata(nft).name; + if (typeof metadataName === "string" && metadataName.trim()) { + return metadataName.trim(); + } + return nft.series > 0 ? `${nft.name} #${nft.series}` : nft.name; +} + +function nftKind(nft: OwnedNft): "NFT" | "RWA" { + const metadata = nftMetadata(nft); + const contractless = metadata.contractless; + if ( + contractless + && typeof contractless === "object" + && String((contractless as Record).metadata_type ?? "") + .toLowerCase() === "rwa" + ) { + return "RWA"; + } + return "NFT"; +} + +function nftAssetLabel(nft: OwnedNft): string { + return nft.series > 0 ? `${nft.name}_${nft.series}` : nft.name; +} + +function shortenNftValue(value: string, start = 7, end = 7): string { + return value.length > start + end + 3 + ? `${value.slice(0, start)}...${value.slice(-end)}` + : value; +} + +function createNftMedia(nft: OwnedNft, className: string): HTMLElement { + const media = document.createElement("div"); + media.className = className; + if (nft.details?.image_data_url) { + const image = document.createElement("img"); + image.src = nft.details.image_data_url; + image.alt = nftDisplayName(nft); + media.append(image); + return media; + } + const fallback = document.createElement("span"); + fallback.textContent = nft.loading ? "..." : nft.name.slice(0, 2).toUpperCase(); + media.append(fallback); + return media; +} + +function visibleNfts(): OwnedNft[] { + const start = (nftPage - 1) * NFT_PAGE_SIZE; + return ownedNfts.slice(start, start + NFT_PAGE_SIZE); +} + +function renderNftGallery(): void { + const list = byId("nfts-list"); + list.replaceChildren(); + const totalPages = Math.max(1, Math.ceil(ownedNfts.length / NFT_PAGE_SIZE)); + nftPage = Math.min(Math.max(1, nftPage), totalPages); + byId("nfts-count").textContent = ownedNfts.length === 1 + ? "1 owned asset" + : `${ownedNfts.length} owned assets`; + byId("nfts-page").textContent = String(nftPage); + byId("nfts-pages").textContent = String(totalPages); + byId("nfts-pagination").hidden = totalPages <= 1; + byId("nfts-previous").disabled = nftPage <= 1; + byId("nfts-next").disabled = nftPage >= totalPages; + + if (!ownedNfts.length) { + const empty = document.createElement("div"); + empty.className = "nfts-empty"; + const title = document.createElement("strong"); + title.textContent = "No NFTs or RWAs found"; + const copy = document.createElement("span"); + copy.textContent = walletBalances?.nftCatalogError + ? `NFT catalog unavailable: ${walletBalances.nftCatalogError}` + : "Assets owned by this wallet will appear here."; + empty.append(title, copy); + list.append(empty); + return; + } + + visibleNfts().forEach((nft) => { + const card = document.createElement("button"); + card.type = "button"; + card.className = "nft-card"; + card.append(createNftMedia(nft, "nft-card-media")); + + const identity = document.createElement("div"); + identity.className = "nft-card-identity"; + const kind = document.createElement("span"); + kind.textContent = nftKind(nft); + const name = document.createElement("strong"); + name.textContent = nftDisplayName(nft); + const asset = document.createElement("small"); + asset.textContent = nftAssetLabel(nft); + const ownership = document.createElement("b"); + ownership.textContent = nftOwnership(nft); + identity.append(kind, name, asset, ownership); + if (nft.error) { + const error = document.createElement("em"); + error.textContent = "Media unavailable"; + identity.append(error); + } + card.append(identity); + card.addEventListener("click", () => openNftDetails(nft)); + list.append(card); + }); +} + +function appendNftDetailField( + container: HTMLElement, + label: string, + value: string +): void { + const row = document.createElement("div"); + const term = document.createElement("dt"); + term.textContent = label; + const description = document.createElement("dd"); + description.textContent = value || "--"; + row.append(term, description); + container.append(row); +} + +function renderNftDetails(nft: OwnedNft): void { + selectedNft = nft; + const details = nft.details; + const metadata = nftMetadata(nft); + const kind = nftKind(nft); + const name = nftDisplayName(nft); + byId("nft-detail-title").textContent = name; + byId("nft-detail-kind").textContent = `${kind} details`; + byId("nft-detail-badge").textContent = kind; + byId("nft-detail-name").textContent = name; + byId("nft-detail-ownership").textContent = nftOwnership(nft); + + const image = byId("nft-detail-image"); + image.src = details?.image_data_url ?? ""; + image.alt = name; + image.hidden = !details?.image_data_url; + + const description = metadata.description ?? metadata.summary; + byId("nft-detail-description").textContent = + typeof description === "string" && description.trim() + ? description.trim() + : details + ? "No description was included in the NFT metadata." + : "Loading NFT metadata and provenance..."; + + const attributes = byId("nft-detail-attributes"); + attributes.replaceChildren(); + const metadataAttributes = Array.isArray(metadata.attributes) + ? metadata.attributes.slice(0, 12) + : []; + metadataAttributes.forEach((attribute) => { + if (!attribute || typeof attribute !== "object") return; + const values = attribute as Record; + const item = document.createElement("div"); + const label = document.createElement("span"); + label.textContent = String(values.trait_type ?? values.type ?? "Attribute"); + const value = document.createElement("strong"); + value.textContent = String(values.value ?? ""); + item.append(label, value); + attributes.append(item); + }); + attributes.hidden = attributes.childElementCount === 0; + + const fields = byId("nft-detail-fields"); + fields.replaceChildren(); + appendNftDetailField(fields, "Asset", nftAssetLabel(nft)); + appendNftDetailField(fields, "Creator", details?.creator ?? "--"); + appendNftDetailField(fields, "IPFS CID", details?.item_ipfs ?? "--"); + appendNftDetailField(fields, "Origin transaction", details?.genesis_txid ?? nft.origin_txid); + + const history = byId("nft-detail-history"); + history.replaceChildren(); + if (!details) { + const loading = document.createElement("p"); + loading.textContent = "Loading provenance..."; + history.append(loading); + } else if (!details.history.length) { + const empty = document.createElement("p"); + empty.textContent = "No provenance records were returned."; + history.append(empty); + } else { + details.history.slice().reverse().forEach((entry) => { + const row = document.createElement("button"); + row.type = "button"; + row.className = "nft-history-row"; + row.title = `View full transaction ${entry.txid}`; + const action = document.createElement("strong"); + action.textContent = entry.action; + const block = document.createElement("span"); + block.textContent = `Block ${Number(entry.block).toLocaleString()}`; + const transaction = document.createElement("small"); + transaction.textContent = shortenNftValue(entry.txid); + row.append(action, block, transaction); + row.addEventListener("click", () => { + void openTransaction( + { + txid: entry.txid, + block_height: Number(entry.block), + transaction_hex: "" + }, + "nft" + ); + }); + history.append(row); + }); + } +} + +function openNftDetails(nft: OwnedNft): void { + renderNftDetails(nft); + showScreen("nft-detail-screen"); +} + +async function hydrateVisibleNfts(loadId: number): Promise { + const queue = visibleNfts().filter((nft) => !nft.details && !nft.loading); + let cursor = 0; + const worker = async (): Promise => { + while (cursor < queue.length && loadId === nftLoadId) { + const nft = queue[cursor++]; + nft.loading = true; + renderNftGallery(); + try { + nft.details = await send("wallet-nft-details", { + name: nft.name, + series: nft.series + }) as NftDetails; + nft.error = nft.details.media_error; + } catch (error) { + nft.error = errorText(error); + } finally { + nft.loading = false; + } + if (loadId !== nftLoadId) return; + renderNftGallery(); + if (selectedNft === nft) renderNftDetails(nft); + } + }; + await Promise.all([worker(), worker()]); +} + +async function loadOwnedNfts(force = false): Promise { + const loadId = ++nftLoadId; + byId("nfts-count").textContent = "Loading ownership..."; + if (!walletBalances || force) { + await loadWalletBalances(); + } + if (loadId !== nftLoadId || !walletBalances) return; + + const previous = force + ? new Map() + : new Map(ownedNfts.map((nft) => [nftKey(nft.name, nft.series), nft])); + ownedNfts = walletBalances.nfts + .map((catalog): OwnedNft | null => { + const balance = walletBalances?.balances.find( + (entry) => + nftKey(entry.asset, Number(entry.nft_series || 0)) + === nftKey(catalog.name, Number(catalog.series || 0)) + ); + if (!balance || !positiveDecimal(balance.balance)) return null; + const existing = previous.get(nftKey(catalog.name, catalog.series)); + return { + name: catalog.name.trim(), + series: Number(catalog.series || 0), + origin_txid: catalog.origin_txid, + ownership_type: Number(catalog.ownership_type || 0), + supply: catalog.supply, + balance: balance.balance, + details: existing?.details, + error: existing?.error + }; + }) + .filter((nft): nft is OwnedNft => nft !== null) + .sort((left, right) => + left.name.localeCompare(right.name) || left.series - right.series + ); + const totalPages = Math.max(1, Math.ceil(ownedNfts.length / NFT_PAGE_SIZE)); + nftPage = Math.min(nftPage, totalPages); + renderNftGallery(); + await hydrateVisibleNfts(loadId); +} + +function renderKnownAddresses(status: WalletStatus): void { + const container = byId("known-addresses"); + container.replaceChildren(); + + status.wallets.forEach((wallet) => { + const row = document.createElement("div"); + row.className = `known-address${wallet.active ? " active-address" : ""}`; + + const select = document.createElement("button"); + select.type = "button"; + select.className = "address-select"; + select.dataset.walletId = wallet.id; + + const avatar = document.createElement("span"); + avatar.className = "address-avatar"; + const logo = document.createElement("img"); + logo.src = chrome.runtime.getURL("contractless-logo.png"); + logo.alt = ""; + avatar.append(logo); + + const details = document.createElement("span"); + details.className = "address-details"; + const label = document.createElement("strong"); + label.textContent = wallet.label; + const address = document.createElement("small"); + address.textContent = wallet.address; + details.append(label, address); + + const selected = document.createElement("span"); + selected.className = "selected-mark"; + selected.textContent = wallet.active ? "✓" : ""; + select.append(avatar, details, selected); + + select.addEventListener("click", () => { + if (wallet.active) { + setAccountDrawer(false); + return; + } + void action(async () => { + setAccountDrawer(false); + currentPassword = ""; + backupImageBase64 = ""; + showStatus(await send("switch-wallet", { walletId: wallet.id }) as WalletStatus); + }); + }); + + const rename = document.createElement("button"); + rename.type = "button"; + rename.className = "rename-address"; + rename.title = "Rename wallet"; + rename.setAttribute("aria-label", `Rename ${wallet.label}`); + rename.textContent = "✎"; + rename.addEventListener("click", () => { + const nextLabel = window.prompt("Wallet label", wallet.label); + if (nextLabel === null || nextLabel.trim() === wallet.label) return; + void action(async () => { + const nextStatus = await send("rename-wallet", { + walletId: wallet.id, + label: nextLabel + }) as WalletStatus; + showStatus(nextStatus); + setAccountDrawer(true); + }); + }); + + row.append(select, rename); + container.append(row); + }); +} + +function setAccountDrawer(open: boolean): void { + const drawer = byId("account-drawer"); + const backdrop = byId("drawer-backdrop"); + const trigger = byId("account-menu-button"); + drawer.classList.toggle("open", open); + drawer.setAttribute("aria-hidden", String(!open)); + trigger.setAttribute("aria-expanded", String(open)); + backdrop.hidden = !open; +} + +function clearRevealedPrivateKey(): void { + byId("revealed-private-key").value = ""; + byId("private-key-result").hidden = true; +} + +function setBackupKeyModal(open: boolean): void { + byId("backup-key-modal").hidden = !open; + byId("backup-key-backdrop").hidden = !open; + if (open) { + const input = byId("backup-key-input"); + input.value = ""; + input.focus(); + } else { + byId("backup-key-input").value = ""; + pendingBackupAction = undefined; + } +} + +async function openReceiveScreen(): Promise { + const result = await send("wallet-receive-address"); + const address = String(result.receiveAddress ?? ""); + if (!address) throw new Error("No active wallet address is available."); + byId("receive-address-label").textContent = result.vanity + ? "Vanity address" + : "Wallet address"; + byId("receive-address-value").textContent = address; + const canvas = byId("receive-qr"); + await QRCode.toCanvas(canvas, address, { + width: 220, + margin: 1, + errorCorrectionLevel: "M", + color: { + dark: "#071016", + light: "#ffffff" + } + }); + showScreen("receive-screen"); +} + +function decimalToAtomic(value: string, label: string): bigint { + const normalized = value.trim(); + if (!/^(?:0|[1-9]\d*)(?:\.\d{1,8})?$/.test(normalized)) { + throw new Error(`${label} must be a positive number with no more than 8 decimal places.`); + } + const [whole, fraction = ""] = normalized.split("."); + return BigInt(whole) * 100_000_000n + BigInt(fraction.padEnd(8, "0")); +} + +function atomicToDecimal(value: bigint): string { + const whole = value / 100_000_000n; + const fraction = (value % 100_000_000n).toString().padStart(8, "0"); + return `${whole}.${fraction}`; +} + +function selectedSendBalance(): string { + if (sendNft) return sendNft.balance; + const asset = byId("send-asset").value; + const balance = walletBalances?.balances.find( + (entry) => entry.nft_series === 0 && entry.asset.trim().toUpperCase() === asset + ); + return balance?.balance ?? "0.00000000"; +} + +function minimumSendFeeAtomic(): bigint { + const asset = byId("send-asset").value; + const baseSymbol = walletStatus?.networkSymbol ?? "CLTC"; + if (asset !== baseSymbol) return 100_000_000n; + const amount = byId("send-amount").value.trim(); + if (!amount) return 0n; + try { + const atomic = decimalToAtomic(amount, "Amount"); + return (atomic + 99n) / 100n; + } catch { + return 0n; + } +} + +function updateSendForm(): void { + const asset = sendNft?.name ?? byId("send-asset").value; + const baseSymbol = walletStatus?.networkSymbol ?? "CLTC"; + const assetLabel = sendNft ? nftAssetLabel(sendNft) : asset; + byId("send-amount-symbol").textContent = assetLabel; + byId("send-available").textContent = + `Available: ${selectedSendBalance()} ${assetLabel}`; + const minimumFee = minimumSendFeeAtomic(); + byId("send-fee").value = atomicToDecimal(minimumFee); + byId("send-fee-note").textContent = !sendNft && asset === baseSymbol + ? "Minimum fee: 1% of the transfer amount." + : `Minimum fee: 1 ${baseSymbol} for asset transfers.`; +} + +function populateSendAssets(): void { + const select = byId("send-asset"); + select.replaceChildren(); + const baseSymbol = walletStatus?.networkSymbol ?? "CLTC"; + const assets = new Set([baseSymbol]); + walletBalances?.tokens.forEach((token) => { + const symbol = token.token.trim().toUpperCase(); + const balance = walletBalances?.balances.find( + (entry) => + entry.nft_series === 0 && + entry.asset.trim().toUpperCase() === symbol && + decimalToAtomic(entry.balance, "Balance") > 0n + ); + if (balance) assets.add(symbol); + }); + assets.forEach((asset) => { + const option = document.createElement("option"); + option.value = asset; + option.textContent = asset; + select.append(option); + }); + updateSendForm(); +} + +function openSendScreen(): void { + if (!walletBalances) throw new Error("Wallet balances are still loading."); + sendNft = undefined; + pendingTransfer = undefined; + byId("send-form").reset(); + byId("send-asset").disabled = false; + byId("send-amount").disabled = false; + populateSendAssets(); + showScreen("send-screen"); +} + +function openNftSendScreen(nft: OwnedNft): void { + if (!walletBalances) throw new Error("Wallet balances are still loading."); + sendNft = nft; + pendingTransfer = undefined; + byId("send-form").reset(); + + const select = byId("send-asset"); + select.replaceChildren(); + const option = document.createElement("option"); + option.value = nft.name; + option.textContent = nftAssetLabel(nft); + select.append(option); + select.disabled = true; + + const amount = byId("send-amount"); + if (nft.ownership_type === 0) { + amount.value = "1.00000000"; + amount.disabled = true; + } else { + amount.value = ""; + amount.disabled = false; + } + + updateSendForm(); + showScreen("send-screen"); +} + +async function prepareTransfer(): Promise { + if (!walletStatus?.address) throw new Error("No active wallet is available."); + const asset = sendNft?.name ?? byId("send-asset").value; + const assetLabel = sendNft ? nftAssetLabel(sendNft) : asset; + const receiver = requireValue( + byId("send-recipient").value, + "Enter a receiving address." + ); + const amount = requireValue( + byId("send-amount").value, + "Enter an amount to send." + ); + const fee = requireValue( + byId("send-fee").value, + "Enter a transaction fee." + ); + const amountAtomic = decimalToAtomic(amount, "Amount"); + const feeAtomic = decimalToAtomic(fee, "Transaction fee"); + if (amountAtomic === 0n) throw new Error("The transfer amount must be greater than zero."); + if (sendNft?.ownership_type === 0 && amountAtomic !== 100_000_000n) { + throw new Error("An indivisible NFT must be transferred as one complete asset."); + } + if (sendNft?.ownership_type === 1 && amountAtomic > 100_000_000n) { + throw new Error("A fractional NFT/RWA transfer cannot exceed one complete asset."); + } + const minimumFee = minimumSendFeeAtomic(); + if (feeAtomic < minimumFee) { + throw new Error(`The minimum transaction fee is ${atomicToDecimal(minimumFee)}.`); + } + const assetBalance = decimalToAtomic(selectedSendBalance(), "Available balance"); + if (amountAtomic > assetBalance) throw new Error(`Insufficient ${asset} balance.`); + + const baseSymbol = walletStatus.networkSymbol; + const baseBalanceText = walletBalances?.balances.find( + (entry) => + entry.nft_series === 0 && entry.asset.trim().toUpperCase() === baseSymbol + )?.balance ?? "0.00000000"; + const baseBalance = decimalToAtomic(baseBalanceText, "Base balance"); + const baseRequired = asset === baseSymbol ? amountAtomic + feeAtomic : feeAtomic; + if (baseRequired > baseBalance) throw new Error(`Insufficient ${baseSymbol} balance including the fee.`); + + const result = await send("wallet-prepare-transfer", { + receiver, + asset, + nftSeries: sendNft?.series ?? 0, + valueAtomic: amountAtomic.toString(), + feeAtomic: feeAtomic.toString() + }) as { + transaction: Record; + review: Record; + receiverInput: string; + }; + const total = asset === baseSymbol + ? `${atomicToDecimal(amountAtomic + feeAtomic)} ${baseSymbol}` + : `${atomicToDecimal(amountAtomic)} ${assetLabel} + ${atomicToDecimal(feeAtomic)} ${baseSymbol}`; + pendingTransfer = { + transaction: result.transaction, + receiverInput: result.receiverInput, + asset, + amount: atomicToDecimal(amountAtomic), + fee: atomicToDecimal(feeAtomic), + total + }; + byId("review-amount").textContent = pendingTransfer.amount; + byId("review-asset").textContent = assetLabel; + byId("review-sender").textContent = walletStatus.address; + byId("review-recipient").textContent = result.receiverInput; + byId("review-resolved-recipient").textContent = String(result.review.receiver ?? ""); + byId("review-fee").textContent = pendingTransfer.fee; + byId("review-total").textContent = total; + showScreen("send-review-screen"); +} + +function transactionTimestamp(transactionHex: string): Date | undefined { + if (!/^[0-9a-f]+$/i.test(transactionHex) || transactionHex.length < 10) return undefined; + const bytes = transactionHex.slice(2, 10).match(/../g); + if (!bytes || bytes.length !== 4) return undefined; + const seconds = bytes.reduce( + (value, byte, index) => value + (Number.parseInt(byte, 16) * (2 ** (index * 8))), + 0 + ); + const date = new Date(seconds * 1_000); + return Number.isNaN(date.getTime()) ? undefined : date; +} + +function fieldLabel(name: string): string { + return name + .split("_") + .map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : part) + .join(" "); +} + +function formatAtomicDisplay(value: unknown): string { + try { + const decimal = atomicToDecimal(BigInt(String(value ?? "0"))); + const [whole, fraction] = decimal.split("."); + const trimmed = fraction.replace(/0+$/, ""); + return trimmed ? `${whole}.${trimmed}` : `${whole}.0`; + } catch { + return String(value ?? ""); + } +} + +function isAtomicTransactionField(txtype: number, name: string): boolean { + if (name.startsWith("txfee")) return true; + const fieldsByType: Record> = { + 1: new Set(["value"]), + 2: new Set(["value"]), + 3: new Set(["number"]), + 6: new Set(["value1", "value2", "tip1", "tip2"]), + 7: new Set([ + "loan_amount", + "collateral_amount", + "payment_amount", + "max_late_value" + ]), + 8: new Set(["payback_amount", "tip"]), + 10: new Set(["value"]), + 11: new Set(["number"]) + }; + return fieldsByType[txtype]?.has(name) ?? false; +} + +function transactionFieldDisplay(txtype: number, name: string, value: unknown): string { + if (typeof value === "boolean") return value ? "True" : "False"; + if (isAtomicTransactionField(txtype, name)) return formatAtomicDisplay(value); + if (name === "time" || name === "timestamp" || name === "offer_expiration") { + const timestamp = Number(value); + if (Number.isFinite(timestamp)) { + return `${new Date(timestamp * 1_000).toLocaleString()} (${timestamp})`; + } + } + if (name === "payment_period") { + const periods: Record = { d: "Daily", w: "Weekly", m: "Monthly" }; + return periods[String(value).trim().toLowerCase()] ?? String(value ?? ""); + } + if (txtype === 3 && name === "hard_limit") { + return Number(value) === 1 ? "Yes" : "No"; + } + if (txtype === 4 && name === "series") { + return Number(value) === 1 ? "Series" : "Single NFT/RWA"; + } + if (txtype === 4 && name === "ownership_type") { + return Number(value) === 1 ? "Fractional ownership" : "Complete ownership"; + } + if ((txtype === 201 || txtype === 202) && name === "vote") { + return Number(value) === 1 ? "Approve" : "Reject"; + } + if ((name === "previous" || name.endsWith("_hash")) && /^0+$/.test(String(value))) { + return "None"; + } + if (txtype === 5 && (name === "impression_value" || name === "click_value")) { + return (Number(value) / 100).toFixed(2); + } + return typeof value === "string" ? value.trim() : String(value ?? ""); +} + +function appendTransactionField( + container: HTMLElement, + name: string, + value: unknown, + technical = false, + txtype = 0 +): void { + const row = document.createElement("div"); + row.className = `transaction-field${technical ? " technical-field" : ""}`; + const label = document.createElement("span"); + label.textContent = fieldLabel(name); + const contents = document.createElement("strong"); + contents.textContent = technical + ? String(value ?? "") + : transactionFieldDisplay(txtype, name, value); + row.append(label, contents); + container.append(row); +} + +function transactionConfirmations(blockHeight: number, currentHeight: number): number { + if (!blockHeight || !currentHeight || currentHeight < blockHeight) return 0; + return currentHeight - blockHeight; +} + +function confirmationLabel(blockHeight: number, currentHeight: number): string { + const confirmations = transactionConfirmations(blockHeight, currentHeight); + if (!blockHeight) return `Pending · 0/${CONFIRMED_AFTER_BLOCKS} confirmations`; + if (confirmations >= CONFIRMED_AFTER_BLOCKS) { + return `${confirmations} confirmations`; + } + return `${confirmations}/${CONFIRMED_AFTER_BLOCKS} confirmations`; +} + +async function openTransaction( + record: ActivityRecord, + returnScreen: "activity" | "nft" = "activity" +): Promise { + selectedTransaction = record; + transactionReturnScreen = returnScreen; + const back = byId("close-transaction"); + back.title = returnScreen === "nft" + ? "Back to NFT/RWA details" + : "Back to activity"; + back.setAttribute( + "aria-label", + returnScreen === "nft" ? "Back to NFT/RWA details" : "Back to activity" + ); + showScreen("transaction-screen"); + byId("transaction-title").textContent = "Loading transaction"; + byId("transaction-header-status").textContent = ""; + byId("transaction-full-txid").textContent = record.txid; + byId("transaction-overview").replaceChildren(); + byId("transaction-fields").replaceChildren(); + byId("transaction-technical-fields").replaceChildren(); + byId("transaction-miner-earnings").replaceChildren(); + byId("miner-earnings-section").hidden = true; + + try { + const lookup = await send("wallet-transaction", { txid: record.txid }) as { + txid: string; + block_height: number; + current_height: number; + transaction_hex: string; + }; + const wasm = await core(walletStatus?.selectedNetwork ?? "testnet"); + const decoded = JSON.parse( + wasm.decode_transaction(lookup.transaction_hex) + ) as DecodedTransaction; + const confirmations = transactionConfirmations( + lookup.block_height, + lookup.current_height + ); + const pending = confirmations < CONFIRMED_AFTER_BLOCKS; + const timestamp = transactionTimestamp(lookup.transaction_hex); + + byId("transaction-title").textContent = decoded.transaction_name; + byId("transaction-header-status").textContent = pending ? "Pending" : "Confirmed"; + byId("transaction-header-status").className = pending + ? "transaction-pending" + : "transaction-confirmed"; + byId("transaction-full-txid").textContent = lookup.txid; + + const overview = byId("transaction-overview"); + appendTransactionField(overview, "status", pending ? "Pending" : "Confirmed"); + appendTransactionField( + overview, + "block_height", + lookup.block_height ? lookup.block_height.toLocaleString() : "Mempool" + ); + appendTransactionField( + overview, + "confirmations", + confirmationLabel(lookup.block_height, lookup.current_height) + ); + if (timestamp) appendTransactionField(overview, "date", timestamp.toLocaleString()); + appendTransactionField(overview, "transaction_type", decoded.txtype); + + const fields = byId("transaction-fields"); + Object.entries(decoded.fields).forEach(([name, value]) => { + appendTransactionField(fields, name, value, false, decoded.txtype); + }); + + const earnings = record.miner_earnings ?? []; + if (earnings.length > 0) { + const earningsContainer = byId("transaction-miner-earnings"); + earnings.forEach((earning, index) => { + appendTransactionField(earningsContainer, `earning_${index + 1}_type`, earning.type); + appendTransactionField(earningsContainer, `earning_${index + 1}_asset`, earning.asset); + appendTransactionField( + earningsContainer, + `earning_${index + 1}_nft_series`, + earning.nft_series + ); + appendTransactionField( + earningsContainer, + `earning_${index + 1}_amount`, + `${formatAtomicDisplay(earning.amount_atomic)} ${earning.asset.trim()}` + ); + }); + byId("miner-earnings-section").hidden = false; + } + + const technical = byId("transaction-technical-fields"); + if (decoded.stored_hash) { + appendTransactionField(technical, "stored_hash", decoded.stored_hash, true); + } + if (decoded.signature) { + appendTransactionField(technical, "signature", decoded.signature, true); + } + if (decoded.signature1) { + appendTransactionField(technical, "signature_1", decoded.signature1, true); + } + if (decoded.signature2) { + appendTransactionField(technical, "signature_2", decoded.signature2, true); + } + appendTransactionField(technical, "raw_transaction_bytes", lookup.transaction_hex, true); + } catch (error) { + byId("transaction-title").textContent = "Transaction unavailable"; + const fields = byId("transaction-fields"); + fields.replaceChildren(); + appendTransactionField(fields, "error", errorText(error)); + } +} + +type ActivitySummary = { + title: string; + subtitle: string; + amount: string; + secondary?: string; + direction: "incoming" | "outgoing" | "neutral"; +}; + +function shortActivityAddress(value: unknown): string { + const address = String(value ?? "").trim(); + return address.length > 18 + ? `${address.slice(0, 8)}...${address.slice(-7)}` + : address; +} + +function activitySummary(decoded: DecodedTransaction, address: string): ActivitySummary { + const fields = decoded.fields; + const active = address.trim().toLowerCase(); + const baseSymbol = walletStatus?.networkSymbol ?? "CLTC"; + const asset = (name: string) => String(fields[name] ?? "").trim(); + const amount = (name: string, symbol: string, sign = "") => + `${sign}${formatAtomicDisplay(fields[name])} ${symbol}`; + const fee = () => `-${formatAtomicDisplay(fields.txfee ?? 0)} ${baseSymbol}`; + + switch (decoded.txtype) { + case 1: + return { + title: "Mining reward", + subtitle: "From network", + amount: amount("value", baseSymbol, "+"), + direction: "incoming" + }; + case 2: { + const symbol = asset("coin"); + const sent = String(fields.sender ?? "").trim().toLowerCase() === active; + return { + title: sent ? `Sent ${symbol}` : `Received ${symbol}`, + subtitle: sent + ? `To: ${shortActivityAddress(fields.receiver)}` + : `From: ${shortActivityAddress(fields.sender)}`, + amount: amount("value", symbol, sent ? "-" : "+"), + direction: sent ? "outgoing" : "incoming" + }; + } + case 3: { + const symbol = asset("ticker"); + return { + title: `Created ${symbol}`, + subtitle: "Token creation", + amount: amount("number", symbol, "+"), + secondary: fee(), + direction: "incoming" + }; + } + case 4: { + const count = String(fields.count ?? "1"); + return { + title: `Created ${asset("nft_name")}`, + subtitle: Number(fields.series ?? 0) === 0 ? "NFT/RWA" : "NFT/RWA collection", + amount: `+${count} ${Number(count) === 1 ? "item" : "items"}`, + secondary: fee(), + direction: "incoming" + }; + } + case 6: { + const first = String(fields.sender1 ?? "").trim().toLowerCase() === active; + const sentAsset = asset(first ? "ticker1" : "ticker2"); + const receivedAsset = asset(first ? "ticker2" : "ticker1"); + return { + title: "Completed swap", + subtitle: `With: ${shortActivityAddress(fields[first ? "sender2" : "sender1"])}`, + amount: amount(first ? "value1" : "value2", sentAsset, "-"), + secondary: amount(first ? "value2" : "value1", receivedAsset, "+"), + direction: "neutral" + }; + } + case 7: { + const lender = String(fields.lender ?? "").trim().toLowerCase() === active; + return { + title: lender ? "Loan funded" : "Loan received", + subtitle: lender + ? `To: ${shortActivityAddress(fields.borrower)}` + : `From: ${shortActivityAddress(fields.lender)}`, + amount: amount("loan_amount", asset("loan_coin"), lender ? "-" : "+"), + direction: lender ? "outgoing" : "incoming" + }; + } + case 8: + return { + title: "Loan payment", + subtitle: `Contract: ${shortActivityAddress(fields.contract_hash)}`, + amount: `-${formatAtomicDisplay(fields.payback_amount)}`, + direction: "outgoing" + }; + case 10: + return { + title: `Burned ${asset("coin")}`, + subtitle: "Removed from supply", + amount: amount("value", asset("coin"), "-"), + direction: "outgoing" + }; + case 11: { + const symbol = asset("ticker"); + return { + title: `Issued ${symbol}`, + subtitle: "Additional token supply", + amount: amount("number", symbol, "+"), + secondary: fee(), + direction: "incoming" + }; + } + case 12: + return { + title: "Created vanity address", + subtitle: String(fields.vanity_address ?? "").trim(), + amount: fee(), + direction: "outgoing" + }; + default: + return { + title: decoded.transaction_name, + subtitle: "Contractless transaction", + amount: fee(), + direction: "outgoing" + }; + } +} + +async function renderActivity( + records: ActivityRecord[], + currentHeight: number +): Promise { + const list = byId("activity-list"); + list.replaceChildren(); + byId("activity-count").textContent = `${records.length} shown`; + + if (records.length === 0) { + const empty = document.createElement("div"); + empty.className = "activity-empty"; + const title = document.createElement("strong"); + title.textContent = "No wallet activity found"; + const description = document.createElement("span"); + description.textContent = "New transactions will appear here after they reach the selected API."; + empty.append(title, description); + list.append(empty); + return; + } + + const wasm = await core(walletStatus?.selectedNetwork ?? "testnet"); + records.forEach((record) => { + let decoded: DecodedTransaction; + try { + decoded = JSON.parse(wasm.decode_transaction(record.transaction_hex)) as DecodedTransaction; + } catch { + const txtype = Number.parseInt(record.transaction_hex.slice(0, 2), 16); + decoded = { + txtype, + transaction_name: TRANSACTION_NAMES[txtype] ?? `Transaction Type ${txtype}`, + fields: {} + }; + } + const summary = activitySummary(decoded, walletStatus?.address ?? ""); + const date = transactionTimestamp(record.transaction_hex); + const confirmations = transactionConfirmations(record.block_height, currentHeight); + const pending = confirmations < CONFIRMED_AFTER_BLOCKS; + const row = document.createElement("button"); + row.type = "button"; + row.className = "activity-row"; + + const marker = document.createElement("span"); + marker.className = `activity-marker ${summary.direction}`; + marker.textContent = pending + ? "..." + : summary.direction === "incoming" + ? "\u2191" + : summary.direction === "outgoing" + ? "\u2193" + : "\u2194"; + + const details = document.createElement("div"); + details.className = "activity-details"; + const name = document.createElement("strong"); + name.textContent = summary.title; + const counterpart = document.createElement("span"); + counterpart.textContent = pending ? `Pending · ${summary.subtitle}` : summary.subtitle; + const timestamp = document.createElement("small"); + timestamp.textContent = date + ? date.toLocaleString() + : "Transaction time unavailable"; + details.append(name, counterpart, timestamp); + + const status = document.createElement("div"); + status.className = "activity-status"; + const transactionAmount = document.createElement("strong"); + transactionAmount.className = summary.direction; + transactionAmount.textContent = summary.amount; + status.append(transactionAmount); + if (summary.secondary) { + const secondary = document.createElement("span"); + secondary.textContent = summary.secondary; + status.append(secondary); + } + const confirmation = document.createElement("span"); + confirmation.className = pending ? "pending" : "confirmed"; + confirmation.textContent = confirmationLabel(record.block_height, currentHeight); + status.append(confirmation); + + row.append(marker, details, status); + row.addEventListener("click", () => { + void openTransaction(record); + }); + list.append(row); + }); +} + +async function loadActivity(): Promise { + if (!walletStatus?.unlocked) return; + byId("activity-count").textContent = "Loading"; + const list = byId("activity-list"); + list.replaceChildren(); + const loading = document.createElement("div"); + loading.className = "activity-empty"; + loading.textContent = "Loading recent activity..."; + list.append(loading); + + try { + const result = await send("wallet-activity") as { + current_height?: number; + transactions?: ActivityRecord[]; + }; + await renderActivity( + Array.isArray(result.transactions) ? result.transactions : [], + Number(result.current_height ?? 0) + ); + } catch (error) { + list.replaceChildren(); + const failed = document.createElement("div"); + failed.className = "activity-empty activity-error"; + failed.textContent = errorText(error); + list.append(failed); + byId("activity-count").textContent = "Unavailable"; + } +} + +function errorText(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error).replace(/^JsValue\((.*)\)$/, "$1"); +} + +async function action(run: () => Promise, loading = false): Promise { + notice.textContent = ""; + if (loading) showScreen("loading"); + try { + await run(); + } catch (error) { + notice.textContent = errorText(error); + throw error; + } +} + +function requireValue(value: string, message: string): string { + if (!value.trim()) throw new Error(message); + return value; +} + +function bytesToBase64(bytes: Uint8Array): string { + let binary = ""; + const chunkSize = 0x8000; + for (let offset = 0; offset < bytes.length; offset += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); + } + return btoa(binary); +} + +function downloadBase64Png(contents: string, filename: string): void { + const binary = atob(contents); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) { + bytes[index] = binary.charCodeAt(index); + } + const url = URL.createObjectURL(new Blob([bytes], { type: "image/png" })); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1_000); +} + +async function openBackupScreen(status: WalletStatus, password: string): Promise { + walletStatus = status; + currentPassword = password; + backupImageBase64 = String(await send("export-private-key-image", { + password, + imagePassword: password + })); + byId("wallet-image-preview").src = + `data:image/png;base64,${backupImageBase64}`; + const confirmation = byId("backup-confirmed"); + confirmation.disabled = true; + confirmation.checked = false; + byId("open-wallet").disabled = true; + showScreen("backup-screen"); +} + +document.querySelectorAll(".navigation").forEach((button) => { + button.addEventListener("click", () => { + const destination = String(button.dataset.screen); + if (destination === "image-screen" && firefoxToolbarPopup) { + void chrome.tabs.create({ + url: chrome.runtime.getURL("import-wallet.html"), + active: true + }); + return; + } + showScreen(destination); + }); +}); + +byId("account-menu-button").addEventListener("click", () => setAccountDrawer(true)); +byId("close-account-drawer").addEventListener("click", () => setAccountDrawer(false)); +byId("drawer-backdrop").addEventListener("click", () => setAccountDrawer(false)); + +function openAddWalletFlow(screen: "create-screen" | "image-screen"): void { + setAccountDrawer(false); + byId("cancel-wallet-add").hidden = false; + showScreen(screen); +} + +byId("menu-create-wallet").addEventListener("click", () => { + openAddWalletFlow("create-screen"); +}); + +byId("menu-import-wallet").addEventListener("click", () => { + setAccountDrawer(false); + if (firefoxToolbarPopup) { + void chrome.tabs.create({ + url: chrome.runtime.getURL("import-wallet.html"), + active: true + }); + } else { + openAddWalletFlow("image-screen"); + } +}); + +byId("cancel-wallet-add").addEventListener("click", () => { + byId("cancel-wallet-add").hidden = true; + if (walletStatus) showStatus(walletStatus); +}); + +byId("menu-settings").addEventListener("click", () => { + setAccountDrawer(false); + clearRevealedPrivateKey(); + if (walletStatus) { + byId("settings-testnet-api-url").value = walletStatus.testnetApiUrl; + byId("settings-mainnet-api-url").value = walletStatus.mainnetApiUrl; + byId("settings-lock-timeout").value = + String(walletStatus.autoLockMinutes); + } + showScreen("settings-screen"); +}); + +byId("close-settings").addEventListener("click", () => { + clearRevealedPrivateKey(); + if (walletStatus) showStatus(walletStatus); +}); + +byId("drawer-lock-wallet").addEventListener("click", () => { + void action(async () => { + setAccountDrawer(false); + currentPassword = ""; + backupImageBase64 = ""; + showStatus(await send("lock") as WalletStatus); + }); +}); + +byId("open-receive").addEventListener("click", () => { + void action(openReceiveScreen); +}); + +byId("open-send").addEventListener("click", () => { + void action(async () => openSendScreen()); +}); + +byId("close-send").addEventListener("click", () => { + pendingTransfer = undefined; + if (sendNft) { + renderNftDetails(sendNft); + showScreen("nft-detail-screen"); + } else if (walletStatus) { + showStatus(walletStatus); + } +}); + +byId("send-asset").addEventListener("change", updateSendForm); +byId("send-amount").addEventListener("input", updateSendForm); + +byId("send-form").addEventListener("submit", (event) => { + event.preventDefault(); + void action(prepareTransfer); +}); + +byId("send-form").addEventListener("keydown", (event) => { + if (!(event instanceof KeyboardEvent) || event.key !== "Enter") return; + if (!(event.target instanceof HTMLInputElement)) return; + event.preventDefault(); + byId("send-form").requestSubmit(); +}); + +byId("edit-send").addEventListener("click", () => { + showScreen("send-screen"); +}); + +byId("sign-broadcast-transfer").addEventListener("click", () => { + void action(async () => { + if (!pendingTransfer) throw new Error("The reviewed transfer is unavailable."); + const result = await send("wallet-broadcast-transfer", { + transaction: pendingTransfer.transaction + }) as { txid: string }; + lastSentTxid = result.txid; + byId("send-result-txid").textContent = lastSentTxid; + pendingTransfer = undefined; + showScreen("send-success-screen"); + }); +}); + +byId("copy-send-txid").addEventListener("click", () => { + if (!lastSentTxid) return; + void navigator.clipboard.writeText(lastSentTxid) + .then(() => { + notice.textContent = "Transaction ID copied."; + }) + .catch((error) => { + notice.textContent = errorText(error); + }); +}); + +byId("finish-send").addEventListener("click", () => { + lastSentTxid = ""; + const nftTransfer = Boolean(sendNft); + sendNft = undefined; + if (walletStatus) { + void loadWalletBalances(); + if (nftTransfer) { + selectedNft = undefined; + showScreen("nfts-screen"); + void loadOwnedNfts(true); + } else { + showStatus(walletStatus); + } + } +}); + +byId("open-activity").addEventListener("click", () => { + showScreen("activity-screen"); + void loadActivity(); +}); + +byId("open-nfts").addEventListener("click", () => { + nftPage = 1; + selectedNft = undefined; + showScreen("nfts-screen"); + void loadOwnedNfts(true); +}); + +byId("close-nfts").addEventListener("click", () => { + nftLoadId += 1; + if (walletStatus) showStatus(walletStatus); +}); + +byId("refresh-nfts").addEventListener("click", () => { + void loadOwnedNfts(true); +}); + +byId("close-nft-detail").addEventListener("click", () => { + selectedNft = undefined; + showScreen("nfts-screen"); +}); + +byId("transfer-nft").addEventListener("click", () => { + void action(async () => { + if (!selectedNft) throw new Error("No NFT or RWA is selected."); + openNftSendScreen(selectedNft); + }); +}); + +byId("nfts-previous").addEventListener("click", () => { + if (nftPage <= 1) return; + nftPage -= 1; + const loadId = ++nftLoadId; + renderNftGallery(); + void hydrateVisibleNfts(loadId); +}); + +byId("nfts-next").addEventListener("click", () => { + const totalPages = Math.max(1, Math.ceil(ownedNfts.length / NFT_PAGE_SIZE)); + if (nftPage >= totalPages) return; + nftPage += 1; + const loadId = ++nftLoadId; + renderNftGallery(); + void hydrateVisibleNfts(loadId); +}); + +byId("close-activity").addEventListener("click", () => { + if (walletStatus) showStatus(walletStatus); +}); + +byId("close-transaction").addEventListener("click", () => { + if (transactionReturnScreen === "nft" && selectedNft) { + renderNftDetails(selectedNft); + showScreen("nft-detail-screen"); + } else { + showScreen("activity-screen"); + } +}); + +byId("copy-transaction-txid").addEventListener("click", () => { + const txid = selectedTransaction?.txid; + if (!txid) return; + void navigator.clipboard.writeText(txid) + .then(() => { + notice.textContent = "Transaction ID copied."; + }) + .catch((error) => { + notice.textContent = errorText(error); + }); +}); + +byId("refresh-activity").addEventListener("click", () => { + void loadActivity(); +}); + +byId("refresh-balances").addEventListener("click", () => { + void loadWalletBalances(); +}); + +document.querySelectorAll("[data-token-filter]").forEach((button) => { + button.addEventListener("click", () => { + tokenFilter = button.dataset.tokenFilter === "all" ? "all" : "held"; + document.querySelectorAll("[data-token-filter]").forEach((candidate) => { + candidate.classList.toggle("active", candidate === button); + }); + renderTokenBalances(); + }); +}); + +byId("close-receive").addEventListener("click", () => { + if (walletStatus) showStatus(walletStatus); +}); + +byId("copy-receive-address").addEventListener("click", () => { + const address = byId("receive-address-value").textContent?.trim(); + if (!address) { + notice.textContent = "No active wallet address is available."; + return; + } + void navigator.clipboard.writeText(address) + .then(() => { + notice.textContent = "Wallet address copied."; + }) + .catch((error) => { + notice.textContent = errorText(error); + }); +}); + +byId("save-wallet-settings").addEventListener("click", () => { + void action(async () => { + const testnetApiUrl = byId("settings-testnet-api-url").value.replace(/\/+$/, ""); + const mainnetApiUrl = byId("settings-mainnet-api-url").value.replace(/\/+$/, ""); + if (!testnetApiUrl) throw new Error("A testnet API URL is required."); + for (const apiUrl of [testnetApiUrl, mainnetApiUrl].filter(Boolean)) { + const parsed = new URL(apiUrl); + if (parsed.protocol !== "https:" + && parsed.hostname !== "localhost" + && parsed.hostname !== "127.0.0.1") { + throw new Error("Contractless API URLs must use HTTPS."); + } + const originPermission = `${parsed.origin}/*`; + const alreadyGranted = await chrome.permissions.contains({ origins: [originPermission] }); + if (!alreadyGranted) { + const granted = await chrome.permissions.request({ origins: [originPermission] }); + if (!granted) throw new Error("Permission to contact this API endpoint was denied."); + } + } + + walletStatus = await send("save-settings", { + testnetApiUrl, + mainnetApiUrl, + autoLockMinutes: byId("settings-lock-timeout").value + }) as WalletStatus; + notice.textContent = "Settings saved."; + }); +}); + +document.querySelectorAll("[data-select-network]").forEach((button) => { + button.addEventListener("click", () => { + const network = button.dataset.selectNetwork; + if (!network || network === walletStatus?.selectedNetwork) return; + void action(async () => { + setAccountDrawer(false); + showStatus(await send("switch-network", { network }) as WalletStatus); + }); + }); +}); + +document.querySelectorAll("[data-backup-action]").forEach((button) => { + button.addEventListener("click", () => { + pendingBackupAction = button.dataset.backupAction as "image" | "private" | "print"; + setBackupKeyModal(true); + }); +}); + +byId("close-backup-key").addEventListener("click", () => setBackupKeyModal(false)); +byId("backup-key-backdrop").addEventListener("click", () => setBackupKeyModal(false)); + +byId("confirm-backup-key").addEventListener("click", () => { + const actionName = pendingBackupAction; + const password = byId("backup-key-input").value; + if (!actionName) return; + if (!password) { + notice.textContent = "Enter the active wallet encryption key."; + return; + } + + void action(async () => { + if (actionName === "image") { + const image = String(await send("export-private-key-image", { + password, + imagePassword: password + })); + const address = walletStatus?.address?.split(".")[0] ?? "contractless"; + downloadBase64Png(image, `${address}.wallet.png`); + setBackupKeyModal(false); + notice.textContent = "Wallet image downloaded."; + return; + } + + const backup = await send("reveal-private-key", { password }) as { + label: string; + address: string; + privateKey: string; + }; + setBackupKeyModal(false); + + if (actionName === "private") { + byId("revealed-private-key").value = backup.privateKey; + byId("private-key-result").hidden = false; + byId("private-key-result").scrollIntoView({ block: "nearest" }); + return; + } + + const printId = crypto.randomUUID(); + await chrome.storage.session.set({ + [`print-backup:${printId}`]: { + label: backup.label, + address: backup.address, + privateKey: backup.privateKey, + encryptionKey: password + } + }); + await chrome.windows.create({ + url: chrome.runtime.getURL(`print.html?id=${encodeURIComponent(printId)}`), + type: "popup", + width: 820, + height: 760 + }); + }); +}); + +byId("clear-private-key").addEventListener("click", clearRevealedPrivateKey); +byId("copy-private-key").addEventListener("click", () => { + const privateKey = byId("revealed-private-key").value; + if (!privateKey) return; + void navigator.clipboard.writeText(privateKey) + .then(() => { + notice.textContent = "Private key copied."; + }) + .catch((error) => { + notice.textContent = errorText(error); + }); +}); + +document.querySelectorAll(".reveal").forEach((button) => { + button.addEventListener("click", () => { + const input = byId(String(button.dataset.input)); + const showing = input.type === "text"; + input.type = showing ? "password" : "text"; + button.textContent = showing ? "Show" : "Hide"; + }); +}); + +byId("continue-create").addEventListener("click", () => { + try { + pendingPassword = requireValue( + byId("new-password").value, + "Enter an encryption key." + ); + byId("verify-password").value = ""; + showScreen("verify-screen"); + } catch (error) { + notice.textContent = errorText(error); + } +}); + +byId("create-wallet").addEventListener("click", () => { + const verification = byId("verify-password").value; + if (verification !== pendingPassword) { + notice.textContent = "The encryption keys do not match."; + return; + } + void action(async () => { + const password = pendingPassword; + const status = await send("create-wallet", { password }) as WalletStatus; + pendingPassword = ""; + byId("new-password").value = ""; + byId("verify-password").value = ""; + await openBackupScreen(status, password); + }, true).catch(() => showScreen("verify-screen", false)); +}); + +const walletImage = byId("wallet-image"); +walletImage.addEventListener("change", () => { + const file = walletImage.files?.[0]; + selectedImageBase64 = ""; + byId("image-file-name").textContent = file?.name ?? "Choose wallet image"; + if (!file) return; + void file.arrayBuffer() + .then((buffer) => { + selectedImageBase64 = bytesToBase64(new Uint8Array(buffer)); + }) + .catch((error) => { + notice.textContent = errorText(error); + }); +}); + +byId("import-image-form").addEventListener("submit", (event) => { + event.preventDefault(); + const password = byId("image-password").value; + try { + requireValue(password, "Enter the wallet image encryption key."); + requireValue(selectedImageBase64, "Choose a wallet image."); + } catch (error) { + notice.textContent = errorText(error); + return; + } + + void action(async () => { + const status = await send("import-wallet-image", { + imageBase64: selectedImageBase64, + imagePassword: password, + password + }) as WalletStatus; + currentPassword = password; + selectedImageBase64 = ""; + walletImage.value = ""; + byId("image-password").value = ""; + showStatus(status); + }, true).catch(() => showScreen("image-screen", false)); +}); + +byId("import-private").addEventListener("click", () => { + const privateKey = byId("private-key").value.trim(); + const password = byId("private-password").value; + try { + requireValue(privateKey, "Paste the private key."); + requireValue(password, "Enter a new encryption key."); + } catch (error) { + notice.textContent = errorText(error); + return; + } + + void action(async () => { + const status = await send("import-private-key", { privateKey, password }) as WalletStatus; + byId("private-key").value = ""; + byId("private-password").value = ""; + await openBackupScreen(status, password); + }, true).catch(() => showScreen("private-screen", false)); +}); + +byId("save-image").addEventListener("click", () => { + void action(async () => { + if (!backupImageBase64) throw new Error("The wallet image is not ready."); + const address = walletStatus?.address?.split(".")[0] ?? "contractless"; + downloadBase64Png(backupImageBase64, `${address}.wallet.png`); + const confirmation = byId("backup-confirmed"); + confirmation.disabled = false; + confirmation.checked = false; + byId("open-wallet").disabled = true; + }); +}); + +byId("backup-confirmed").addEventListener("change", (event) => { + byId("open-wallet").disabled = + !(event.currentTarget as HTMLInputElement).checked; +}); + +byId("open-wallet").addEventListener("click", () => { + currentPassword = ""; + backupImageBase64 = ""; + byId("wallet-image-preview").removeAttribute("src"); + if (walletStatus) showStatus(walletStatus); +}); + +byId("unlock-form").addEventListener("submit", (event) => { + event.preventDefault(); + const password = byId("unlock-password").value; + try { + requireValue(password, "Enter your encryption key."); + } catch (error) { + notice.textContent = errorText(error); + return; + } + void action(async () => { + const status = await send("unlock", { password }) as WalletStatus; + currentPassword = password; + byId("unlock-password").value = ""; + showStatus(status); + }, true).catch(() => showScreen("locked-screen", false)); +}); + +byId("approve-provider-approval").addEventListener("click", () => { + if (!pendingProviderApprovalId) return; + setApprovalProcessing("approve"); + void action(async () => { + const result = await send("resolve-provider-approval", { + id: pendingProviderApprovalId, + approved: true + }) as { remaining?: number }; + pendingProviderApprovalId = ""; + await showNextProviderApproval(result.remaining ?? 0, true); + }).catch(() => setApprovalProcessing(null)); +}); + +byId("reject-provider-approval").addEventListener("click", () => { + if (!pendingProviderApprovalId) return; + setApprovalProcessing("reject"); + void action(async () => { + const result = await send("resolve-provider-approval", { + id: pendingProviderApprovalId, + approved: false + }) as { remaining?: number }; + pendingProviderApprovalId = ""; + await showNextProviderApproval(result.remaining ?? 0, false); + }).catch(() => setApprovalProcessing(null)); +}); + +send("status") + .then((status) => showStatus(status as WalletStatus)) + .catch((error) => { + showScreen("create-screen"); + notice.textContent = errorText(error); + }); diff --git a/extension/src/print.css b/extension/src/print.css new file mode 100644 index 0000000..d2c3468 --- /dev/null +++ b/extension/src/print.css @@ -0,0 +1,73 @@ +* { + box-sizing: border-box; +} + +body { + max-width: 760px; + margin: 0 auto; + padding: 34px; + color: #111820; + font: 14px/1.45 Arial, Helvetica, sans-serif; +} + +header { + display: flex; + align-items: center; + gap: 15px; + padding-bottom: 18px; + border-bottom: 3px solid #2167a5; +} + +header img { + width: 58px; +} + +h1 { + margin: 0; + font-size: 24px; +} + +header p { + margin: 2px 0 0; + color: #5c6972; +} + +.warning { + margin: 22px 0; + border: 2px solid #9b1c1c; + padding: 12px; + color: #7a1111; + font-weight: 700; +} + +section { + margin-top: 18px; +} + +h2 { + margin: 0 0 7px; + color: #42515b; + font-size: 12px; + text-transform: uppercase; +} + +pre { + margin: 0; + overflow-wrap: anywhere; + white-space: pre-wrap; + border: 1px solid #aebac2; + padding: 11px; + font: 10px/1.35 Consolas, monospace; +} + +.print-error { + color: #9b1c1c; + font-weight: 700; +} + +@media print { + body { + max-width: none; + padding: 0; + } +} diff --git a/extension/src/print.html b/extension/src/print.html new file mode 100644 index 0000000..49e4901 --- /dev/null +++ b/extension/src/print.html @@ -0,0 +1,43 @@ + + + + + + Contractless Wallet Backup + + + +
+ +
+

Contractless Wallet Backup

+

Private recovery information

+
+
+ +

+ Keep this page private. Anyone with the private key and encryption key can + control this wallet. +

+ +
+

Wallet

+

+    
+
+

Address

+

+    
+
+

Private Key

+

+    
+
+

Encryption Key

+

+    
+ + + + + diff --git a/extension/src/print.ts b/extension/src/print.ts new file mode 100644 index 0000000..284e663 --- /dev/null +++ b/extension/src/print.ts @@ -0,0 +1,32 @@ +type PrintableBackup = { + label: string; + address: string; + privateKey: string; + encryptionKey: string; +}; + +async function initialize(): Promise { + const id = new URLSearchParams(location.search).get("id"); + if (!id) throw new Error("Missing printable wallet backup."); + + const storageKey = `print-backup:${id}`; + const stored = await chrome.storage.session.get(storageKey); + await chrome.storage.session.remove(storageKey); + const backup = stored[storageKey] as PrintableBackup | undefined; + if (!backup) throw new Error("The printable wallet backup has expired."); + + document.getElementById("wallet-label")!.textContent = backup.label; + document.getElementById("wallet-address")!.textContent = backup.address; + document.getElementById("wallet-private-key")!.textContent = backup.privateKey; + document.getElementById("wallet-encryption-key")!.textContent = backup.encryptionKey; + + setTimeout(() => { + window.focus(); + window.print(); + }, 150); +} + +initialize().catch((error: unknown) => { + document.getElementById("print-error")!.textContent = + error instanceof Error ? error.message : String(error); +}); diff --git a/extension/src/provider.ts b/extension/src/provider.ts new file mode 100644 index 0000000..d8a0ca5 --- /dev/null +++ b/extension/src/provider.ts @@ -0,0 +1,36 @@ +type Pending = { + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; +}; + +const pending = new Map(); + +window.addEventListener("message", (event) => { + if (event.source !== window || event.data?.source !== "contractless-wallet") return; + const request = pending.get(event.data.id); + if (!request) return; + pending.delete(event.data.id); + if (event.data.response?.ok) request.resolve(event.data.response.result); + else request.reject(new Error(event.data.response?.error ?? "Wallet request failed.")); +}); + +const provider = Object.freeze({ + isContractless: true, + request({ method, params }: { method: string; params?: unknown }): Promise { + const id = crypto.randomUUID(); + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + window.postMessage( + { source: "contractless-page", id, method, params }, + window.location.origin + ); + }); + } +}); + +Object.defineProperty(window, "contractless", { + value: provider, + writable: false, + configurable: false +}); + diff --git a/extension/src/types.ts b/extension/src/types.ts new file mode 100644 index 0000000..dfe2524 --- /dev/null +++ b/extension/src/types.ts @@ -0,0 +1,48 @@ +export type ContractlessNetwork = "testnet" | "mainnet"; + +export type StoredSettings = { + selectedNetwork: ContractlessNetwork; + testnetApiUrl: string; + mainnetApiUrl: string; + apiUrl: string; + autoLockMinutes: number; +}; + +export type WalletStatus = { + exists: boolean; + unlocked: boolean; + activeWalletId?: string; + activeWalletLabel?: string; + address?: string; + publicKey?: string; + wallets: WalletSummary[]; + selectedNetwork: ContractlessNetwork; + networkName: "Testnet" | "Mainnet"; + networkSymbol: "CLTC" | "CLC"; + testnetApiUrl: string; + mainnetApiUrl: string; + apiUrl: string; + autoLockMinutes: number; +}; + +export type WalletSummary = { + id: string; + label: string; + address: string; + publicKey: string; + network: ContractlessNetwork; + active: boolean; +}; + +export type ProviderRequest = { + id: string; + method: string; + params?: unknown; + origin: string; +}; + +export type ApprovalRecord = ProviderRequest & { + createdAt: number; + risk: "safe" | "low" | "high"; + display?: Record; +}; diff --git a/extension/src/wallet.css b/extension/src/wallet.css new file mode 100644 index 0000000..f267e41 --- /dev/null +++ b/extension/src/wallet.css @@ -0,0 +1,2076 @@ +:root { + --blue: #2167a5; + --blue-deep: #174d7c; + --ink: #13202a; + --muted-white: rgb(255 255 255 / 90%); + --danger: #b9382e; + --danger-soft: #fff0ee; + --success: #1d8054; + --dark: #0c151d; + --surface: #111e28; + --surface-raised: #172936; + --surface-active: #1b3547; + --dark-line: #314957; + --dark-muted: #c4d0d7; + --accent: #55c5dc; + font: 14px/1.45 Arial, Helvetica, sans-serif; +} + +* { box-sizing: border-box; } + +body { + width: 390px; + min-width: 390px; + min-height: 600px; + margin: 0; + background: var(--blue); + color: white; +} + +button, +input, +textarea, +select { font: inherit; } + +button { cursor: pointer; } +button:disabled { cursor: not-allowed; } + +.wallet-shell { + position: relative; + width: 390px; + height: 600px; + overflow: hidden; + background: var(--blue); +} + +.screen { + width: 100%; + height: 100%; + background: var(--blue); +} + +.screen[hidden] { display: none; } + +.wallet-header { + height: 72px; + display: flex; + align-items: center; + gap: 11px; + padding: 0 20px; + border-bottom: 1px solid rgb(255 255 255 / 70%); +} + +.wallet-header img { + width: 37px; + filter: brightness(0) invert(1); +} + +.wallet-header div { display: grid; } +.wallet-header strong { font-size: 18px; } +.wallet-header small { color: var(--muted-white); font-size: 14px; } +.wallet-header > span { margin-left: auto; font-size: 13px; font-weight: 700; } + +.screen-content { padding: 28px 24px 22px; } +.screen-content.compact { padding-top: 18px; } + +.eyebrow { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; +} + +h1 { + margin: 7px 0 9px; + font-size: 27px; + line-height: 1.12; + letter-spacing: 0; +} + +.intro { + margin: 0 0 24px; + color: var(--muted-white); + line-height: 1.5; +} + +label { + display: block; + margin-bottom: 8px; + font-size: 12px; + font-weight: 700; +} + +.input-row { + height: 49px; + display: grid; + grid-template-columns: 1fr auto; + border: 1px solid white; + background: white; +} + +.input-row:focus-within, +textarea:focus { + outline: 2px solid rgb(255 255 255 / 28%); + outline-offset: 2px; +} + +.input-row input { + min-width: 0; + border: 0; + padding: 0 13px; + color: var(--ink); + outline: 0; +} + +.reveal { + border: 0; + border-left: 1px solid #d9e0e5; + padding: 0 13px; + background: white; + color: var(--blue); + font-size: 11px; + font-weight: 700; +} + +textarea { + width: 100%; + min-height: 82px; + margin-bottom: 13px; + resize: none; + border: 1px solid white; + padding: 11px 13px; + color: var(--ink); + outline: 0; +} + +.note { + display: grid; + gap: 2px; + margin: 18px 0 23px; + border-left: 3px solid var(--accent); + padding: 13px; + background: var(--surface-raised); + color: white; + font-size: 12px; +} + +.note span { color: var(--dark-muted); } +.compact-note { margin: 12px 0 14px; padding: 10px 13px; } + +.warning { + margin: 18px 0 25px; + border-left: 3px solid var(--danger); + padding: 13px; + background: #321c20; + color: #ffbbb5; + font-size: 12px; + line-height: 1.4; +} + +.warning strong { display: block; } + +.primary, +.secondary { + width: 100%; + min-height: 48px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + border: 1px solid var(--accent); + background: var(--accent); + color: #08202b; + font-weight: 700; +} + +.primary span { font-size: 18px; } + +.secondary { + min-height: 42px; + margin-top: 9px; + background: transparent; + color: #dcecf3; + font-size: 12px; +} + +.secondary:disabled { + border-color: rgb(255 255 255 / 35%); + color: rgb(255 255 255 / 50%); +} + +.text-button { + width: 100%; + margin-top: 8px; + border: 0; + padding: 8px; + background: transparent; + color: var(--muted-white); +} + +.divider { + display: flex; + align-items: center; + gap: 10px; + margin: 17px 0 12px; + color: rgb(255 255 255 / 68%); + font-size: 12px; + text-transform: uppercase; +} + +.divider::before, +.divider::after { + height: 1px; + flex: 1; + background: rgb(255 255 255 / 30%); + content: ""; +} + +.file-heading { margin-top: 20px; } + +.file-picker { + min-height: 86px; + display: flex; + align-items: center; + gap: 13px; + margin-bottom: 24px; + border: 1px dashed rgb(255 255 255 / 72%); + padding: 14px; + background: rgb(255 255 255 / 10%); + cursor: pointer; +} + +.file-picker > span:last-child { display: grid; gap: 4px; } +.file-picker small { color: rgb(255 255 255 / 68%); font-weight: 400; } + +.file-icon { + width: 34px; + height: 34px; + display: grid; + flex: 0 0 34px; + place-items: center; + border: 1px solid white; + font-size: 22px; + font-weight: 400; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; +} + +.loading-screen { + position: relative; +} + +.loading-brand { + position: absolute; + inset: 0; + display: grid; + place-content: center; + justify-items: center; + gap: 34px; +} + +.loading-brand img, +.large-logo { + width: 148px; + filter: brightness(0) invert(1); +} + +.spinner { + width: 28px; + aspect-ratio: 1; + border: 3px solid rgb(255 255 255 / 28%); + border-top-color: white; + border-radius: 50%; + animation: spin 0.85s linear infinite; +} + +.loading-screen > p { + position: absolute; + bottom: 35px; + width: 100%; + margin: 0; + color: rgb(255 255 255 / 72%); + font-size: 12px; + text-align: center; + text-transform: uppercase; +} + +@keyframes spin { to { transform: rotate(360deg); } } + +.success-mark { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border-radius: 50%; + border: 1px solid #55c98a; + background: #123727; + color: #66dfa0; + font-size: 22px; + font-weight: 700; +} + +.image-preview { + position: relative; + height: 142px; + margin: 18px 0; + overflow: hidden; + border: 1px solid var(--dark-line); + background: var(--surface); +} + +.image-preview img { + width: 100%; + height: 100%; + display: block; + object-fit: contain; +} + +.confirmation { + display: grid; + grid-template-columns: 18px 1fr; + gap: 9px; + align-items: start; + margin: 14px 0; + color: rgb(255 255 255 / 84%); + font-size: 11px; + line-height: 1.35; +} + +.confirmation input { width: 16px; height: 16px; margin: 0; } + +.centered-content { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + padding: 36px 28px; + text-align: center; +} + +.centered-content { align-items: stretch; } +.centered-content .large-logo { align-self: center; width: 92px; margin-bottom: 25px; } +.centered-content h1 { text-align: center; } +.centered-content .primary { margin-top: 22px; } + +.dashboard-header { + height: 66px; +} + +.account-button { + min-width: 105px; + max-width: 132px; + height: 34px; + display: flex; + align-items: center; + gap: 7px; + margin-left: auto; + overflow: hidden; + border: 1px solid rgb(255 255 255 / 45%); + padding: 0 10px; + background: transparent; + color: white; + font-size: 14px; +} + +.account-button > span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.network-dot { + width: 7px; + height: 7px; + flex: 0 0 7px; + border-radius: 50%; + background: #63d18d; +} + +.assets-dashboard { + height: calc(100% - 66px); + overflow: hidden; +} + +.portfolio-balance { + padding: 22px 22px 19px; + border-bottom: 1px solid rgb(255 255 255 / 24%); + background: var(--blue-deep); +} + +.portfolio-balance > span { + color: var(--muted-white); + font-size: 14px; + text-transform: uppercase; +} + +.portfolio-balance > strong { + display: block; + margin-top: 4px; + font-size: 31px; + line-height: 1.2; +} + +.portfolio-balance > strong small { + font-size: 13px; +} + +.portfolio-balance p { + max-width: 100%; + margin: 7px 0 0; + overflow: hidden; + color: rgb(255 255 255 / 82%); + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wallet-actions { + height: 88px; + display: grid; + grid-template-columns: repeat(4, 1fr); + border-bottom: 1px solid rgb(255 255 255 / 24%); +} + +.wallet-actions button { + display: grid; + place-items: center; + align-content: center; + gap: 5px; + border: 0; + border-right: 1px solid rgb(255 255 255 / 18%); + background: transparent; + color: white; + font-size: 14px; + font-weight: 700; +} + +.wallet-actions button:last-child { + border-right: 0; +} + +.action-icon { + width: 29px; + height: 29px; + display: grid; + place-items: center; + border: 1px solid rgb(255 255 255 / 65%); + font-size: 16px; + font-weight: 400; +} + +.asset-ledger { + height: calc(100% - 209px); + background: var(--dark); + color: white; +} + +.asset-filter { + height: 42px; + display: grid; + grid-template-columns: 1fr 1fr; + border-bottom: 1px solid var(--dark-line); + padding: 5px 19px; + background: #0f1a22; +} + +.asset-filter button { + border: 0; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--dark-muted); + font-size: 14px; + font-weight: 700; +} + +.asset-filter button.active { + border-bottom-color: var(--accent); + color: white; +} + +.asset-ledger-heading { + height: 45px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 19px; + border-bottom: 1px solid var(--dark-line); +} + +.asset-ledger-heading h1 { + margin: 0; + font-size: 19px; +} + +.asset-ledger-heading button { + width: 30px; + height: 30px; + border: 0; + background: transparent; + color: var(--accent); + font-size: 17px; +} + +.asset-row { + min-height: 66px; + display: grid; + grid-template-columns: 38px minmax(0, 1fr) auto; + gap: 11px; + align-items: center; + padding: 10px 19px; + border-bottom: 1px solid var(--dark-line); + background: var(--surface); +} + +.asset-symbol { + width: 36px; + height: 36px; + display: grid; + place-items: center; + background: var(--blue); + color: white; + font-size: 12px; + font-weight: 700; +} + +.contractless-symbol img { + width: 25px; + filter: brightness(0) invert(1); +} + +.asset-name, +.asset-value { + min-width: 0; + display: grid; + gap: 2px; +} + +.asset-name strong, +.asset-value strong { + overflow: hidden; + font-size: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.asset-name span, +.asset-value span { + color: var(--dark-muted); + font-size: 13px; +} + +.asset-value { + max-width: 125px; + text-align: right; +} + +.token-list { + max-height: calc(100% - 148px); + overflow-y: auto; +} + +.empty-assets { + display: grid; + gap: 5px; + padding: 27px 20px; + color: var(--dark-muted); + text-align: center; +} + +.empty-assets strong { + color: #dce8ee; + font-size: 16px; +} + +.empty-assets span { + font-size: 14px; +} + +.receive-screen { + background: var(--dark); +} + +.receive-screen .wallet-header { + background: var(--blue); +} + +.send-screen, +.send-review-screen, +.send-success-screen { + background: var(--dark); + color: white; +} + +.send-screen .wallet-header, +.send-review-screen .wallet-header, +.send-success-screen .wallet-header { + background: var(--blue); +} + +.send-content, +.send-review-content, +.send-success-content { + height: calc(100% - 72px); + padding: 22px; + overflow-y: auto; +} + +.send-content { + display: grid; + align-content: start; + gap: 17px; +} + +.send-content label { + color: white; + font-size: 14px; +} + +.send-content input, +.send-content select { + width: 100%; + min-height: 48px; + border: 1px solid var(--dark-line); + padding: 0 13px; + background: var(--surface); + color: white; + outline: 0; +} + +.send-content input:focus, +.send-content select:focus { + border-color: var(--accent); +} + +.send-amount-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; +} + +.send-amount-row input { + border-right: 0; +} + +.send-amount-row > span { + min-width: 64px; + display: grid; + place-items: center; + border: 1px solid var(--dark-line); + background: var(--surface-raised); + color: var(--accent); + font-weight: 700; +} + +.field-note { + display: block; + margin-top: 7px; + color: var(--dark-muted); + font-size: 13px; +} + +.send-continue { + margin-top: 5px; +} + +.send-review-content { + padding: 0 22px 24px; +} + +.send-review-amount { + margin: 0 -22px 20px; + padding: 24px 22px; + background: var(--blue-deep); +} + +.send-review-amount > span { + color: var(--muted-white); + font-size: 13px; + text-transform: uppercase; +} + +.send-review-amount strong { + display: block; + margin-top: 5px; + font-size: 29px; +} + +.send-review-amount small { + color: var(--accent); + font-size: 15px; +} + +.send-review-details { + margin: 0 0 18px; +} + +.send-review-details > div { + padding: 12px 0; + border-bottom: 1px solid var(--dark-line); +} + +.send-review-details dt { + margin-bottom: 4px; + color: var(--dark-muted); + font-size: 13px; +} + +.send-review-details dd { + margin: 0; + overflow-wrap: anywhere; + color: white; + font-weight: 700; +} + +.signing-notice { + margin: 18px 0; + padding-left: 12px; + border-left: 3px solid var(--accent); + color: var(--dark-muted); +} + +.send-success-content { + text-align: center; +} + +.send-success-content p { + color: var(--dark-muted); +} + +.send-txid { + margin: 24px 0 14px; + padding: 14px; + border: 1px solid var(--dark-line); + background: var(--surface); + text-align: left; +} + +.send-txid span { + display: block; + margin-bottom: 5px; + color: var(--dark-muted); + font-size: 13px; +} + +.send-txid strong { + overflow-wrap: anywhere; +} + +.send-success-content .secondary { + margin-bottom: 10px; +} + +.receive-content { + height: calc(100% - 72px); + overflow-y: auto; + padding: 25px 24px 22px; + text-align: center; +} + +.receive-label { + color: var(--accent); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.receive-content h1 { + margin-top: 8px; +} + +.receive-content > p { + margin: 0 auto 20px; + color: var(--dark-muted); + font-size: 11px; + line-height: 1.45; +} + +.qr-frame { + width: 238px; + height: 238px; + display: grid; + place-items: center; + margin: 0 auto 19px; + border: 1px solid #708693; + padding: 8px; + background: white; +} + +.qr-frame canvas { + width: 220px; + height: 220px; + display: block; +} + +.receive-address { + display: grid; + gap: 6px; + border: 1px solid var(--dark-line); + padding: 13px; + background: var(--surface); + text-align: left; +} + +.receive-address span { + color: var(--dark-muted); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; +} + +.receive-address strong { + overflow-wrap: anywhere; + color: #e5f0f5; + font: 12px/1.4 Consolas, monospace; +} + +.receive-copy { + margin-top: 13px; +} + +.nfts-screen, +.nft-detail-screen { + background: var(--dark); + color: white; +} + +.nfts-screen .wallet-header, +.nft-detail-screen .wallet-header { + background: var(--blue); +} + +.header-refresh { + width: 34px; + height: 34px; + display: grid; + place-items: center; + margin-left: auto; + border: 1px solid rgb(255 255 255 / 70%); + background: transparent; + color: white; + font-size: 18px; +} + +.nfts-content, +.nft-detail-content { + height: calc(100% - 72px); + overflow-y: auto; +} + +.nfts-summary { + min-height: 42px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 18px; + border-bottom: 1px solid var(--dark-line); + background: #0f1a22; + color: var(--dark-muted); + font-size: 13px; + font-weight: 700; + text-transform: uppercase; +} + +.nfts-summary span:first-child { + color: var(--accent); +} + +.nfts-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + padding: 1px; + background: var(--dark-line); +} + +.nft-card { + min-width: 0; + display: grid; + align-content: start; + border: 0; + padding: 0; + background: var(--surface); + color: white; + text-align: left; +} + +.nft-card:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.nft-card-media { + width: 100%; + aspect-ratio: 1; + display: grid; + place-items: center; + overflow: hidden; + background: #17303e; + color: var(--accent); + font-size: 28px; + font-weight: 700; +} + +.nft-card-media img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; +} + +.nft-card-identity { + min-height: 116px; + display: grid; + align-content: start; + gap: 3px; + padding: 11px 12px 13px; +} + +.nft-card-identity > span { + color: var(--accent); + font-size: 11px; + font-weight: 700; +} + +.nft-card-identity strong { + overflow: hidden; + font-size: 15px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nft-card-identity small { + overflow: hidden; + color: var(--dark-muted); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nft-card-identity b { + margin-top: 5px; + color: white; + font-size: 12px; +} + +.nft-card-identity em { + color: #e9c667; + font-size: 11px; + font-style: normal; +} + +.nfts-empty { + grid-column: 1 / -1; + min-height: 300px; + display: grid; + place-content: center; + gap: 6px; + padding: 30px; + background: var(--surface); + color: var(--dark-muted); + text-align: center; +} + +.nfts-empty strong { + color: white; + font-size: 17px; +} + +.nfts-pagination { + min-height: 52px; + display: grid; + grid-template-columns: 42px 1fr 42px; + align-items: center; + gap: 10px; + padding: 7px 14px; + border-top: 1px solid var(--dark-line); + background: #0f1a22; + text-align: center; +} + +.nfts-pagination[hidden] { + display: none; +} + +.nfts-pagination button { + width: 38px; + height: 38px; + border: 1px solid var(--dark-line); + background: var(--surface); + color: var(--accent); + font-size: 17px; +} + +.nfts-pagination button:disabled { + color: #61727c; +} + +.nfts-pagination span { + color: var(--dark-muted); + font-size: 13px; +} + +.nft-detail-media { + width: 100%; + aspect-ratio: 16 / 11; + display: grid; + place-items: center; + overflow: hidden; + border-bottom: 1px solid var(--dark-line); + background: #142934; +} + +.nft-detail-media img { + width: 100%; + height: 100%; + display: block; + object-fit: contain; +} + +.nft-detail-heading { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 15px; + align-items: end; + padding: 18px 20px 14px; +} + +.nft-detail-heading > div { + min-width: 0; +} + +.nft-detail-heading span { + color: var(--accent); + font-size: 11px; + font-weight: 700; +} + +.nft-detail-heading h1 { + overflow-wrap: anywhere; + margin: 4px 0 0; + font-size: 23px; +} + +.nft-detail-heading > strong { + color: #63d18d; + font-size: 13px; + text-align: right; +} + +.nft-detail-description { + margin: 0; + padding: 0 20px 17px; + color: var(--dark-muted); + font-size: 14px; + line-height: 1.5; +} + +.nft-detail-attributes { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 7px; + padding: 0 20px 18px; +} + +.nft-detail-attributes[hidden] { + display: none; +} + +.nft-detail-attributes > div { + min-width: 0; + display: grid; + gap: 2px; + border: 1px solid var(--dark-line); + padding: 9px; + background: var(--surface-raised); +} + +.nft-detail-attributes span { + overflow: hidden; + color: var(--dark-muted); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nft-detail-attributes strong { + overflow-wrap: anywhere; + font-size: 13px; +} + +.nft-detail-fields { + margin: 0; + border-top: 1px solid var(--dark-line); +} + +.nft-detail-fields > div { + display: grid; + gap: 4px; + padding: 12px 20px; + border-bottom: 1px solid var(--dark-line); +} + +.nft-detail-fields dt { + color: var(--dark-muted); + font-size: 12px; +} + +.nft-detail-fields dd { + margin: 0; + overflow-wrap: anywhere; + color: white; + font: 12px/1.45 Consolas, monospace; +} + +.nft-detail-actions { + padding: 18px 20px 0; +} + +.nft-provenance { + padding: 18px 20px 25px; +} + +.nft-provenance h2 { + margin: 0 0 11px; + font-size: 16px; +} + +.nft-provenance p { + color: var(--dark-muted); +} + +.nft-history-row { + width: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 3px 12px; + border: 0; + padding: 10px 0; + border-bottom: 1px solid var(--dark-line); + background: transparent; + color: white; + text-align: left; +} + +.nft-history-row:hover, +.nft-history-row:focus-visible { + background: var(--surface-raised); +} + +.nft-history-row:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.nft-history-row strong { + font-size: 13px; +} + +.nft-history-row span { + color: var(--dark-muted); + font-size: 12px; + text-align: right; +} + +.nft-history-row small { + grid-column: 1 / -1; + color: var(--accent); + font: 11px Consolas, monospace; +} + +.activity-screen { + background: var(--dark); + color: white; +} + +.activity-screen .wallet-header { + background: var(--blue); +} + +.activity-content { + height: calc(100% - 72px); + overflow-y: auto; +} + +.activity-summary { + min-height: 42px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 18px; + border-bottom: 1px solid var(--dark-line); + background: #0f1a22; + color: var(--dark-muted); + font-size: 13px; + text-transform: uppercase; +} + +.activity-summary strong { + color: var(--accent); + font-size: 13px; +} + +.activity-summary > div { + display: flex; + align-items: center; + gap: 8px; +} + +.activity-summary button { + width: 30px; + height: 30px; + border: 1px solid var(--dark-line); + background: transparent; + color: var(--accent); + font-size: 17px; +} + +.activity-list { + background: var(--surface); +} + +.activity-row { + width: 100%; + min-height: 98px; + display: grid; + grid-template-columns: 31px minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 11px 17px; + border-bottom: 1px solid var(--dark-line); + border-top: 0; + border-right: 0; + border-left: 0; + background: transparent; + color: white; + text-align: left; +} + +.activity-row:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.activity-marker { + width: 29px; + height: 29px; + display: grid; + place-items: center; + border: 1px solid #607987; + color: var(--accent); + font-size: 13px; + font-weight: 700; +} + +.activity-marker.incoming, +.activity-status .incoming { + color: #63d18d; +} + +.activity-marker.outgoing, +.activity-status .outgoing { + color: white; +} + +.activity-details, +.activity-status { + min-width: 0; + display: grid; + gap: 2px; +} + +.activity-details strong { + overflow: hidden; + font-size: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.activity-details span { + color: #c1d0d8; + font: 13px Consolas, monospace; +} + +.activity-details small, +.activity-status span { + color: var(--dark-muted); + font-size: 13px; +} + +.activity-status { + max-width: 132px; + text-align: right; +} + +.activity-status strong { + font-size: 14px; + white-space: nowrap; +} + +.activity-status .confirmed { + color: #63d18d; +} + +.activity-status .pending { + color: #e9c667; +} + +.activity-empty { + display: grid; + gap: 6px; + padding: 42px 24px; + color: var(--dark-muted); + font-size: 14px; + text-align: center; +} + +.activity-empty strong { + color: #dce8ee; + font-size: 16px; +} + +.activity-error { + color: #ffaaa4; +} + +.transaction-screen { + background: var(--dark); + color: white; +} + +.transaction-screen .wallet-header { + background: var(--blue); +} + +.transaction-confirmed { + color: #84e7aa; +} + +.transaction-pending { + color: #f1d27b; +} + +.transaction-content { + height: calc(100% - 72px); + overflow-y: auto; + padding-bottom: 24px; +} + +.transaction-identity { + display: grid; + gap: 13px; + padding: 18px; + border-bottom: 1px solid var(--dark-line); + background: var(--surface-raised); +} + +.transaction-identity > div { + display: grid; + gap: 7px; +} + +.transaction-identity span, +.transaction-field span { + color: var(--dark-muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.transaction-identity strong { + overflow-wrap: anywhere; + color: white; + font: 13px/1.55 Consolas, monospace; +} + +.transaction-identity button { + min-height: 40px; + border: 1px solid var(--accent); + background: transparent; + color: var(--accent); + font-size: 14px; + font-weight: 700; +} + +.transaction-section { + border-top: 1px solid var(--dark-line); +} + +.transaction-section h2 { + margin: 0; + padding: 16px 18px 10px; + font-size: 16px; +} + +.transaction-fields { + display: grid; +} + +.transaction-field { + display: grid; + gap: 6px; + padding: 13px 18px; + border-bottom: 1px solid var(--dark-line); + background: var(--surface); +} + +.transaction-field strong { + overflow-wrap: anywhere; + color: white; + font-size: 15px; + font-weight: 600; + line-height: 1.45; +} + +.transaction-technical { + border-top: 1px solid var(--dark-line); +} + +.transaction-technical summary { + padding: 17px 18px; + color: var(--accent); + font-size: 15px; + font-weight: 700; + cursor: pointer; +} + +.technical-field strong { + font: 12px/1.55 Consolas, monospace; + word-break: break-all; +} + +.drawer-backdrop { + position: absolute; + inset: 0; + z-index: 10; + background: rgb(9 26 39 / 54%); +} + +.account-drawer { + position: absolute; + top: 0; + right: 0; + bottom: 0; + z-index: 11; + width: 338px; + display: flex; + flex-direction: column; + border-left: 1px solid var(--dark-line); + background: var(--dark); + color: white; + transform: translateX(100%); + transition: transform 180ms ease-out; +} + +.account-drawer.open { + transform: translateX(0); +} + +.drawer-header { + height: 72px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 18px; + border-bottom: 1px solid var(--dark-line); + background: var(--blue); + color: white; +} + +.drawer-header > div { + display: grid; + gap: 2px; +} + +.drawer-header strong { + font-size: 19px; +} + +.drawer-header span { + color: var(--muted-white); + font-size: 14px; +} + +.drawer-header button { + width: 34px; + height: 34px; + border: 1px solid rgb(255 255 255 / 45%); + background: transparent; + color: white; + font-size: 23px; + line-height: 1; +} + +.drawer-section { + max-height: 244px; + overflow-y: auto; + padding: 17px 15px 14px; + border-bottom: 1px solid var(--dark-line); +} + +.drawer-label { + display: block; + margin: 0 3px 8px; + color: var(--dark-muted); + font-size: 13px; + font-weight: 700; + text-transform: uppercase; +} + +.known-address { + width: 100%; + min-height: 58px; + display: grid; + grid-template-columns: minmax(0, 1fr) 30px; + gap: 4px; + align-items: center; + border: 1px solid var(--dark-line); + padding: 4px; + background: var(--surface); + color: white; +} + +.known-address + .known-address { + margin-top: 7px; +} + +.known-address.active-address { + border-color: var(--accent); + background: var(--surface-active); +} + +.address-select { + min-width: 0; + min-height: 48px; + display: grid; + grid-template-columns: 36px minmax(0, 1fr) 20px; + gap: 10px; + align-items: center; + border: 0; + padding: 5px 6px; + background: transparent; + color: white; + text-align: left; +} + +.rename-address { + width: 28px; + height: 34px; + border: 0; + background: transparent; + color: var(--accent); + font-size: 16px; +} + +.address-avatar { + width: 34px; + height: 34px; + display: grid; + place-items: center; + background: #204f70; +} + +.address-avatar img { + width: 24px; + filter: brightness(0) invert(1); +} + +.address-details { + min-width: 0; + display: grid; + gap: 2px; +} + +.address-details strong { + font-size: 15px; +} + +.address-details small { + overflow: hidden; + color: var(--dark-muted); + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.selected-mark { + color: var(--success); + font-size: 14px; + font-weight: 700; +} + +.drawer-navigation { + display: grid; + padding: 8px 0; +} + +.drawer-navigation button { + min-height: 72px; + display: grid; + grid-template-columns: 34px minmax(0, 1fr) 16px; + gap: 10px; + align-items: center; + border: 0; + border-bottom: 1px solid var(--dark-line); + padding: 9px 18px; + background: transparent; + color: white; + text-align: left; +} + +.drawer-navigation button > span:nth-child(2) { + display: grid; + gap: 2px; +} + +.drawer-navigation strong { + font-size: 16px; +} + +.drawer-navigation small { + color: var(--dark-muted); + font-size: 13px; +} + +.menu-icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid #607987; + color: var(--accent); + font-size: 15px; + font-weight: 700; +} + +.menu-arrow { + color: var(--dark-muted); + font-size: 20px; + text-align: right; +} + +.drawer-lock { + min-height: 46px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin: auto 15px 15px; + border: 1px solid var(--dark-line); + background: var(--surface-raised); + color: #dce8ee; + font-size: 14px; + font-weight: 700; +} + +.header-back { + width: 34px; + height: 34px; + border: 1px solid rgb(255 255 255 / 45%); + background: transparent; + color: white; + font-size: 18px; +} + +.settings-screen { + background: var(--dark); + color: white; +} + +.settings-screen .wallet-header { + background: var(--blue); + color: white; +} + +.settings-content { + height: calc(100% - 72px); + overflow-y: auto; + padding-bottom: 20px; +} + +.settings-group { + padding: 18px 20px; + border-bottom: 1px solid var(--dark-line); + background: var(--surface); +} + +.settings-heading { + display: grid; + gap: 3px; + margin-bottom: 15px; +} + +.settings-heading strong { + font-size: 13px; +} + +.settings-heading span { + color: var(--dark-muted); + font-size: 12px; + line-height: 1.4; +} + +.settings-group label { + color: #b7c6ce; + font-size: 12px; + text-transform: uppercase; +} + +.settings-group input, +.settings-group select { + width: 100%; + height: 44px; + border: 1px solid #8699a5; + padding: 0 11px; + background: white; + color: var(--ink); + outline: 0; +} + +.settings-group input:focus, +.settings-group select:focus { + border-color: var(--blue); + outline: 2px solid rgb(33 103 165 / 14%); +} + +.settings-save { + width: calc(100% - 40px); + margin: 16px 20px; + border-color: var(--accent); + background: var(--accent); + color: #08202b; +} + +.backup-settings { + padding-right: 0; + padding-left: 0; +} + +.backup-settings .settings-heading { + padding: 0 20px; +} + +.settings-action { + width: 100%; + min-height: 61px; + display: grid; + grid-template-columns: 34px minmax(0, 1fr) 16px; + gap: 10px; + align-items: center; + border: 0; + border-top: 1px solid var(--dark-line); + padding: 9px 20px; + background: transparent; + color: white; + text-align: left; +} + +.settings-action:last-child { + border-bottom: 1px solid var(--dark-line); +} + +.settings-action > span:nth-child(2) { + display: grid; + gap: 2px; +} + +.settings-action strong { + font-size: 14px; +} + +.settings-action small { + color: var(--dark-muted); + font-size: 11px; +} + +.settings-action > span:last-child { + color: var(--dark-muted); + font-size: 20px; +} + +.settings-action-icon { + width: 32px; + height: 32px; + display: grid; + place-items: center; + border: 1px solid #607987; + color: var(--accent); + font-size: 15px; + font-weight: 700; +} + +.private-key-result { + margin: 16px 20px 0; + border: 1px solid var(--dark-line); + padding: 13px; + background: var(--surface-raised); +} + +.private-key-result > div { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 9px; +} + +.private-key-result > div > strong { + font-size: 11px; + text-transform: uppercase; +} + +.private-key-result > div > button { + width: 28px; + height: 28px; + border: 0; + background: transparent; + color: var(--dark-muted); + font-size: 19px; +} + +.private-key-result textarea { + height: 95px; + min-height: 95px; + margin: 0 0 10px; + border: 1px solid #8699a5; + padding: 9px; + background: white; + color: var(--ink); + font-size: 11px; +} + +.private-key-result .secondary { + border-color: var(--accent); + color: var(--accent); +} + +.modal-backdrop { + position: absolute; + inset: 0; + z-index: 30; + background: rgb(9 26 39 / 68%); +} + +.backup-key-modal { + position: absolute; + top: 50%; + right: 20px; + left: 20px; + z-index: 31; + border: 1px solid white; + background: var(--blue); + color: white; + transform: translateY(-50%); +} + +.backup-key-modal > header { + min-height: 65px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 15px; + border-bottom: 1px solid rgb(255 255 255 / 40%); +} + +.backup-key-modal > header > div { + display: grid; + gap: 2px; +} + +.backup-key-modal > header strong { + font-size: 13px; +} + +.backup-key-modal > header span { + color: var(--muted-white); + font-size: 11px; +} + +.backup-key-modal > header button { + width: 30px; + height: 30px; + border: 1px solid rgb(255 255 255 / 45%); + background: transparent; + color: white; + font-size: 20px; +} + +.backup-key-modal > div { + padding: 17px 15px; +} + +.backup-key-modal .primary { + margin-top: 16px; +} + +#notice { + position: absolute; + right: 18px; + bottom: 14px; + left: 18px; + z-index: 40; + margin: 0; + padding: 10px 12px; + border: 1px solid #8d4448; + border-left: 3px solid #ff7168; + background: #321c20; + color: #ffc0bb; + font-size: 11px; +} + +#notice:empty { display: none; } + +.provider-approval-screen { + display: flex; + flex-direction: column; +} + +.provider-approval-screen .screen-content { + min-height: 0; + flex: 1; + overflow-y: auto; + padding-bottom: 18px; +} + +.approval-context { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.approval-context .eyebrow { + min-width: 0; + overflow-wrap: anywhere; +} + +.risk-label { + flex: 0 0 auto; + padding: 5px 8px; + border: 1px solid currentColor; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; +} + +.risk-safe { + background: #123b2d; + color: #63e5a9; +} + +.risk-low { + background: #403612; + color: #ffe16c; +} + +.risk-high { + background: #481f24; + color: #ff8b8b; +} + +.approval-details { + display: grid; + grid-template-columns: minmax(100px, 0.8fr) minmax(0, 1.4fr); + margin: 20px 0; + border: 1px solid #315466; + background: #091923; +} + +.approval-details dt, +.approval-details dd { + min-width: 0; + margin: 0; + padding: 11px 12px; + border-bottom: 1px solid #203d4c; + overflow-wrap: anywhere; +} + +.approval-details dt { + color: #85dce8; + font-size: 12px; + font-weight: 700; + text-transform: capitalize; +} + +.approval-details dd { + color: #f5f9fb; + font: 13px/1.45 Arial, Helvetica, sans-serif; + white-space: pre-wrap; +} + +.approval-details dt:last-of-type, +.approval-details dd:last-of-type { + border-bottom: 0; +} + +.approval-actions { + display: grid; + flex: 0 0 auto; + grid-template-columns: 1fr 1fr; + gap: 10px; + padding: 14px 24px 18px; + border-top: 1px solid rgb(255 255 255 / 35%); + background: var(--blue-deep); +} + +.approval-actions button { + min-height: 48px; + margin: 0; +} + +.approval-actions button.is-processing::before { + width: 15px; + height: 15px; + content: ""; + border: 2px solid currentcolor; + border-right-color: transparent; + border-radius: 50%; + animation: approval-spin 700ms linear infinite; +} + +.approval-actions button:disabled { + opacity: 0.72; +} + +@keyframes approval-spin { + to { transform: rotate(360deg); } +} + +.network-choice { + display: grid; + grid-template-columns: 1fr 1fr; + width: 100%; + margin: 0 0 18px; + border: 1px solid #5b8caf; + background: #0a1b27; +} + +.network-choice button { + min-height: 42px; + border: 0; + border-right: 1px solid #315466; + background: transparent; + color: #b9cfdb; + font: 700 13px/1 Arial, Helvetica, sans-serif; +} + +.network-choice button:last-child { border-right: 0; } + +.network-choice button.active { + background: #43d6e8; + color: #06151e; +} + +.drawer-network { margin: 8px 0 0; } + +.settings-help { + display: block; + margin-top: 8px; + color: #a9c2cf; + font-size: 12px; + line-height: 1.45; +} diff --git a/extension/tsconfig.json b/extension/tsconfig.json new file mode 100644 index 0000000..1c7ad3a --- /dev/null +++ b/extension/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "lib": ["ESNext", "DOM"], + "types": ["chrome", "node"] + }, + "include": ["src/**/*.ts", "vite.config.ts"] +} diff --git a/extension/vite.config.ts b/extension/vite.config.ts new file mode 100644 index 0000000..07c9127 --- /dev/null +++ b/extension/vite.config.ts @@ -0,0 +1,42 @@ +import { defineConfig } from "vite"; +import { viteStaticCopy } from "vite-plugin-static-copy"; +import { resolve } from "node:path"; + +export default defineConfig(({ mode }) => { + const browser = mode === "firefox" ? "firefox" : "chrome"; + + return { + root: "src", + build: { + outDir: `../dist/${browser}`, + emptyOutDir: true, + rollupOptions: { + input: { + popup: resolve(__dirname, "src/popup.html"), + "import-wallet": resolve(__dirname, "src/import-wallet.html"), + print: resolve(__dirname, "src/print.html"), + background: resolve(__dirname, "src/background.ts"), + content: resolve(__dirname, "src/content.ts"), + provider: resolve(__dirname, "src/provider.ts") + }, + output: { + entryFileNames: "[name].js", + chunkFileNames: "chunks/[name]-[hash].js", + assetFileNames: "assets/[name]-[hash][extname]" + } + } + }, + plugins: [ + viteStaticCopy({ + targets: [ + { + src: `manifest.${browser}.json`, + dest: ".", + rename: "manifest.json" + }, + { src: "assets/contractless-logo*.png", dest: "." } + ] + }) + ] + }; +}); diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 0000000..b1abe19 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,310 @@ +# Contractless Browser Wallet JavaScript + +`contractless.js` is the website client for the Contractless browser wallet. +It detects the injected wallet provider and manages a temporary, +origin-bound application session. + +## Application ID + +Choose a readable application ID containing 5 to 32 letters, numbers, +underscores, or hyphens, and keep it unchanged for the lifetime of the +application. It only needs to distinguish applications using the same website +origin. The wallet binds the ID to the website's actual browser origin, so the +same ID used by a different website does not share access. + +Example: + +```text +example-marketplace +``` + +## Installation + +Download `contractless.js` with the application and import it as an ES module: + +```js +import { + ContractlessWallet, + ContractlessWalletNotInstalledError +} from "./contractless.js"; + +const wallet = new ContractlessWallet({ + appId: "example-marketplace", + api: { + url: "https://api.contractless.dev", + address: "dedicated-application-address.cltc", + publicKey: "dedicated-application-public-key", + signature: "precomputed-signature-for-aced", + apiKey: "optional-public-quota-key" + } +}); +``` + +## Detecting The Wallet + +```js +if (!wallet.isInstalled()) { + // Show the application's Contractless Wallet installation link. +} +``` + +The provider is injected at document start. Applications that execute +immediately can use `waitForContractlessWallet()` before showing an +installation notice. + +## Connecting + +Use a server-generated nonce when the signed proof will authenticate a user +with an application server: + +```js +const connection = await wallet.connect({ + message: "Sign in to Example Marketplace", + timestamp: Math.floor(Date.now() / 1000), + nonce: "4f3c2a1b9d8e7f60514233445566778899aabbccddeeff001122334455667788" +}); + +console.log(connection.address); +console.log(connection.signature); +console.log(connection.proof); +``` + +After approval, the wallet generates a 256-bit access key. The library stores +it in the website tab's `sessionStorage`. The matching authorization remains +inside extension session storage. + +## Reading Balances + +Applications can request every balance owned by the connected wallet: + +```js +const balances = await wallet.balances(); +``` + +Use `getBalance()` when only one base coin, token, NFT, or RWA balance is +needed. Missing assets return a zero balance instead of an error: + +```js +const cltc = await wallet.getBalance({ asset: "CLTC" }); +const nft = await wallet.getBalance({ asset: "FLOWERPRINCESS", nftSeries: 0 }); + +console.log(cltc.balance, cltc.balance_atomic); +``` + +Applications may use these methods to warn about an insufficient balance before +requesting a transaction. The wallet still performs its own checks, and the +node remains the final authority because balances can change between lookup +and broadcast. + +## Transaction Errors + +Rejected transaction Promises contain the useful verification reason returned +by the node, such as an insufficient balance, an unregistered participant, or +a fee below the required minimum. Applications should display the Error +message to the user instead of replacing it with a generic failure notice. + +The wallet checks directly named participants before showing transfer, swap, +and loan approvals. This catches unregistered receivers and counterparties +before the user is asked to sign. + +The key is reusable for that wallet unlock session. It is not consumed after +one request. + +The session becomes invalid when: + +- The Contractless wallet locks. +- The active wallet changes. +- The configured wallet lock period expires. +- The application calls `disconnect()`. +- The website tab's session storage is cleared. + +The SDK removes the stored website session as soon as its expiration time is +reached. Applications can update their interface when that happens: + +```js +window.addEventListener("contractless:sessionExpired", () => { + showWalletAsDisconnected(); +}); +``` + +If an application later requests an account, signature, or transaction without +a usable session, the SDK automatically opens the wallet connection flow. A +locked wallet asks the user to unlock it, creates a new session after approval, +and then continues the original request. The website never reuses an expired +access key. + +## Reading The Connected Address + +```js +const accounts = await wallet.accounts(); +``` + +An empty array means the application no longer has a valid wallet session. + +## Signing A Text Message + +```js +const proof = await wallet.signMessage({ + message: "Confirm profile ownership", + nonce: "1188aabbccddeeff00112233445566778899aabbccddeeff00112233445566" +}); +``` + +Text signatures are domain-separated from transactions and cannot move +wallet assets. Website connection and text-signature messages are limited to +75 UTF-8 bytes. The wallet calculates this limit from bytes rather than visible +character count. + +## Sending A Transaction + +```js +const result = await wallet.sendTransaction({ + transaction: { + txtype: 2, + time: Math.floor(Date.now() / 1000), + value: "2500000000", + coin: "CLTC ", + nft_series: 0, + sender: "sender-address.cltc", + receiver: "receiver-address.cltc", + txfee: "25000000" + } +}); + +console.log(result.txid); +console.log(result.bytes); +``` + +Applications must provide the complete transaction object. The wallet +validates and serializes that object, calculates its hash locally, displays +the signed fields, and signs only the hash it calculated. After approval, the +wallet broadcasts the signed bytes through its configured Contractless API. +The transaction ID and signed bytes are returned only after that API accepts +the broadcast. Arbitrary hash signing is not exposed. + +## Swaps And Loan Creation + +Swaps and loan creation use two independent wallet signatures. The application +sends the exact same unsigned transaction to two separate computers. Neither +party receives or validates the other party's signature before signing. + +The first party's browser runs: + +```js +const result = await wallet.signDualTransaction({ + transaction, + signerSlot: 1 +}); + +await sendSignatureToApplication({ + transactionId: result.txid, + result +}); +``` + +The second party's browser independently runs: + +```js +const result = await wallet.signDualTransaction({ + transaction, + signerSlot: 2 +}); + +await sendSignatureToApplication({ + transactionId: result.txid, + result +}); +``` + +Each response contains the same transaction hash, canonical unsigned data, +unsigned wire-byte prefix, the signer's slot, public key, and that wallet's +signature. `sendSignatureToApplication()` represents an application endpoint, +database, matchmaking service, or other shared coordination layer. It is not a +wallet SDK function. + +After the shared application has received both results, it returns them to one +client for final verification, assembly, and broadcast: + +```js +const { transaction, signer1, signer2 } = await getCompletedSigningRequest( + transactionId +); + +const complete = await wallet.completeDualTransaction({ + transaction, + signer1, + signer2 +}); + +const broadcast = await wallet.broadcastTransaction({ + bytes: complete.bytes_hex +}); + +console.log(complete.hash); +console.log(complete.bytes_hex); +console.log(broadcast); +``` + +The final client does not sign again. It only invokes the browser core to +compare the transaction data, verify both Falcon signatures, and construct the +complete bytes. An application backend implementing the same Contractless +codec may instead perform verification, assembly, and broadcasting itself. + +For swaps, signer slot 1 is `sender1` and signer slot 2 is `sender2`. For loan +creation, signer slot 1 is the `lender` and signer slot 2 is the `borrower`. +The application may store pending details locally, but it must never store a +wallet private key or encryption key. + +`completeDualTransaction()` checks that both wallets signed the same hash, +that each public key belongs to the address assigned to its signer slot, and +that both Falcon signatures are valid. It does not ask either wallet to sign +again. + +## Direct API Identity + +An application can use a dedicated, registered, zero-balance wallet identity +for public API lookups and broadcasting already signed transactions. Generate +its reusable handshake signature with the node CLI: + +```bash +./sign_message "aced" +``` + +Configure that address, its public key, and the returned signature in the SDK +constructor as shown above. This proof permits the API and node handshake; it +does not permit the application to sign transactions or spend from the +identity. + +Applications can use the same identity for API lookups: + +```js +const balance = await wallet.apiRequest("/api/v1/balances/base", { + query: { + address: userAddress, + coin: "CLTC" + } +}); +``` + +`apiRequest()` accepts any enabled `/api/v1` route, optional query values, an +HTTP method, and an optional JSON body. It returns the API's `data` value and +throws when the API rejects the request. + +These values are visible to anyone who can inspect the website. Use a dedicated +address that holds no funds. An API key embedded in browser JavaScript is also +public and should only identify traffic or select a quota. Services requiring +a secret API key or HMAC secret must proxy API requests through a backend. + +Contractless API and PHP integration source: + +- https://contractless.dev/contractless/Contractless-PHP-API +- https://contractless.dev/contractless/Contractless-PHP-RPC + +## Disconnecting + +```js +await wallet.disconnect(); +``` + +Applications must never ask users to paste private keys, wallet files, +encryption keys, or access keys into a website. diff --git a/sdk/contractless.js b/sdk/contractless.js new file mode 100644 index 0000000..1388a8a --- /dev/null +++ b/sdk/contractless.js @@ -0,0 +1,399 @@ +const APP_ID_PATTERN = /^[A-Za-z0-9_-]{5,32}$/; +const SESSION_PREFIX = "contractless:web3:"; + +export class ContractlessWalletNotInstalledError extends Error { + constructor() { + super( + "Contractless Wallet is not installed. Install the browser wallet before continuing." + ); + this.name = "ContractlessWalletNotInstalledError"; + this.code = "CONTRACTLESS_WALLET_NOT_INSTALLED"; + } +} + +function randomHex(byteLength) { + const bytes = crypto.getRandomValues(new Uint8Array(byteLength)); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function provider() { + return window.contractless; +} + +function websiteMessage(value, label) { + const message = String(value ?? "").trim(); + if (!message || new TextEncoder().encode(message).length > 75) { + throw new TypeError(`${label} must contain 1 to 75 bytes.`); + } + return message; +} + +export function isContractlessWalletInstalled() { + return Boolean(provider()?.isContractless && typeof provider()?.request === "function"); +} + +export async function waitForContractlessWallet(timeoutMs = 1_500) { + const expiresAt = Date.now() + timeoutMs; + while (Date.now() < expiresAt) { + if (isContractlessWalletInstalled()) return true; + await new Promise((resolve) => setTimeout(resolve, 25)); + } + return isContractlessWalletInstalled(); +} + +export class ContractlessWallet { + constructor({ appId, api = null }) { + if (!APP_ID_PATTERN.test(String(appId ?? ""))) { + throw new TypeError( + "appId must contain 5 to 32 URL-safe characters and must never change for the application." + ); + } + this.appId = appId; + this.api = api; + this.storageKey = `${SESSION_PREFIX}${location.origin}:${appId}`; + this.expirationTimer = undefined; + this.scheduleStoredSessionExpiration(); + } + + isInstalled() { + return isContractlessWalletInstalled(); + } + + session() { + let stored; + try { + stored = sessionStorage.getItem(this.storageKey); + } catch { + return null; + } + if (!stored) return null; + try { + const session = JSON.parse(stored); + if ( + session.origin !== location.origin + || session.appId !== this.appId + || typeof session.accessKey !== "string" + || session.accessKey.length !== 64 + || !Number.isFinite(Number(session.sessionExpiresAt)) + || Number(session.sessionExpiresAt) <= Date.now() + ) { + this.clearSession({ expired: true }); + return null; + } + this.scheduleSessionExpiration(session); + return session; + } catch { + this.clearSession(); + return null; + } + } + + async connect({ message, timestamp, nonce } = {}) { + await this.requireProvider(); + const connectionMessage = websiteMessage( + message ?? `Connect to ${location.origin}`, + "Connection message" + ); + const response = await provider().request({ + method: "contractless_connect", + params: { + origin: location.origin, + appId: this.appId, + message: connectionMessage, + timestamp: Number(timestamp ?? Math.floor(Date.now() / 1_000)), + nonce: String(nonce ?? randomHex(32)) + } + }); + const session = { + origin: location.origin, + appId: this.appId, + address: response.address, + publicKey: response.publicKey, + accessKey: response.accessKey, + sessionExpiresAt: response.sessionExpiresAt + }; + sessionStorage.setItem(this.storageKey, JSON.stringify(session)); + this.scheduleSessionExpiration(session); + return { + ...response, + session + }; + } + + async accounts() { + const response = await this.authorizedRequest("contractless_accounts"); + const accounts = Array.isArray(response) ? response : []; + if (accounts.length === 0) this.clearSession(); + return accounts; + } + + async balances() { + const response = await this.authorizedRequest("contractless_balances"); + return Array.isArray(response?.balances) ? response.balances : []; + } + + async getBalance({ asset, nftSeries = 0 } = {}) { + const requestedAsset = String(asset ?? "").trim().toUpperCase(); + const requestedSeries = Number(nftSeries); + if (!requestedAsset) throw new TypeError("An asset name is required."); + if (!Number.isInteger(requestedSeries) || requestedSeries < 0) { + throw new TypeError("nftSeries must be a non-negative integer."); + } + const balance = (await this.balances()).find((entry) => + String(entry.asset ?? "").trim().toUpperCase() === requestedAsset + && Number(entry.nft_series ?? 0) === requestedSeries + ); + return balance ?? { + asset: requestedAsset, + nft_series: requestedSeries, + balance_atomic: 0, + balance: "0.00000000" + }; + } + + async signMessage({ message, nonce, expiresAt } = {}) { + const signedMessage = websiteMessage(message, "Signed message"); + const now = Math.floor(Date.now() / 1_000); + return this.authorizedRequest("contractless_signMessage", { + message: signedMessage, + nonce: String(nonce ?? randomHex(32)), + expires_at: Number(expiresAt ?? now + 300) + }); + } + + async sendTransaction({ transaction } = {}) { + if (!transaction || typeof transaction !== "object") { + throw new TypeError("A complete transaction object is required."); + } + const txtype = Number(transaction.txtype); + if (txtype === 6 || txtype === 7) { + throw new TypeError( + "Swaps and loan creation require a staged signing workflow." + ); + } + return this.authorizedRequest("contractless_sendTransaction", { + transaction + }); + } + + async signDualTransaction({ + transaction, + signerSlot + } = {}) { + if (!transaction || typeof transaction !== "object") { + throw new TypeError("A complete or partially signed transaction object is required."); + } + const txtype = Number(transaction.txtype + ?? transaction.unsigned_swap?.txtype + ?? transaction.unsigned_loan_contract?.txtype); + if (txtype !== 6 && txtype !== 7) { + throw new TypeError("Staged signing is only available for swaps and loan creation."); + } + if (signerSlot !== 1 && signerSlot !== 2) { + throw new TypeError("signerSlot must be 1 or 2."); + } + return this.authorizedRequest("contractless_signDualTransaction", { + transaction, + signerSlot + }); + } + + async completeDualTransaction({ transaction, signer1, signer2 } = {}) { + if (!transaction || typeof transaction !== "object") { + throw new TypeError("The original unsigned transaction is required."); + } + for (const [name, signer, slot] of [ + ["signer1", signer1, 1], + ["signer2", signer2, 2] + ]) { + if (!signer || Number(signer.signerSlot) !== slot) { + throw new TypeError(`${name} must contain the result for signer slot ${slot}.`); + } + } + if (signer1.txid !== signer2.txid) { + throw new Error("The two wallets signed different transaction hashes."); + } + if (signer1.unsignedBytes !== signer2.unsignedBytes) { + throw new Error("The two wallets serialized different unsigned transaction bytes."); + } + return this.authorizedRequest("contractless_completeDualTransaction", { + transaction, + signature1: signer1.signature, + publicKey1: signer1.signerPublicKey, + signature2: signer2.signature, + publicKey2: signer2.signerPublicKey + }); + } + + async broadcastTransaction({ bytes, api = this.api } = {}) { + if (typeof bytes !== "string" || !/^[0-9a-f]+$/i.test(bytes) || bytes.length % 2) { + throw new TypeError("Complete hexadecimal transaction bytes are required."); + } + return this.apiRequest("/api/v1/transactions/broadcast", { + method: "POST", + body: { transaction_hex: bytes }, + api + }); + } + + async apiRequest(path, { + method = "GET", + query = null, + body = null, + api = this.api + } = {}) { + const url = String(api?.url ?? "").replace(/\/+$/, ""); + const address = String(api?.address ?? "").trim().toLowerCase(); + const publicKey = String(api?.publicKey ?? "").trim().toLowerCase(); + const signature = String(api?.signature ?? "").trim().toLowerCase(); + if (!url || !address || !publicKey || !signature) { + throw new TypeError( + "API url, address, publicKey, and the precomputed 'aced' signature are required." + ); + } + const route = String(path ?? ""); + if (!route.startsWith("/api/v1")) { + throw new TypeError("Contractless API routes must begin with /api/v1."); + } + const requestUrl = new URL(`${url}${route}`); + if (query && typeof query === "object") { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== null) { + requestUrl.searchParams.set(key, String(value)); + } + } + } + const headers = { + "X-Contractless-Address": address, + "X-Contractless-Public-Key": publicKey, + "X-Contractless-Signature": signature + }; + if (api.apiKey) headers["X-API-Key"] = String(api.apiKey); + if (body !== null) headers["Content-Type"] = "application/json"; + const response = await fetch(requestUrl, { + method: String(method).toUpperCase(), + headers, + body: body === null ? undefined : JSON.stringify(body) + }); + const reply = await response.json().catch(() => undefined); + if (!response.ok || reply?.success !== true) { + throw new Error(reply?.error ?? `Contractless API request failed with HTTP ${response.status}.`); + } + return reply.data; + } + + async disconnect() { + const active = this.session(); + if (!active) return { disconnected: true }; + try { + return await this.authorizedRequest( + "contractless_disconnect", + {}, + { reconnect: false } + ); + } finally { + this.clearSession(); + } + } + + clearSession({ expired = false } = {}) { + if (this.expirationTimer !== undefined) { + clearTimeout(this.expirationTimer); + this.expirationTimer = undefined; + } + try { + sessionStorage.removeItem(this.storageKey); + } catch { + // The extension session still expires when the wallet locks. + } + if (expired) { + window.dispatchEvent(new CustomEvent("contractless:sessionExpired", { + detail: { + origin: location.origin, + appId: this.appId + } + })); + } + } + + scheduleStoredSessionExpiration() { + let stored; + try { + stored = sessionStorage.getItem(this.storageKey); + } catch { + return; + } + if (!stored) return; + try { + this.scheduleSessionExpiration(JSON.parse(stored)); + } catch { + this.clearSession(); + } + } + + scheduleSessionExpiration(session) { + if (this.expirationTimer !== undefined) { + clearTimeout(this.expirationTimer); + this.expirationTimer = undefined; + } + const expiresAt = Number(session?.sessionExpiresAt); + if (!Number.isFinite(expiresAt)) return; + const remaining = expiresAt - Date.now(); + if (remaining <= 0) { + this.clearSession({ expired: true }); + return; + } + this.expirationTimer = setTimeout(() => { + this.clearSession({ expired: true }); + }, remaining); + } + + async requireProvider() { + if (!await waitForContractlessWallet()) { + throw new ContractlessWalletNotInstalledError(); + } + return provider(); + } + + async authorizedRequest(method, params = {}, { reconnect = true } = {}) { + await this.requireProvider(); + let session = this.session(); + if (!session && reconnect) { + const connection = await this.connect({ + message: "Connect this application to continue" + }); + session = connection.session; + } + if (!session) { + throw new Error( + "No active Contractless wallet session exists. Call connect() first." + ); + } + try { + return await this.sendAuthorizedRequest(method, params, session); + } catch (error) { + if (/session is invalid|session.*expired|wallet is locked/i.test(String(error))) { + this.clearSession(); + if (reconnect) { + const connection = await this.connect({ + message: "Reconnect this application to continue" + }); + return this.sendAuthorizedRequest(method, params, connection.session); + } + } + throw error; + } + } + + async sendAuthorizedRequest(method, params, session) { + return provider().request({ + method, + params: { + ...params, + origin: location.origin, + appId: this.appId, + accessKey: session.accessKey + } + }); + } +} diff --git a/serve-tests.mjs b/serve-tests.mjs new file mode 100644 index 0000000..160e3e4 --- /dev/null +++ b/serve-tests.mjs @@ -0,0 +1,33 @@ +import { createReadStream } from "node:fs"; +import { createServer } from "node:http"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const root = dirname(fileURLToPath(import.meta.url)); +const port = Number(process.env.PORT ?? 8080); +const files = new Map([ + ["/", ["tests/tests.html", "text/html; charset=utf-8"]], + ["/tests.html", ["tests/tests.html", "text/html; charset=utf-8"]], + ["/sdk/contractless.js", ["sdk/contractless.js", "text/javascript; charset=utf-8"]] +]); + +const server = createServer((request, response) => { + const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname; + const target = files.get(pathname); + if (!target) { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not found"); + return; + } + + response.writeHead(200, { + "Content-Type": target[1], + "Cache-Control": "no-store", + "X-Content-Type-Options": "nosniff" + }); + createReadStream(join(root, target[0])).pipe(response); +}); + +server.listen(port, "127.0.0.1", () => { + console.log(`Contractless wallet test: http://127.0.0.1:${port}/tests.html`); +}); diff --git a/tests/dual-signature-store.php b/tests/dual-signature-store.php new file mode 100644 index 0000000..7aa89cb --- /dev/null +++ b/tests/dual-signature-store.php @@ -0,0 +1,141 @@ + false, 'error' => 'POST is required.']); +} + +$length = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0); +if ($length > 2_000_000) { + reply(413, ['success' => false, 'error' => 'The request body is too large.']); +} + +try { + $request = json_decode(file_get_contents('php://input'), true, 64, JSON_THROW_ON_ERROR); +} catch (JsonException) { + reply(400, ['success' => false, 'error' => 'The request body must be valid JSON.']); +} + +if (!is_array($request)) { + reply(400, ['success' => false, 'error' => 'The request body must be a JSON object.']); +} + +$action = (string) ($request['action'] ?? ''); +$txid = strtolower(trim((string) ($request['txid'] ?? ''))); +if (!preg_match('/^[a-f0-9]{64}$/', $txid)) { + reply(400, ['success' => false, 'error' => 'A valid 64-character transaction hash is required.']); +} + +$storageDirectory = __DIR__ . '/storage/dual-signatures'; +if (!is_dir($storageDirectory) && !mkdir($storageDirectory, 0770, true) && !is_dir($storageDirectory)) { + reply(500, ['success' => false, 'error' => 'The JSON storage directory could not be created.']); +} + +$fileName = $txid . '.json'; +$path = $storageDirectory . '/' . $fileName; + +if ($action === 'load') { + if (!is_file($path)) { + reply(404, ['success' => false, 'error' => 'No saved transaction exists for that hash.']); + } + $handle = fopen($path, 'rb'); + if ($handle === false || !flock($handle, LOCK_SH)) { + if (is_resource($handle)) fclose($handle); + reply(500, ['success' => false, 'error' => 'The saved transaction could not be opened.']); + } + $record = json_decode(stream_get_contents($handle) ?: '', true); + flock($handle, LOCK_UN); + fclose($handle); + if (!is_array($record)) { + reply(500, ['success' => false, 'error' => 'The saved transaction JSON is invalid.']); + } + reply(200, ['success' => true, 'data' => ['file' => $fileName, 'record' => $record]]); +} + +if ($action !== 'save_signature') { + reply(400, ['success' => false, 'error' => 'The storage action is not supported.']); +} + +$kind = strtolower(trim((string) ($request['kind'] ?? ''))); +$slot = (int) ($request['signer_slot'] ?? 0); +$transaction = $request['transaction'] ?? null; +$signature = $request['signature'] ?? null; +if (!in_array($kind, ['swap', 'loan'], true)) { + reply(400, ['success' => false, 'error' => 'The transaction kind must be swap or loan.']); +} +if ($slot !== 1 && $slot !== 2) { + reply(400, ['success' => false, 'error' => 'The signer slot must be 1 or 2.']); +} +if (!is_array($transaction) || !is_array($signature)) { + reply(400, ['success' => false, 'error' => 'The transaction and signature result are required.']); +} +if (strtolower((string) ($signature['txid'] ?? '')) !== $txid) { + reply(400, ['success' => false, 'error' => 'The signature result does not match the transaction hash.']); +} +if ((int) ($signature['signerSlot'] ?? 0) !== $slot) { + reply(400, ['success' => false, 'error' => 'The signature result does not match the signer slot.']); +} + +$handle = fopen($path, 'c+'); +if ($handle === false || !flock($handle, LOCK_EX)) { + if (is_resource($handle)) fclose($handle); + reply(500, ['success' => false, 'error' => 'The transaction JSON could not be locked.']); +} + +$existingJson = stream_get_contents($handle) ?: ''; +$record = $existingJson === '' ? null : json_decode($existingJson, true); +if ($record !== null && !is_array($record)) { + flock($handle, LOCK_UN); + fclose($handle); + reply(500, ['success' => false, 'error' => 'The existing transaction JSON is invalid.']); +} +if ($record !== null && ( + ($record['kind'] ?? null) !== $kind + || json_encode($record['transaction'] ?? null) !== json_encode($transaction) +)) { + flock($handle, LOCK_UN); + fclose($handle); + reply(409, ['success' => false, 'error' => 'That hash is already associated with different transaction data.']); +} + +$record ??= [ + 'txid' => $txid, + 'kind' => $kind, + 'transaction' => $transaction, + 'signer1' => null, + 'signer2' => null, + 'created_at' => gmdate(DATE_ATOM), +]; +$record['signer' . $slot] = $signature; +$record['updated_at'] = gmdate(DATE_ATOM); +$encoded = json_encode($record, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); +if ($encoded === false) { + flock($handle, LOCK_UN); + fclose($handle); + reply(500, ['success' => false, 'error' => 'The transaction record could not be encoded.']); +} + +rewind($handle); +ftruncate($handle, 0); +if (fwrite($handle, $encoded . PHP_EOL) === false) { + flock($handle, LOCK_UN); + fclose($handle); + reply(500, ['success' => false, 'error' => 'The transaction record could not be saved.']); +} +fflush($handle); +flock($handle, LOCK_UN); +fclose($handle); + +reply(200, ['success' => true, 'data' => ['file' => $fileName, 'record' => $record]]); + diff --git a/tests/storage/.gitkeep b/tests/storage/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/storage/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/tests.html b/tests/tests.html new file mode 100644 index 0000000..ffae2d0 --- /dev/null +++ b/tests/tests.html @@ -0,0 +1,1652 @@ + + + + + + Contractless Wallet Integration Test + + + +
+

Contractless Wallet Integration Test

+

Website SDK and browser extension connection checks

+
+ +
+
+
+ Wallet detected + Checking +
+
+ Application session + Not connected +
+
+ Address retrieved + Not tested +
+
+ +
+ + + + +
+ +
+

Connected Wallet

+
+
Origin
+
+
Application ID
+
+
Address
+
No address received
+
Session expires
+
No active session
+
+
+ +
+

Wallet Connection Code

+

+        
+ +
+
+ +
+

Transaction Examples

+

+ Select a transaction type, replace any example-dependent values, then + submit it to the wallet for review, signing, and broadcast. + Dual-signature tests use a small PHP backend to save one JSON file + per transaction hash, allowing two separate computers to sign the + same transaction before it is broadcast. +

+
+ + + +
+
+ +
+
+

Editable Transaction JSON

+ +
+
+

Application Code

+

+          
+
+
+ + +
+ +
+
+ + + +