diff --git a/.github/workflows/bindings-nodejs.yml b/.github/workflows/bindings-nodejs.yml index 28f1c03aad..886906126b 100644 --- a/.github/workflows/bindings-nodejs.yml +++ b/.github/workflows/bindings-nodejs.yml @@ -2,7 +2,7 @@ name: Nodejs bindings checks on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".github/actions/**" - "**.rs" # Include all rust files @@ -15,7 +15,7 @@ on: - "bindings/nodejs/**" - ".github/workflows/bindings-nodejs.yml" pull_request: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".github/actions/**" - "**.rs" # Include all rust files diff --git a/.github/workflows/bindings-python-publish.yml b/.github/workflows/bindings-python-publish.yml index 1bc832619f..fbf928fa25 100644 --- a/.github/workflows/bindings-python-publish.yml +++ b/.github/workflows/bindings-python-publish.yml @@ -130,7 +130,7 @@ jobs: pip install -r requirements-dev.txt pip install patchelf pip install maturin - maturin build --out ../../../dist --profile=production + maturin build --out ../../dist --profile=production - name: Upload wheels uses: actions/upload-artifact@v3 diff --git a/.github/workflows/bindings-python.yml b/.github/workflows/bindings-python.yml index 14ce95321f..190048b735 100644 --- a/.github/workflows/bindings-python.yml +++ b/.github/workflows/bindings-python.yml @@ -2,7 +2,7 @@ name: Python bindings checks on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".github/actions/**" - "**.rs" # Include all rust files @@ -15,7 +15,7 @@ on: - "bindings/python/**" - ".github/workflows/bindings-python.yml" pull_request: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".github/actions/**" - "**.rs" # Include all rust files @@ -120,5 +120,4 @@ jobs: # TODO temporarily disabled https://github.com/iotaledger/iota-sdk/issues/647 # - name: Run tests # working-directory: bindings/python - # run: tox - + # run: tox \ No newline at end of file diff --git a/.github/workflows/bindings-wasm.yml b/.github/workflows/bindings-wasm.yml index 2d29b074c0..196dc27a61 100644 --- a/.github/workflows/bindings-wasm.yml +++ b/.github/workflows/bindings-wasm.yml @@ -1,7 +1,7 @@ name: Wasm bindings checks on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".github/actions/**" - "**.rs" # Include all rust files @@ -15,7 +15,7 @@ on: - "bindings/nodejs/**" - ".github/workflows/bindings-wasm.yml" pull_request: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".github/actions/**" - "**.rs" # Include all rust files diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8d7b53df3b..4a0646618a 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -2,7 +2,7 @@ name: Build and Run Tests on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/build-and-test.yml" @@ -12,7 +12,7 @@ on: - "**Cargo.lock" # Include all Cargo.lock files - "!**/bindings/**" # Exclude all bindings pull_request: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/build-and-test.yml" diff --git a/.github/workflows/common-features.yml b/.github/workflows/common-features.yml new file mode 100644 index 0000000000..0c566bf693 --- /dev/null +++ b/.github/workflows/common-features.yml @@ -0,0 +1,66 @@ +name: Common Feature Sets +on: + push: + branches: [develop, production, "1.1"] + paths: + - ".cargo/config.toml" + - ".github/workflows/common-features.yml" + - ".github/actions/**" + - "**.rs" # Include all rust files + - "**Cargo.toml" # Include all Cargo.toml files + - "**Cargo.lock" # Include all Cargo.lock files + - "!**/bindings/**" # Exclude all bindings + pull_request: + branches: [develop, production, "1.1"] + paths: + - ".cargo/config.toml" + - ".github/workflows/common-features.yml" + - ".github/actions/**" + - "**.rs" # Include all rust files + - "**Cargo.toml" # Include all Cargo.toml files + - "**Cargo.lock" # Include all Cargo.lock files + - "!**/bindings/**" # Exclude all bindings + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_INCREMENTAL: 0 + +jobs: + common-sets: + name: Check common feature sets + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + set: + - pow + - irc_27 + - irc_30 + - client,private_key_secret_manager + - client,mqtt + - client,participation + - wallet,storage + - wallet,stronghold + - wallet,rocksdb + - wallet,participation + - wallet,events + - wallet,events,ledger_nano + + steps: + - name: Checkout the Source Code + uses: actions/checkout@v3 + + - name: Set up Rust + uses: ./.github/actions/setup-rust + + # Required for ledger-nano + - name: Install required packages (Ubuntu) + run: | + sudo apt-get update + sudo apt-get install libudev-dev libusb-1.0-0-dev + + - name: Check features [ ${{ matrix.set }} ] + run: cargo check --no-default-features -p iota-sdk -F ${{ matrix.set }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 845caea9c2..77502ad596 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,7 @@ name: Test coverage on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/coverage.yml" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8884a2179c..db2122a7a1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,7 @@ name: Linting on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/lint.yml" @@ -11,7 +11,7 @@ on: - "**Cargo.lock" # Include all Cargo.lock files - "!**/bindings/**" # Exclude all bindings pull_request: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/lint.yml" diff --git a/.github/workflows/private-tangle-tests.yml b/.github/workflows/private-tangle-tests.yml index f7980cad08..1650aef093 100644 --- a/.github/workflows/private-tangle-tests.yml +++ b/.github/workflows/private-tangle-tests.yml @@ -2,7 +2,7 @@ name: Build and run specific tests on a private tangle on: push: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" @@ -14,7 +14,7 @@ on: - "**Cargo.lock" - "!cli/**" # Exclude CLI pull_request: - branches: [develop, production, 2.0] + branches: [develop, production, "2.0"] paths: - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" diff --git a/Cargo.lock b/Cargo.lock index f2a24f102b..a92603b205 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead", "aes", @@ -65,9 +65,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -461,9 +461,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.3" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" dependencies = [ "clap_builder", "clap_derive", @@ -471,9 +471,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.2" +version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" dependencies = [ "anstream", "anstyle", @@ -512,12 +512,24 @@ dependencies = [ "iota-sdk", "log", "prefix-hex", + "rustyline", "serde_json", "thiserror", "tokio", "zeroize", ] +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -673,9 +685,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", @@ -863,7 +875,7 @@ version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" dependencies = [ - "curve25519-dalek 4.1.0", + "curve25519-dalek 4.1.1", "ed25519", "hashbrown 0.14.0", "hex", @@ -909,6 +921,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enum-iterator" version = "0.6.0" @@ -956,6 +974,27 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "fern" version = "0.6.2" @@ -1284,9 +1323,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1324,6 +1363,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "http" version = "0.2.9" @@ -1621,7 +1669,7 @@ dependencies = [ [[package]] name = "iota-sdk-python" -version = "1.0.2" +version = "1.0.3" dependencies = [ "futures", "iota-sdk-bindings-core", @@ -1981,6 +2029,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.24.3" @@ -1993,6 +2050,17 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -2066,9 +2134,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "packable" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99ae55c2e3dc657f87a74d549bfe44187dba690310738d384454b8101f7c0f8" +checksum = "11259b086696fc9256f790485d8f14f11f0fa60a60351af9693e3d49fd24fdb6" dependencies = [ "autocfg", "packable-derive", @@ -2419,6 +2487,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -2649,9 +2727,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno", @@ -2668,7 +2746,7 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", - "rustls-webpki 0.101.5", + "rustls-webpki 0.101.6", "sct", ] @@ -2705,14 +2783,49 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.5" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", ] +[[package]] +name = "rustyline" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix 0.26.4", + "radix_trie", + "rustyline-derive", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "rustyline-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a32af5427251d2e4be14fc151eabe18abb4a7aad5efee7044da9f096c906a43" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "ryu" version = "1.0.15" @@ -2898,9 +3011,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2951,9 +3064,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "snafu" @@ -3028,6 +3141,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "stronghold-derive" version = "1.0.0" @@ -3050,7 +3169,7 @@ dependencies = [ "libc", "libsodium-sys", "log", - "nix", + "nix 0.24.3", "rand", "serde", "thiserror", @@ -3266,9 +3385,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -3398,11 +3517,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unindent" @@ -3610,9 +3735,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] diff --git a/bindings/core/Cargo.toml b/bindings/core/Cargo.toml index 0c754e909d..ab15484fa3 100644 --- a/bindings/core/Cargo.toml +++ b/bindings/core/Cargo.toml @@ -23,7 +23,7 @@ iota-crypto = { version = "0.23.0", default-features = false, features = [ "bip44", ] } log = { version = "0.4.20", default-features = false } -packable = { version = "0.8.2", default-features = false } +packable = { version = "0.8.3", default-features = false } prefix-hex = { version = "0.7.1", default-features = false } primitive-types = { version = "0.12.1", default-features = false } serde = { version = "1.0.188", default-features = false } diff --git a/bindings/core/src/method/client.rs b/bindings/core/src/method/client.rs index adfe2b977f..3488f4f04e 100644 --- a/bindings/core/src/method/client.rs +++ b/bindings/core/src/method/client.rs @@ -205,6 +205,12 @@ pub enum ClientMethod { ////////////////////////////////////////////////////////////////////// // Node indexer API ////////////////////////////////////////////////////////////////////// + /// Fetch alias/basic/NFT/foundry output IDs + #[serde(rename_all = "camelCase")] + OutputIds { + /// Query parameters for output requests + query_parameters: Vec, + }, /// Fetch basic output IDs #[serde(rename_all = "camelCase")] BasicOutputIds { diff --git a/bindings/core/src/method_handler/client.rs b/bindings/core/src/method_handler/client.rs index 1289b02f65..40347336dc 100644 --- a/bindings/core/src/method_handler/client.rs +++ b/bindings/core/src/method_handler/client.rs @@ -234,6 +234,9 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM ClientMethod::GetIncludedBlockMetadata { transaction_id } => { Response::BlockMetadata(client.get_included_block_metadata(&transaction_id).await?) } + ClientMethod::OutputIds { query_parameters } => { + Response::OutputIdsResponse(client.output_ids(query_parameters).await?) + } ClientMethod::BasicOutputIds { query_parameters } => { Response::OutputIdsResponse(client.basic_output_ids(query_parameters).await?) } diff --git a/bindings/core/src/response.rs b/bindings/core/src/response.rs index 19ce50143d..693bee2335 100644 --- a/bindings/core/src/response.rs +++ b/bindings/core/src/response.rs @@ -137,6 +137,7 @@ pub enum Response { /// - [`BasicOutputIds`](crate::method::ClientMethod::BasicOutputIds) /// - [`FoundryOutputIds`](crate::method::ClientMethod::FoundryOutputIds) /// - [`NftOutputIds`](crate::method::ClientMethod::NftOutputIds) + /// - [`OutputIds`](crate::method::ClientMethod::OutputIds) OutputIdsResponse(OutputIdsResponse), /// Response for: /// - [`FindBlocks`](crate::method::ClientMethod::FindBlocks) diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index df82e74815..203d9f9861 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Account::{burn(), consolidateOutputs(), createAliasOutput(), meltNativeToken(), mintNativeToken(), createNativeToken(), mintNfts(), sendTransaction(), sendNativeTokens(), sendNft()}` methods; +- `Client::outputIds()` method; +- `GenericQueryParameter, UnlockableByAddress` types; ## 1.0.11 - 2023-09-14 diff --git a/bindings/nodejs/lib/client/client.ts b/bindings/nodejs/lib/client/client.ts index 75bb908fce..8c347f5247 100644 --- a/bindings/nodejs/lib/client/client.ts +++ b/bindings/nodejs/lib/client/client.ts @@ -16,6 +16,7 @@ import { FoundryQueryParameter, NftQueryParameter, AccountQueryParameter, + GenericQueryParameter, } from '../types/client'; import type { INodeInfoWrapper } from '../types/client/nodeInfo'; import { @@ -88,6 +89,22 @@ export class Client { return JSON.parse(response).payload; } + /** + * Fetch alias/basic/NFT/foundry output IDs based on the given query parameters. + */ + async outputIds( + queryParameters: GenericQueryParameter[], + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'outputIds', + data: { + queryParameters, + }, + }); + + return JSON.parse(response).payload; + } + /** * Fetch basic output IDs based on the given query parameters. */ diff --git a/bindings/nodejs/lib/types/client/bridge/client.ts b/bindings/nodejs/lib/types/client/bridge/client.ts index 1cd3f8875a..fc0fe3f988 100644 --- a/bindings/nodejs/lib/types/client/bridge/client.ts +++ b/bindings/nodejs/lib/types/client/bridge/client.ts @@ -10,6 +10,7 @@ import type { PreparedTransactionData } from '../prepared-transaction-data'; import type { AccountQueryParameter, FoundryQueryParameter, + GenericQueryParameter, NftQueryParameter, QueryParameter, } from '../query-parameters'; @@ -31,6 +32,13 @@ export interface __GetOutputMethod__ { }; } +export interface __GetOutputIdsMethod__ { + name: 'outputIds'; + data: { + queryParameters: GenericQueryParameter[]; + }; +} + export interface __GetBasicOutputIdsMethod__ { name: 'basicOutputIds'; data: { diff --git a/bindings/nodejs/lib/types/client/bridge/index.ts b/bindings/nodejs/lib/types/client/bridge/index.ts index 96453df62e..e807215cec 100644 --- a/bindings/nodejs/lib/types/client/bridge/index.ts +++ b/bindings/nodejs/lib/types/client/bridge/index.ts @@ -3,6 +3,7 @@ import type { __GetInfoMethod__, + __GetOutputIdsMethod__, __GetBasicOutputIdsMethod__, __GetOutputMethod__, __GetOutputsMethod__, @@ -52,6 +53,7 @@ import type { export type __ClientMethods__ = | __GetInfoMethod__ | __GetOutputMethod__ + | __GetOutputIdsMethod__ | __GetBasicOutputIdsMethod__ | __GetOutputsMethod__ | __PostBlockMethod__ diff --git a/bindings/nodejs/lib/types/client/query-parameters.ts b/bindings/nodejs/lib/types/client/query-parameters.ts index aadc5193d5..6afbf28c13 100644 --- a/bindings/nodejs/lib/types/client/query-parameters.ts +++ b/bindings/nodejs/lib/types/client/query-parameters.ts @@ -64,6 +64,17 @@ type CommonQueryParameters = | PageSize | Cursor; +/** Query parameters for filtering alias/basic/NFT/foundry Outputs*/ +export type GenericQueryParameter = + | UnlockableByAddress + | HasNativeTokens + | MinNativeTokenCount + | MaxNativeTokenCount + | CreatedAfter + | CreatedBefore + | PageSize + | Cursor; + /** Bech32-encoded address that should be searched for. */ interface Address { address: string; @@ -157,7 +168,11 @@ interface StateController { interface Governor { governor: string; } -/** Define the page size for the results */ +/** Define the page size for the results. */ interface PageSize { pageSize: number; } +/** Returns outputs that are unlockable by the bech32 address. */ +interface UnlockableByAddress { + unlockableByAddress: string; +} diff --git a/bindings/nodejs/package.json b/bindings/nodejs/package.json index 8893283c0b..0471b271cd 100644 --- a/bindings/nodejs/package.json +++ b/bindings/nodejs/package.json @@ -18,7 +18,7 @@ "prebuild-windows-arm64": "prebuild --runtime napi --target 6 --prepack 'yarn run neon-build-windows-arm64' --strip --arch arm64", "neon-build-windows-arm64": "cargo-cp-artifact -ac iota-sdk-nodejs ./index.node -- cargo build --profile=production --message-format=json-render-diagnostics --target aarch64-pc-windows-msvc && node -e \"require('./scripts/move-artifact.js')()\"", "rebuild": "node scripts/neon-build && tsc && node scripts/strip.js", - "install": "prebuild-install --runtime napi --tag-prefix='iota-sdk-nodejs-v' && tsc || npm run rebuild", + "install": "prebuild-install --runtime napi --tag-prefix=iota-sdk-nodejs-v && tsc || npm run rebuild", "test": "jest", "create-api-docs": "typedoc ./lib/index.ts --githubPages false --disableSources --excludePrivate --excludeInternal --plugin typedoc-plugin-markdown --theme markdown --hideBreadcrumbs --entryDocument api_ref.md --readme none --hideGenerator --sort source-order" }, diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 21acfb10f0..c5c04e213c 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -26,6 +26,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConflictReason` display implementation with an explanation of the conflict; - `Account::{burn(), consolidate_outputs(), create_alias_output(), create_native_token(), melt_native_token(), mint_native_token(), mint_nfts(), send_transaction(), send_native_tokens(), send_nft()}` methods; +- `Irc27Metadata` and `Irc30Metadata` helpers; +- `Client::output_ids()` method; +- `QueryParameter::unlockable_by_address` field; + +## 1.0.3 - 2023-09-19 + +### Fixed + +- Wheel upload; ## 1.0.2 - 2023-09-12 diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index a71e89e167..d6257d7cec 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iota-sdk-python" -version = "1.0.2" +version = "1.0.3" authors = ["IOTA Stiftung"] edition = "2021" description = "Python bindings for the IOTA SDK library" diff --git a/bindings/python/examples/client/build_nft.py b/bindings/python/examples/client/build_nft.py index be055765d2..fd30b5cb6b 100644 --- a/bindings/python/examples/client/build_nft.py +++ b/bindings/python/examples/client/build_nft.py @@ -3,9 +3,18 @@ from dotenv import load_dotenv -from iota_sdk import (AddressUnlockCondition, Client, Ed25519Address, - IssuerFeature, MetadataFeature, SenderFeature, - TagFeature, Utils, utf8_to_hex) +from iota_sdk import ( + AddressUnlockCondition, + Client, + Ed25519Address, + IssuerFeature, + MetadataFeature, + SenderFeature, + TagFeature, + Utils, + utf8_to_hex, + Irc27Metadata, +) load_dotenv() @@ -17,15 +26,11 @@ hexAddress = Utils.bech32_to_hex( 'rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy') -# IOTA NFT Standard - IRC27: -# https://github.com/iotaledger/tips/blob/main/tips/TIP-0027/tip-0027.md -tip_27_immutable_metadata = { - "standard": "IRC27", - "version": "v1.0", - "type": "image/jpeg", - "uri": "https://mywebsite.com/my-nft-files-1.jpeg", - "name": "My NFT #0001" -} +tip_27_immutable_metadata = Irc27Metadata( + "image/jpeg", + "https://mywebsite.com/my-nft-files-1.jpeg", + "My NFT #0001", +) # Build NFT output nft_output = client.build_nft_output( @@ -36,8 +41,7 @@ nft_id='0x0000000000000000000000000000000000000000000000000000000000000000', immutable_features=[ IssuerFeature(Ed25519Address(hexAddress)), - MetadataFeature(utf8_to_hex(json.dumps( - tip_27_immutable_metadata, separators=(',', ':')))) + tip_27_immutable_metadata.as_feature() ], features=[ SenderFeature(Ed25519Address(hexAddress)), diff --git a/bindings/python/examples/how_tos/account_output/governance_transition.py b/bindings/python/examples/how_tos/account_output/governance_transition.py index 44dc5a0531..244f49c9f4 100644 --- a/bindings/python/examples/how_tos/account_output/governance_transition.py +++ b/bindings/python/examples/how_tos/account_output/governance_transition.py @@ -45,7 +45,9 @@ def update_state_controller(unlock_condition): - """Update unlock condition if it's a state controlled type.""" + """ + Replace the address in the StateControllerAddressUnlockCondition + """ if unlock_condition.type == UnlockConditionType.StateControllerAddress: return StateControllerAddressUnlockCondition(new_state_controller) return unlock_condition diff --git a/bindings/python/examples/how_tos/native_tokens/create.py b/bindings/python/examples/how_tos/native_tokens/create.py index 335ebcbb22..94b3fa5068 100644 --- a/bindings/python/examples/how_tos/native_tokens/create.py +++ b/bindings/python/examples/how_tos/native_tokens/create.py @@ -2,7 +2,7 @@ from dotenv import load_dotenv -from iota_sdk import CreateNativeTokenParams, Wallet, utf8_to_hex +from iota_sdk import CreateNativeTokenParams, Wallet, Irc30Metadata load_dotenv() @@ -38,10 +38,14 @@ print('Preparing transaction to create native token...') +metadata = Irc30Metadata( + "My Native Token", "MNT", 10, description="A native token to test the iota-sdk." +) + params = CreateNativeTokenParams( 100, 100, - utf8_to_hex('Hello, World!'), + metadata.as_hex(), ) prepared_transaction = account.prepare_create_native_token(params, None) diff --git a/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py b/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py index e3fb66af60..fb1acd5c4e 100644 --- a/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py +++ b/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py @@ -1,10 +1,9 @@ -import json import os import sys from dotenv import load_dotenv -from iota_sdk import MintNftParams, Utils, Wallet, utf8_to_hex +from iota_sdk import MintNftParams, Utils, Wallet, Irc27Metadata load_dotenv() @@ -36,41 +35,43 @@ issuer = Utils.nft_id_to_bech32(issuer_nft_id, bech32_hrp) -def get_immutable_metadata(index: int, collection_id: str) -> str: +def get_immutable_metadata(index: int) -> str: """Returns the immutable metadata for the NFT with the given index""" - data = { - "standard": "IRC27", - "version": "v1.0", - "type": "video/mp4", - "uri": "ipfs://wrongcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5Ywrong", - "name": "Shimmer OG NFT #" + str(index), - "description": "The Shimmer OG NFT was handed out 1337 times by the IOTA Foundation to celebrate the official launch of the Shimmer Network.", - "issuerName": "IOTA Foundation", - "collectionId": collection_id, - "collectionName": "Shimmer OG" - } - return json.dumps(data, separators=(',', ':')) + Irc27Metadata( + "video/mp4", + "https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT", + "Shimmer OG NFT #" + str(index), + description="The Shimmer OG NFT was handed out 1337 times by the IOTA Foundation to celebrate the official launch of the Shimmer Network.", + issuerName="IOTA Foundation", + collectionName="Shimmer OG", + ).as_hex() # Create the metadata with another index for each -nft_mint_params = list(map(lambda index: MintNftParams( - immutable_metadata=utf8_to_hex( - get_immutable_metadata(index, issuer_nft_id)), - issuer=issuer -), range(NFT_COLLECTION_SIZE))) +nft_mint_params = list( + map( + lambda index: MintNftParams( + immutable_metadata=get_immutable_metadata(index), issuer=issuer + ), + range(NFT_COLLECTION_SIZE), + ) +) while nft_mint_params: - chunk, nft_mint_params = nft_mint_params[:NUM_NFTS_MINTED_PER_TRANSACTION], nft_mint_params[NUM_NFTS_MINTED_PER_TRANSACTION:] + chunk, nft_mint_params = ( + nft_mint_params[:NUM_NFTS_MINTED_PER_TRANSACTION], + nft_mint_params[NUM_NFTS_MINTED_PER_TRANSACTION:], + ) print( - f'Minting {len(chunk)} NFTs... ({NFT_COLLECTION_SIZE-len(nft_mint_params)}/{NFT_COLLECTION_SIZE})') + f'Minting {len(chunk)} NFTs... ({NFT_COLLECTION_SIZE-len(nft_mint_params)}/{NFT_COLLECTION_SIZE})' + ) transaction = account.mint_nfts(chunk) # Wait for transaction to get included block_id = account.reissue_transaction_until_included( transaction.transaction_id) - print( - f'Block sent: {os.environ["EXPLORER_URL"]}/block/{block_id}') + print(f'Block sent: {os.environ["EXPLORER_URL"]}/block/{block_id}') # Sync so the new outputs are available again for new transactions account.sync() diff --git a/bindings/python/iota_sdk/__init__.py b/bindings/python/iota_sdk/__init__.py index 1778053ae6..a7ad78d8a1 100644 --- a/bindings/python/iota_sdk/__init__.py +++ b/bindings/python/iota_sdk/__init__.py @@ -20,6 +20,8 @@ from .types.context_input import * from .types.event import * from .types.feature import * +from .types.irc_27 import * +from .types.irc_30 import * from .types.filter_options import * from .types.input import * from .types.native_token import * diff --git a/bindings/python/iota_sdk/client/_node_indexer_api.py b/bindings/python/iota_sdk/client/_node_indexer_api.py index 79bc972b8b..da44ef8396 100644 --- a/bindings/python/iota_sdk/client/_node_indexer_api.py +++ b/bindings/python/iota_sdk/client/_node_indexer_api.py @@ -67,6 +67,8 @@ class QueryParameters: Returns outputs that are timelocked after a certain Unix timestamp. timelocked_before : Returns outputs that are timelocked before a certain Unix timestamp. + unlockable_by_address : + Returns outputs that are unlockable by the bech32 address. """ address: Optional[str] = None account_address: Optional[str] = None @@ -91,6 +93,7 @@ class QueryParameters: tag: Optional[str] = None timelocked_after: Optional[int] = None timelocked_before: Optional[int] = None + unlockable_by_address: Optional[str] = None class OutputIdsResponse: """Response type for output IDs. @@ -107,6 +110,22 @@ def __init__(self, output_dict: Dict): self.items = [OutputId.from_string( output_id) for output_id in output_dict["items"]] + def output_ids( + self, query_parameters: QueryParameters) -> OutputIdsResponse: + """Fetch alias/basic/NFT/foundry output IDs from the given query parameters. + Supported query parameters are: "hasNativeTokens", "minNativeTokenCount", "maxNativeTokenCount", "unlockableByAddress", "createdBefore", "createdAfter", "cursor", "pageSize". + + Returns: + The corresponding output IDs of the outputs. + """ + + query_parameters_camelized = query_parameters.as_dict() + + response = self._call_method('outputIds', { + 'queryParameters': query_parameters_camelized, + }) + return self.OutputIdsResponse(response) + def basic_output_ids( self, query_parameters: QueryParameters) -> OutputIdsResponse: """Fetch basic output IDs from the given query parameters. diff --git a/bindings/python/iota_sdk/types/essence.py b/bindings/python/iota_sdk/types/essence.py index 7185423d23..23e448e0eb 100644 --- a/bindings/python/iota_sdk/types/essence.py +++ b/bindings/python/iota_sdk/types/essence.py @@ -48,10 +48,14 @@ class RegularTransactionEssence(TransactionEssence): network_id: str # TODO: Replace with a proper SlotIndex type creation_slot: HexStr - context_inputs: Optional[List[Union[CommitmentInput, BlockIssuanceCreditInput, RewardInput]]] = None + context_inputs: Optional[List[Union[CommitmentInput, + BlockIssuanceCreditInput, RewardInput]]] = None inputs: List[UtxoInput] inputs_commitment: HexStr - outputs: List[Union[BasicOutput, AccountOutput, FoundryOutput, NftOutput, DelegationOutput]] + outputs: List[Union[BasicOutput, AccountOutput, + FoundryOutput, NftOutput, DelegationOutput]] allotments: Optional[List[Allotment]] = None payload: Optional[TaggedDataPayload] = None - type: int = field(default_factory=lambda: EssenceType.RegularTransactionEssence, init=False) + type: int = field( + default_factory=lambda: EssenceType.RegularTransactionEssence, + init=False) diff --git a/bindings/python/iota_sdk/types/irc_27.py b/bindings/python/iota_sdk/types/irc_27.py new file mode 100644 index 0000000000..a589a7748a --- /dev/null +++ b/bindings/python/iota_sdk/types/irc_27.py @@ -0,0 +1,66 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +import json +from iota_sdk import utf8_to_hex, MetadataFeature +from dataclasses import dataclass, field +from typing import Optional, List, Any +from dacite import from_dict + + +@dataclass +class Attribute: + """An attribute which follows [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). + Attributes: + trait_type: The trait type. + value: The value of the specified Attribute. + display_type: The optional type used to display the Attribute. + """ + + trait_type: str + value: Any + display_type: Optional[str] = None + + +@dataclass +class Irc27Metadata: + """The IRC27 NFT standard schema. + Attributes: + standard: The metadata standard (IRC27). + version: The metadata spec version (v1.0). + type: The media type (MIME) of the asset. + Examples: + - Image files: `image/jpeg`, `image/png`, `image/gif`, etc. + - Video files: `video/x-msvideo` (avi), `video/mp4`, `video/mpeg`, etc. + - Audio files: `audio/mpeg`, `audio/wav`, etc. + - 3D Assets: `model/obj`, `model/u3d`, etc. + - Documents: `application/pdf`, `text/plain`, etc. + uri: URL pointing to the NFT file location. + name: The human-readable name of the native token. + collectionName: The human-readable collection name of the native token. + royalties: Royalty payment addresses mapped to the payout percentage. + issuerName: The human-readable name of the native token creator. + description: The human-readable description of the token. + attributes: Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). + """ + + standard: str = field(default="IRC27", init=False) + version: str = field(default="v1.0", init=False) + type: str + uri: str + name: str + collectionName: Optional[str] = None + royalties: dict[str, float] = field(default_factory=dict) + issuerName: Optional[str] = None + description: Optional[str] = None + attributes: List[Attribute] = field(default_factory=list) + + @staticmethod + def from_dict(metadata_dict: dict): + return from_dict(Irc27Metadata, metadata_dict) + + def as_hex(self): + utf8_to_hex(json.dumps(self.as_dict(), separators=(",", ":"))) + + def as_feature(self): + MetadataFeature(self.as_hex()) diff --git a/bindings/python/iota_sdk/types/irc_30.py b/bindings/python/iota_sdk/types/irc_30.py new file mode 100644 index 0000000000..ebb958fbdb --- /dev/null +++ b/bindings/python/iota_sdk/types/irc_30.py @@ -0,0 +1,43 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +import json +from iota_sdk.types.common import HexStr +from iota_sdk import utf8_to_hex, MetadataFeature +from dataclasses import dataclass, field +from typing import Optional +from dacite import from_dict + + +@dataclass +class Irc30Metadata: + """The IRC30 native token metadata standard schema. + Attributes: + standard: The metadata standard (IRC30). + name: The human-readable name of the native token. + symbol: The symbol/ticker of the token. + decimals: Number of decimals the token uses (divide the token amount by 10^decimals to get its user representation). + description: The human-readable description of the token. + url: URL pointing to more resources about the token. + logoUrl: URL pointing to an image resource of the token logo. + logo: The svg logo of the token encoded as a byte string. + """ + + standard: str = field(default="IRC30", init=False) + name: str + symbol: str + decimals: int + description: Optional[str] = None + url: Optional[str] = None + logoUrl: Optional[str] = None + logo: Optional[HexStr] = None + + @staticmethod + def from_dict(metadata_dict: dict): + return from_dict(Irc30Metadata, metadata_dict) + + def as_hex(self): + utf8_to_hex(json.dumps(self.as_dict(), separators=(",", ":"))) + + def as_feature(self): + MetadataFeature(self.as_hex()) diff --git a/bindings/python/iota_sdk/types/send_params.py b/bindings/python/iota_sdk/types/send_params.py index 316318f041..b153303370 100644 --- a/bindings/python/iota_sdk/types/send_params.py +++ b/bindings/python/iota_sdk/types/send_params.py @@ -16,8 +16,10 @@ class SendParams(): Attributes: address: The address to send to. amount: The amount to send. - return_address : The address to return the funds to if not claimed. - expiration: The expiration timestamp until funds can be claimed. + return_address: The address to return the funds to if not claimed. + expiration: Expiration in seconds, after which the output will be available for the sender again, if not spent by the + receiver already. The expiration will only be used if one is necessary given the provided amount. If an + expiration is needed but not provided, it will default to one day. """ address: str amount: str diff --git a/bindings/python/iota_sdk/utils.py b/bindings/python/iota_sdk/utils.py index 09a4864e71..12905bc998 100644 --- a/bindings/python/iota_sdk/utils.py +++ b/bindings/python/iota_sdk/utils.py @@ -58,7 +58,8 @@ def nft_id_to_bech32(nft_id: HexStr, bech32_hrp: str) -> str: }) @staticmethod - def hex_public_key_to_bech32_address(hex_str: HexStr, bech32_hrp: str) -> str: + def hex_public_key_to_bech32_address( + hex_str: HexStr, bech32_hrp: str) -> str: """Convert a hex encoded public key to a Bech32 encoded address. """ return _call_method('hexPublicKeyToBech32Address', { diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 620d7bfc9d..2ebc287ebd 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "maturin" [project] name = "iota-sdk" +dependencies = ["dacite >= 1.8.1", "pyhumps >= 3.8.0"] [tool.maturin] python-packages = ["iota_sdk"] \ No newline at end of file diff --git a/bindings/python/setup.py b/bindings/python/setup.py index e709f935d0..70be8a1d84 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -22,7 +22,7 @@ def get_py_version_cfgs(): setup( name="iota_sdk", - version="1.0.2", + version="1.0.3", classifiers=[ "License :: SPDX-License-Identifier :: Apache-2.0", "Intended Audience :: Developers", diff --git a/bindings/python/tests/test_offline.py b/bindings/python/tests/test_offline.py index bea741357f..f81ca2f36d 100644 --- a/bindings/python/tests/test_offline.py +++ b/bindings/python/tests/test_offline.py @@ -2,7 +2,7 @@ # Copyright 2023 IOTA Stiftung # SPDX-License-Identifier: Apache-2.0 -from iota_sdk import Block, Client, MnemonicSecretManager, Utils, SecretManager, OutputId, hex_to_utf8, utf8_to_hex, Bip44, CoinType +from iota_sdk import Block, Client, MnemonicSecretManager, Utils, SecretManager, OutputId, hex_to_utf8, utf8_to_hex, Bip44, CoinType, Irc27Metadata, Irc30Metadata import json import unittest @@ -113,3 +113,49 @@ def test_block(): "data": "0x68656c6c6f"}} block = Block.from_dict(block_dict) assert block.id() == "0x7ce5ad074d4162e57f83cfa01cd2303ef5356567027ce0bcee0c9f57bc11656e" + + +def test_irc_27(): + metadata = Irc27Metadata( + "video/mp4", + "https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT", + "Shimmer OG NFT", + description="The original Shimmer NFT" + ) + metadata_dict = { + "standard": "IRC27", + "version": metadata.version, + "type": metadata.type, + "uri": metadata.uri, + "name": metadata.name, + "collectionName": metadata.collectionName, + "royalties": metadata.royalties, + "issuerName": metadata.issuerName, + "description": metadata.description, + "attributes": metadata.attributes + } + metadata_deser = Irc27Metadata.from_dict(metadata_dict) + assert metadata == metadata_deser + + +def test_irc_30(): + metadata = Irc30Metadata( + "FooCoin", + "FOO", + 3, + description="FooCoin is the utility and governance token of FooLand, \ + a revolutionary protocol in the play-to-earn crypto gaming field.", + url="https://foocoin.io/", + logoUrl="https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR" + ) + metadata_dict = { + "standard": "IRC30", + "name": metadata.name, + "description": metadata.description, + "decimals": metadata.decimals, + "symbol": metadata.symbol, + "url": metadata.url, + "logoUrl": metadata.logoUrl + } + metadata_deser = Irc30Metadata.from_dict(metadata_dict) + assert metadata == metadata_deser diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index fc2c6b59ac..42f8e1d971 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `addresses` now additionally prints the hex version of the address; - `outputs`, `unspent-outputs` print a list that includes number and type of the output; - `Account::switch` command to allow changing accounts quickly; +- UX improvements (Ctrl+l, TAB completion/suggestions and more) during interactive account management; +- `WalletCommand::SetPow` command; ### Changed @@ -37,6 +39,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `print_account_help` changed to `AccountCli::print_help`; - `AccountCommand::Addresses` now prints an overview that includes NTs, NFTs, Aliases and Foundries; +## 1.0.1 - 2023-MM-DD + +### Fixed + +- Potential bug in the addresses command; + ## 1.0.0 - 2023-07-27 First release of the `cli-wallet`. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index fda87fd0d3..e006eb76e9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,8 +21,8 @@ iota-sdk = { path = "../sdk", default-features = false, features = [ "participation", ] } -chrono = { version = "0.4.29", default-features = false, features = ["std"] } -clap = { version = "4.4.2", default-features = false, features = [ +chrono = { version = "0.4.31", default-features = false, features = ["std"] } +clap = { version = "4.4.4", default-features = false, features = [ "std", "color", "help", @@ -33,15 +33,12 @@ clap = { version = "4.4.2", default-features = false, features = [ "env", ] } colored = { version = "2.0.4", default-features = false } -dialoguer = { version = "0.10.4", default-features = false, features = [ - "history", - "password", - "completion", -] } +dialoguer = { version = "0.10.4", default-features = false, features = ["password"] } dotenvy = { version = "0.15.7", default-features = false } fern-logger = { version = "0.5.0", default-features = false } log = { version = "0.4.20", default-features = false } prefix-hex = { version = "0.7.1", default-features = false, features = ["std"] } +rustyline = { version = "12.0.0", features = ["derive"] } serde_json = { version = "1.0.107", default-features = false } thiserror = { version = "1.0.48", default-features = false } tokio = { version = "1.32.0", default-features = false, features = ["fs"] } diff --git a/cli/src/account.rs b/cli/src/account.rs index 5dae5f790d..b01e21142a 100644 --- a/cli/src/account.rs +++ b/cli/src/account.rs @@ -3,21 +3,22 @@ use clap::Parser; use colored::Colorize; -use dialoguer::Input; use iota_sdk::wallet::{Account, Wallet}; +use rustyline::{error::ReadlineError, history::MemHistory, Config, Editor}; use crate::{ - account_completion::AccountCompletion, - account_history::AccountHistory, - command::account::{ - addresses_command, balance_command, burn_native_token_command, burn_nft_command, claim_command, - claimable_outputs_command, consolidate_command, create_account_output_command, create_native_token_command, - decrease_voting_power_command, destroy_account_command, destroy_foundry_command, faucet_command, - increase_voting_power_command, melt_native_token_command, mint_native_token, mint_nft_command, - new_address_command, node_info_command, output_command, outputs_command, participation_overview_command, - send_command, send_native_token_command, send_nft_command, stop_participating_command, sync_command, - transaction_command, transactions_command, unspent_outputs_command, vote_command, voting_output_command, - voting_power_command, AccountCli, AccountCommand, + command::{ + account::{ + addresses_command, balance_command, burn_native_token_command, burn_nft_command, claim_command, + claimable_outputs_command, consolidate_command, create_account_output_command, create_native_token_command, + decrease_voting_power_command, destroy_account_command, destroy_foundry_command, faucet_command, + increase_voting_power_command, melt_native_token_command, mint_native_token, mint_nft_command, + new_address_command, node_info_command, output_command, outputs_command, participation_overview_command, + send_command, send_native_token_command, send_nft_command, stop_participating_command, sync_command, + transaction_command, transactions_command, unspent_outputs_command, vote_command, voting_output_command, + voting_power_command, AccountCli, AccountCommand, + }, + account_completion::AccountPromptHelper, }, error::Error, helper::bytes_from_hex_or_file, @@ -26,9 +27,18 @@ use crate::{ // loop on the account prompt pub async fn account_prompt(wallet: &Wallet, mut account: Account) -> Result<(), Error> { - let mut history = AccountHistory::default(); + let config = Config::builder() + .auto_add_history(true) + .history_ignore_space(true) + .completion_type(rustyline::CompletionType::List) + .edit_mode(rustyline::EditMode::Emacs) + .build(); + + let mut rl = Editor::with_history(config, MemHistory::with_config(config))?; + rl.set_helper(Some(AccountPromptHelper::default())); + loop { - match account_prompt_internal(wallet, &account, &mut history).await { + match account_prompt_internal(wallet, &account, &mut rl).await { Ok(res) => match res { AccountPromptResponse::Reprompt => (), AccountPromptResponse::Done => { @@ -55,146 +65,170 @@ pub enum AccountPromptResponse { pub async fn account_prompt_internal( wallet: &Wallet, account: &Account, - history: &mut AccountHistory, + rl: &mut Editor, ) -> Result { - let alias = { - let account = account.details().await; - account.alias().clone() - }; - let command: String = Input::new() - .with_prompt(format!("Account \"{}\"", alias).green().to_string()) - .history_with(history) - .completion_with(&AccountCompletion) - .interact_text()?; - match command.as_str() { - "h" | "help" => AccountCli::print_help()?, - "c" | "clear" => { - // Clear console - let _ = std::process::Command::new("clear").status(); - } - "accounts" => { - // List all accounts - let accounts = wallet.get_accounts().await?; - println!("INDEX\tALIAS"); - for account in accounts { - let details = &*account.details().await; - println!("{}\t{}", details.index(), details.alias()); - } - } - _ => { - // Prepend `Account: ` so the parsing will be correct - let command = format!("Account: {}", command.trim()); - let account_cli = match AccountCli::try_parse_from(command.split_whitespace()) { - Ok(account_cli) => account_cli, - Err(err) => { - println!("{err}"); - return Ok(AccountPromptResponse::Reprompt); - } - }; - if let Err(err) = match account_cli.command { - AccountCommand::Addresses => addresses_command(account).await, - AccountCommand::Balance { addresses } => balance_command(account, addresses).await, - AccountCommand::BurnNativeToken { token_id, amount } => { - burn_native_token_command(account, token_id, amount).await - } - AccountCommand::BurnNft { nft_id } => burn_nft_command(&account, nft_id).await, - AccountCommand::Claim { output_id } => claim_command(&account, output_id).await, - AccountCommand::ClaimableOutputs => claimable_outputs_command(&account).await, - AccountCommand::Consolidate => consolidate_command(&account).await, - AccountCommand::CreateAccountOutput => create_account_output_command(&account).await, - AccountCommand::CreateNativeToken { - circulating_supply, - maximum_supply, - foundry_metadata_hex, - foundry_metadata_file, - } => { - create_native_token_command( - account, - circulating_supply, - maximum_supply, - bytes_from_hex_or_file(foundry_metadata_hex, foundry_metadata_file).await?, - ) - .await - } - AccountCommand::DestroyAccount { account_id } => destroy_account_command(&account, account_id).await, - AccountCommand::DestroyFoundry { foundry_id } => destroy_foundry_command(&account, foundry_id).await, - AccountCommand::Exit => { - return Ok(AccountPromptResponse::Done); - } - AccountCommand::Faucet { address, url } => faucet_command(account, address, url).await, - AccountCommand::MeltNativeToken { token_id, amount } => { - melt_native_token_command(account, token_id, amount).await - } - AccountCommand::MintNativeToken { token_id, amount } => { - mint_native_token(account, token_id, amount).await + let alias = account.details().await.alias().clone(); + let prompt = format!("Account \"{alias}\": "); + + if let Some(helper) = rl.helper_mut() { + helper.set_prompt(prompt.green().to_string()); + } + + let input = rl.readline(&prompt); + match input { + Ok(command) => { + match command.as_str() { + "h" | "help" => AccountCli::print_help()?, + "c" | "clear" => { + // Clear console + let _ = std::process::Command::new("clear").status(); } - AccountCommand::MintNft { - address, - immutable_metadata_hex, - immutable_metadata_file, - metadata_hex, - metadata_file, - tag, - sender, - issuer, - } => { - mint_nft_command( - account, - address, - bytes_from_hex_or_file(immutable_metadata_hex, immutable_metadata_file).await?, - bytes_from_hex_or_file(metadata_hex, metadata_file).await?, - tag, - sender, - issuer, - ) - .await + "accounts" => { + // List all accounts + let accounts = wallet.get_accounts().await?; + println!("INDEX\tALIAS"); + for account in accounts { + let details = &*account.details().await; + println!("{}\t{}", details.index(), details.alias()); + } } - AccountCommand::NewAddress => new_address_command(account).await, - AccountCommand::NodeInfo => node_info_command(account).await, - AccountCommand::Output { output_id } => output_command(account, output_id).await, - AccountCommand::Outputs => outputs_command(account).await, - AccountCommand::Send { - address, - amount, - return_address, - expiration, - allow_micro_amount, - } => { - let allow_micro_amount = if return_address.is_some() || expiration.is_some() { - true - } else { - allow_micro_amount + _ => { + // Prepend `Account: ` so the parsing will be correct + let command = format!("Account: {}", command.trim()); + let account_cli = match AccountCli::try_parse_from(command.split_whitespace()) { + Ok(account_cli) => account_cli, + Err(err) => { + println!("{err}"); + return Ok(AccountPromptResponse::Reprompt); + } }; - send_command(account, address, amount, return_address, expiration, allow_micro_amount).await + match account_cli.command { + AccountCommand::Addresses => addresses_command(&account).await, + AccountCommand::Balance { addresses } => balance_command(&account, addresses).await, + AccountCommand::BurnNativeToken { token_id, amount } => { + burn_native_token_command(&account, token_id, amount).await + } + AccountCommand::BurnNft { nft_id } => burn_nft_command(&account, nft_id).await, + AccountCommand::Claim { output_id } => claim_command(&account, output_id).await, + AccountCommand::ClaimableOutputs => claimable_outputs_command(&account).await, + AccountCommand::Consolidate => consolidate_command(&account).await, + AccountCommand::CreateAccountOutput => create_account_output_command(&account).await, + AccountCommand::CreateNativeToken { + circulating_supply, + maximum_supply, + foundry_metadata_hex, + foundry_metadata_file, + } => { + create_native_token_command( + account, + circulating_supply, + maximum_supply, + bytes_from_hex_or_file(foundry_metadata_hex, foundry_metadata_file).await?, + ) + .await + } + AccountCommand::DestroyAccount { account_id } => { + destroy_account_command(&account, account_id).await + } + AccountCommand::DestroyFoundry { foundry_id } => { + destroy_foundry_command(&account, foundry_id).await + } + AccountCommand::Exit => { + return Ok(AccountPromptResponse::Done); + } + AccountCommand::Faucet { address, url } => faucet_command(&account, address, url).await, + AccountCommand::MeltNativeToken { token_id, amount } => { + melt_native_token_command(&account, token_id, amount).await + } + AccountCommand::MintNativeToken { token_id, amount } => { + mint_native_token(&account, token_id, amount).await + } + AccountCommand::MintNft { + address, + immutable_metadata_hex, + immutable_metadata_file, + metadata_hex, + metadata_file, + tag, + sender, + issuer, + } => { + mint_nft_command( + account, + address, + bytes_from_hex_or_file(immutable_metadata_hex, immutable_metadata_file).await?, + bytes_from_hex_or_file(metadata_hex, metadata_file).await?, + tag, + sender, + issuer, + ) + .await + } + AccountCommand::NewAddress => new_address_command(&account).await, + AccountCommand::NodeInfo => node_info_command(&account).await, + AccountCommand::Output { output_id } => output_command(&account, output_id).await, + AccountCommand::Outputs => outputs_command(&account).await, + AccountCommand::Send { + address, + amount, + return_address, + expiration, + allow_micro_amount, + } => { + let allow_micro_amount = if return_address.is_some() || expiration.is_some() { + true + } else { + allow_micro_amount + }; + send_command(account, address, amount, return_address, expiration, allow_micro_amount).await + } + AccountCommand::SendNativeToken { + address, + token_id, + amount, + gift_storage_deposit, + } => send_native_token_command(&account, address, token_id, amount, gift_storage_deposit).await, + AccountCommand::SendNft { address, nft_id } => { + send_nft_command(&account, address, nft_id).await + } + AccountCommand::Switch { account_id } => { + return Ok(AccountPromptResponse::Switch(wallet.get_account(account_id).await?)); + } + AccountCommand::Sync => sync_command(&account).await, + AccountCommand::Transaction { selector } => transaction_command(&account, selector).await, + AccountCommand::Transactions { show_details } => { + transactions_command(&account, show_details).await + } + AccountCommand::UnspentOutputs => unspent_outputs_command(&account).await, + AccountCommand::Vote { event_id, answers } => vote_command(&account, event_id, answers).await, + AccountCommand::StopParticipating { event_id } => { + stop_participating_command(&account, event_id).await + } + AccountCommand::ParticipationOverview { event_ids } => { + let event_ids = (!event_ids.is_empty()).then_some(event_ids); + participation_overview_command(&account, event_ids).await + } + AccountCommand::VotingPower => voting_power_command(&account).await, + AccountCommand::IncreaseVotingPower { amount } => { + increase_voting_power_command(&account, amount).await + } + AccountCommand::DecreaseVotingPower { amount } => { + decrease_voting_power_command(&account, amount).await + } + AccountCommand::VotingOutput => voting_output_command(&account).await, + } + .unwrap_or_else(|err| { + println_log_error!("{err}"); + }); } - AccountCommand::SendNativeToken { - address, - token_id, - amount, - gift_storage_deposit, - } => send_native_token_command(account, address, token_id, amount, gift_storage_deposit).await, - AccountCommand::SendNft { address, nft_id } => send_nft_command(account, address, nft_id).await, - AccountCommand::Switch { account_id } => { - return Ok(AccountPromptResponse::Switch(wallet.get_account(account_id).await?)); - } - AccountCommand::Sync => sync_command(account).await, - AccountCommand::Transaction { selector } => transaction_command(account, selector).await, - AccountCommand::Transactions { show_details } => transactions_command(account, show_details).await, - AccountCommand::UnspentOutputs => unspent_outputs_command(account).await, - AccountCommand::Vote { event_id, answers } => vote_command(account, event_id, answers).await, - AccountCommand::StopParticipating { event_id } => stop_participating_command(account, event_id).await, - AccountCommand::ParticipationOverview { event_ids } => { - let event_ids = (!event_ids.is_empty()).then_some(event_ids); - participation_overview_command(account, event_ids).await - } - AccountCommand::VotingPower => voting_power_command(account).await, - AccountCommand::IncreaseVotingPower { amount } => increase_voting_power_command(account, amount).await, - AccountCommand::DecreaseVotingPower { amount } => decrease_voting_power_command(account, amount).await, - AccountCommand::VotingOutput => voting_output_command(account).await, - } { - println_log_error!("{err}"); } } + Err(ReadlineError::Interrupted) => { + return Ok(AccountPromptResponse::Done); + } + Err(err) => { + println_log_error!("{err}"); + } } Ok(AccountPromptResponse::Reprompt) diff --git a/cli/src/account_completion.rs b/cli/src/account_completion.rs deleted file mode 100644 index 3eb6e14dd4..0000000000 --- a/cli/src/account_completion.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use dialoguer::Completion; - -pub(crate) struct AccountCompletion; - -pub(crate) const ACCOUNT_COMPLETION: &[&str] = &[ - "accounts", - "addresses", - "balance", - "burn-native-token", - "burn-nft", - "claim", - "claimable-outputs", - "consolidate", - "create-account-output", - "create-native-token", - "destroy-account", - "destroy-foundry", - "exit", - "faucet", - "melt-native-token", - "mint-native-token", - "mint-nft", - "new-address", - "node-info", - "output", - "outputs", - "send", - "send-native-token", - "send-nft", - "switch", - "sync", - "transaction", - "transactions", - "tx", - "txs", - "unspent-outputs", - "vote", - "stop-participating", - "participation-overview", - "voting-power", - "increase-voting-power", - "decrease-voting-power", - "voting-output", - "help", -]; - -impl Completion for AccountCompletion { - fn get(&self, input: &str) -> Option { - let matches = ACCOUNT_COMPLETION - .iter() - .filter(|option| option.starts_with(input)) - .collect::>(); - - if matches.len() == 1 { - Some(matches[0].to_string()) - } else { - None - } - } -} diff --git a/cli/src/account_history.rs b/cli/src/account_history.rs deleted file mode 100644 index 37afabedaa..0000000000 --- a/cli/src/account_history.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::VecDeque; - -use dialoguer::History; - -pub struct AccountHistory { - max: usize, - history: VecDeque, -} - -impl Default for AccountHistory { - fn default() -> Self { - AccountHistory { - max: 25, - history: VecDeque::new(), - } - } -} - -impl History for AccountHistory { - fn read(&self, pos: usize) -> Option { - self.history.get(pos).cloned() - } - - fn write(&mut self, val: &T) { - let entry = val.to_string(); - - // If the last used command is the same, don't change anything - if matches!(self.history.front(), Some(command) if command == &entry) { - return; - } - - // Check if we have used this command before - match self.history.iter().position(|e| e == &entry) { - Some(index) => { - // Remove the old command - self.history.remove(index); - } - None => { - // We have not used this command - if self.history.len() == self.max { - // Remove the oldest used command - self.history.pop_back(); - } - } - } - - // Add command as most recent used - self.history.push_front(entry); - } -} diff --git a/cli/src/command/account.rs b/cli/src/command/account.rs index d9503ea979..5ea180908c 100644 --- a/cli/src/command/account.rs +++ b/cli/src/command/account.rs @@ -941,10 +941,11 @@ async fn print_address(account: &Account, address: &Bip44Address) -> Result<(), let mut accounts = Vec::new(); let mut foundries = Vec::new(); - if let Ok(index) = addresses.binary_search_by_key(&(address.key_index(), address.internal()), |a| { - (a.key_index(), a.internal()) - }) { - output_ids = addresses[index].output_ids().as_slice(); + if let Some(address) = addresses + .iter() + .find(|a| a.key_index() == address.key_index() && a.internal() == address.internal()) + { + output_ids = address.output_ids().as_slice(); for output_id in output_ids { if let Some(output_data) = account.get_output(output_id).await { diff --git a/cli/src/command/account_completion.rs b/cli/src/command/account_completion.rs new file mode 100644 index 0000000000..53a78c1261 --- /dev/null +++ b/cli/src/command/account_completion.rs @@ -0,0 +1,113 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Cow; + +use colored::Colorize; +use rustyline::{ + completion::Completer, highlight::Highlighter, hint::HistoryHinter, Completer, Context, Helper, Hinter, Validator, +}; + +#[derive(Default)] +pub struct AccountCompleter; + +const ACCOUNT_COMMANDS: &[&str] = &[ + "accounts", + "addresses", + "balance", + "burn-native-token", + "burn-nft", + "claim", + "claimable-outputs", + "clear", + "consolidate", + "create-alias-output", + "create-native-token", + "destroy-alias", + "destroy-foundry", + "exit", + "faucet", + "melt-native-token", + "mint-native-token", + "mint-nft", + "new-address", + "node-info", + "output", + "outputs", + "send", + "send-native-token", + "send-nft", + "switch", + "sync", + "transaction", + "transactions", + "tx", + "txs", + "unspent-outputs", + "vote", + "stop-participating", + "participation-overview", + "voting-power", + "increase-voting-power", + "decrease-voting-power", + "voting-output", + "help", +]; + +impl Completer for AccountCompleter { + type Candidate = &'static str; + + fn complete( + &self, + input: &str, + _pos: usize, + _ctx: &Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + Ok(( + 0, + ACCOUNT_COMMANDS + .iter() + .filter_map(|cmd| cmd.starts_with(input).then_some(*cmd)) + .collect(), + )) + } +} + +#[derive(Helper, Completer, Hinter, Validator)] +pub struct AccountPromptHelper { + #[rustyline(Completer)] + completer: AccountCompleter, + #[rustyline(Hinter)] + hinter: HistoryHinter, + prompt: String, +} + +impl AccountPromptHelper { + pub fn set_prompt(&mut self, prompt: String) { + self.prompt = prompt; + } +} + +impl Highlighter for AccountPromptHelper { + fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str, default: bool) -> Cow<'b, str> { + if default { + Cow::Borrowed(&self.prompt) + } else { + Cow::Borrowed(prompt) + } + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Cow::Owned(hint.bold().to_string()) + } +} + +impl Default for AccountPromptHelper { + fn default() -> Self { + Self { + completer: AccountCompleter, + hinter: HistoryHinter {}, + prompt: String::new(), + } + } +} diff --git a/cli/src/command/mod.rs b/cli/src/command/mod.rs index b67764ee45..503b26a487 100644 --- a/cli/src/command/mod.rs +++ b/cli/src/command/mod.rs @@ -2,4 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 pub mod account; +pub mod account_completion; pub mod wallet; diff --git a/cli/src/error.rs b/cli/src/error.rs index f328062d54..bea7e656b2 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -5,6 +5,7 @@ use fern_logger::Error as LoggerError; use iota_sdk::{ client::error::Error as ClientError, types::block::Error as BlockError, wallet::error::Error as WalletError, }; +use rustyline::error::ReadlineError; use serde_json::Error as SerdeJsonError; #[derive(Debug, thiserror::Error)] @@ -21,6 +22,8 @@ pub enum Error { Miscellaneous(String), #[error("generate at least one address before using the faucet")] NoAddressForFaucet, + #[error("prompt error: {0}")] + Prompt(#[from] ReadlineError), #[error("serde_json error: {0}")] SerdeJson(#[from] SerdeJsonError), #[error("wallet error: {0}")] diff --git a/cli/src/main.rs b/cli/src/main.rs index a3f417f228..ea0336e05b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 mod account; -mod account_completion; -mod account_history; mod command; mod error; mod helper; diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 2dbc64c595..26d9189eb3 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConflictReason` display implementation with an explanation of the conflict; - `TokenScheme` methods `is_simple` and `as_simple`; - `Irc27Metadata` and `Irc30Metadata` helpers; +- `Client::output_ids()` method; +- `QueryParameter::UnlockableByAddress` variant; ### Changed diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index e846f321de..eda2106ca9 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -42,7 +42,7 @@ iota-crypto = { version = "0.23.0", default-features = false, features = [ "secp256k1", ] } iterator-sorted = { version = "0.1.0", default-features = false } -packable = { version = "0.8.2", default-features = false, features = [ +packable = { version = "0.8.3", default-features = false, features = [ "primitive-types", ] } prefix-hex = { version = "0.7.1", default-features = false, features = [ @@ -137,7 +137,6 @@ tokio = { version = "1.32.0", default-features = false, features = [ [features] default = ["client", "wallet", "tls"] - irc_27 = ["serde", "dep:url"] irc_30 = ["serde", "dep:url"] ledger_nano = ["dep:iota-ledger-nano"] diff --git a/sdk/src/client/core.rs b/sdk/src/client/core.rs index fba62c7096..e2ca71c2f4 100644 --- a/sdk/src/client/core.rs +++ b/sdk/src/client/core.rs @@ -177,4 +177,10 @@ impl ClientInner { }; Ok(()) } + + /// Resize the client's request pool + #[cfg(not(target_family = "wasm"))] + pub async fn resize_request_pool(&self, new_size: usize) { + self.request_pool.resize(new_size).await; + } } diff --git a/sdk/src/client/node_api/indexer/query_parameters.rs b/sdk/src/client/node_api/indexer/query_parameters.rs index ae49d93ac0..d9e1d54333 100644 --- a/sdk/src/client/node_api/indexer/query_parameters.rs +++ b/sdk/src/client/node_api/indexer/query_parameters.rs @@ -126,6 +126,8 @@ pub enum QueryParameter { TimelockedAfter(SlotIndex), /// Returns outputs that are timelocked before a certain slot index. TimelockedBefore(SlotIndex), + /// Returns outputs that are unlockable by the bech32 address. + UnlockableByAddress(Bech32Address), } impl QueryParameter { @@ -154,6 +156,7 @@ impl QueryParameter { Self::Tag(v) => format!("tag={v}"), Self::TimelockedAfter(v) => format!("timelockedAfter={v}"), Self::TimelockedBefore(v) => format!("timelockedBefore={v}"), + Self::UnlockableByAddress(v) => format!("unlockableByAddress={v}"), } } @@ -182,6 +185,7 @@ impl QueryParameter { Self::Tag(_) => 20, Self::TimelockedAfter(_) => 21, Self::TimelockedBefore(_) => 22, + Self::UnlockableByAddress(_) => 23, } } } @@ -204,6 +208,22 @@ macro_rules! verify_query_parameters { }; } +pub(crate) fn verify_query_parameters_outputs(query_parameters: Vec) -> Result { + verify_query_parameters!( + query_parameters, + QueryParameter::HasNativeTokens, + QueryParameter::MinNativeTokenCount, + QueryParameter::MaxNativeTokenCount, + QueryParameter::CreatedBefore, + QueryParameter::CreatedAfter, + QueryParameter::PageSize, + QueryParameter::Cursor, + QueryParameter::UnlockableByAddress + )?; + + Ok(QueryParameters::new(query_parameters)) +} + pub(crate) fn verify_query_parameters_basic_outputs(query_parameters: Vec) -> Result { verify_query_parameters!( query_parameters, diff --git a/sdk/src/client/node_api/indexer/routes.rs b/sdk/src/client/node_api/indexer/routes.rs index d3ad57e0e7..7da81319aa 100644 --- a/sdk/src/client/node_api/indexer/routes.rs +++ b/sdk/src/client/node_api/indexer/routes.rs @@ -8,7 +8,8 @@ use crate::{ node_api::indexer::{ query_parameters::{ verify_query_parameters_account_outputs, verify_query_parameters_basic_outputs, - verify_query_parameters_foundry_outputs, verify_query_parameters_nft_outputs, QueryParameter, + verify_query_parameters_foundry_outputs, verify_query_parameters_nft_outputs, + verify_query_parameters_outputs, QueryParameter, }, QueryParameters, }, @@ -23,6 +24,23 @@ use crate::{ // hornet: https://github.com/gohornet/hornet/blob/develop/plugins/indexer/routes.go impl ClientInner { + /// Get basic, alias, nft and foundry outputs filtered by the given parameters. + /// GET with query parameter returns all outputIDs that fit these filter criteria. + /// Query parameters: "hasNativeTokens", "minNativeTokenCount", "maxNativeTokenCount", "unlockableByAddress", + /// "createdBefore", "createdAfter", "cursor", "pageSize". + /// Returns Err(Node(NotFound) if no results are found. + /// api/indexer/v1/outputs + pub async fn output_ids( + &self, + query_parameters: impl Into> + Send, + ) -> Result { + let route = "api/indexer/v1/outputs"; + + let query_parameters = verify_query_parameters_outputs(query_parameters.into())?; + + self.get_output_ids(route, query_parameters, true, false).await + } + /// Get basic outputs filtered by the given parameters. /// GET with query parameter returns all outputIDs that fit these filter criteria. /// Query parameters: "address", "hasStorageDepositReturn", "storageDepositReturnAddress", diff --git a/sdk/src/types/fuzz/Cargo.toml b/sdk/src/types/fuzz/Cargo.toml index 43f256a55c..44190bc39e 100644 --- a/sdk/src/types/fuzz/Cargo.toml +++ b/sdk/src/types/fuzz/Cargo.toml @@ -12,7 +12,7 @@ cargo-fuzz = true iota-types = { path = "..", default-features = false } libfuzzer-sys = { version = "0.4.7", default-features = false } -packable = { version = "0.8.2", default-features = false } +packable = { version = "0.8.3", default-features = false } # Prevent this from interfering with workspaces [workspace] diff --git a/sdk/src/wallet/account/operations/output_consolidation.rs b/sdk/src/wallet/account/operations/output_consolidation.rs index 25bec14c21..d27feae1ce 100644 --- a/sdk/src/wallet/account/operations/output_consolidation.rs +++ b/sdk/src/wallet/account/operations/output_consolidation.rs @@ -150,6 +150,7 @@ where drop(account_details); + #[allow(clippy::option_if_let_else)] let output_threshold = match params.output_threshold { Some(t) => t, None => { diff --git a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs b/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs index f4d7d2db8e..ae10e9aa92 100644 --- a/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs +++ b/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs @@ -12,7 +12,7 @@ use futures::FutureExt; use instant::Instant; use crate::{ - client::secret::SecretManage, + client::{node_api::indexer::QueryParameter, secret::SecretManage}, types::block::{ address::{Address, Bech32Address}, output::OutputId, @@ -44,6 +44,18 @@ where return Ok(output_ids); } + // If interested in alias, basic, NFT and foundry outputs, get them all at once + if (address.is_ed25519() && sync_options.account.all_outputs()) + || (address.is_nft() && sync_options.nft.all_outputs()) + || (address.is_account() && sync_options.alias.all_outputs()) + { + return Ok(self + .client() + .output_ids([QueryParameter::UnlockableByAddress(bech32_address)]) + .await? + .items); + } + #[cfg(target_family = "wasm")] let mut results = Vec::new(); @@ -136,6 +148,33 @@ where .boxed(), ); } + } else if address.is_account() && sync_options.alias.foundry_outputs { + // foundries + #[cfg(target_family = "wasm")] + { + results.push(Ok(self + .client() + .foundry_output_ids([QueryParameter::AccountAddress(bech32_address)]) + .await? + .items)) + } + + #[cfg(not(target_family = "wasm"))] + { + tasks.push( + async move { + let client = self.client().clone(); + tokio::spawn(async move { + Ok(client + .foundry_output_ids([QueryParameter::AccountAddress(bech32_address)]) + .await? + .items) + }) + .await + } + .boxed(), + ); + } } #[cfg(not(target_family = "wasm"))] diff --git a/sdk/src/wallet/account/operations/syncing/options.rs b/sdk/src/wallet/account/operations/syncing/options.rs index 1dfe7057b3..ae52169ee2 100644 --- a/sdk/src/wallet/account/operations/syncing/options.rs +++ b/sdk/src/wallet/account/operations/syncing/options.rs @@ -120,6 +120,12 @@ impl Default for AccountSyncOptions { } } +impl AccountSyncOptions { + pub(crate) fn all_outputs(&self) -> bool { + self.basic_outputs && self.nft_outputs && self.account_outputs + } +} + /// Sync options for addresses from account outputs #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -142,6 +148,12 @@ impl Default for AliasSyncOptions { } } +impl AliasSyncOptions { + pub(crate) fn all_outputs(&self) -> bool { + self.basic_outputs && self.nft_outputs && self.account_outputs && self.foundry_outputs + } +} + /// Sync options for addresses from NFT outputs #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -150,3 +162,9 @@ pub struct NftSyncOptions { pub nft_outputs: bool, pub account_outputs: bool, } + +impl NftSyncOptions { + pub(crate) fn all_outputs(&self) -> bool { + self.basic_outputs && self.nft_outputs && self.account_outputs + } +} diff --git a/sdk/src/wallet/core/operations/storage.rs b/sdk/src/wallet/core/operations/storage.rs index 9eb6db79a7..2109618030 100644 --- a/sdk/src/wallet/core/operations/storage.rs +++ b/sdk/src/wallet/core/operations/storage.rs @@ -3,7 +3,6 @@ #[cfg(feature = "storage")] mod storage_stub { - use async_trait::async_trait; use crate::{ diff --git a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs index 1ba02a9500..5744e8f6c9 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs @@ -9,7 +9,7 @@ use crate::{ wallet::{ account::{AccountDetails, AccountDetailsDto}, migration::{latest_backup_migration_version, migrate, MIGRATION_VERSION_KEY}, - ClientOptions, Wallet, + ClientOptions, Error as WalletError, Wallet, }, }; @@ -67,7 +67,7 @@ pub(crate) async fn read_data_from_stronghold_snapshot