From 7aa1e7afbff4e468681ca0a805d72e80e9774d3f Mon Sep 17 00:00:00 2001 From: Daniel Melero Martinez Date: Mon, 18 Sep 2023 20:50:17 +0200 Subject: [PATCH 01/11] Fix: nodejs broken install command for windows (#1251) --- bindings/nodejs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" }, From ba38c55f0455b102a5ec32739568819b29a91995 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:12:20 +0200 Subject: [PATCH 02/11] Fix wheel path (#1258) --- .github/workflows/bindings-python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 64413fa8a90d9271d99498b7bdc2126b983bc0db Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:51:37 +0200 Subject: [PATCH 03/11] Fix comment and bump version, add dependencies (#1259) --- Cargo.lock | 2 +- bindings/python/CHANGELOG.md | 6 ++++++ bindings/python/Cargo.toml | 2 +- bindings/python/iota_sdk/types/send_params.py | 4 +++- bindings/python/pyproject.toml | 1 + bindings/python/setup.py | 2 +- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2d368b24f..fefaa67e95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1716,7 +1716,7 @@ dependencies = [ [[package]] name = "iota-sdk-python" -version = "1.0.2" +version = "1.0.3" dependencies = [ "futures", "iota-sdk-bindings-core", diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 1ada400f33..5dfeea1196 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 1.0.3 - 2023-09-19 + +### Fixed + +- Wheel upload; + ## 1.0.2 - 2023-09-12 ### Added 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/iota_sdk/types/send_params.py b/bindings/python/iota_sdk/types/send_params.py index df1fe5bc1a..d2af0f603e 100644 --- a/bindings/python/iota_sdk/types/send_params.py +++ b/bindings/python/iota_sdk/types/send_params.py @@ -16,7 +16,9 @@ class SendParams(): address: The address to send to. amount: The amount to send. returnAddress: The address to return the funds to if not claimed. - expiration: The expiration timestamp until funds can be 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/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", From 9975a40e3814304513fb1c7bf2f8e13fd3ce065d Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:27:14 +0200 Subject: [PATCH 04/11] Add indexer /outputs route (#1246) * Add indexer /outputs route * Merge imports * Add new GenericQueryParameter type * no-else-return * Add comment * Comment like this? * Add and use all_outputs() * Fix comment * Add cursor and pageSize to comment * Foundries included now * Update bindings/nodejs/lib/types/client/query-parameters.ts Co-authored-by: Thibault Martinez * Update sdk/src/client/node_api/indexer/routes.rs Co-authored-by: Thibault Martinez * Request foundry outputs extra if not not synced directly * Update sdk/src/client/node_api/indexer/routes.rs * Doc comment * Update comment --------- Co-authored-by: Thibault Martinez --- bindings/core/src/method/client.rs | 6 +++ bindings/core/src/method_handler/client.rs | 3 ++ bindings/core/src/response.rs | 1 + bindings/nodejs/CHANGELOG.md | 2 + bindings/nodejs/lib/client/client.ts | 17 ++++++++ .../nodejs/lib/types/client/bridge/client.ts | 8 ++++ .../nodejs/lib/types/client/bridge/index.ts | 2 + .../lib/types/client/query-parameters.ts | 17 +++++++- bindings/python/CHANGELOG.md | 2 + .../how_tos/alias/governance_transition.py | 6 ++- .../iota_sdk/client/_node_indexer_api.py | 19 +++++++++ sdk/CHANGELOG.md | 2 + .../node_api/indexer/query_parameters.rs | 20 +++++++++ sdk/src/client/node_api/indexer/routes.rs | 20 ++++++++- .../syncing/addresses/output_ids/mod.rs | 41 ++++++++++++++++++- .../account/operations/syncing/options.rs | 18 ++++++++ 16 files changed, 179 insertions(+), 5 deletions(-) diff --git a/bindings/core/src/method/client.rs b/bindings/core/src/method/client.rs index 5f64613de2..c095ffe9da 100644 --- a/bindings/core/src/method/client.rs +++ b/bindings/core/src/method/client.rs @@ -281,6 +281,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 be1aa19f4d..f5c1e3a4a8 100644 --- a/bindings/core/src/method_handler/client.rs +++ b/bindings/core/src/method_handler/client.rs @@ -325,6 +325,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 1fc759662a..583d7e6bc9 100644 --- a/bindings/core/src/response.rs +++ b/bindings/core/src/response.rs @@ -168,6 +168,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 9096fa9d41..bc6f7610ed 100644 --- a/bindings/nodejs/lib/client/client.ts +++ b/bindings/nodejs/lib/client/client.ts @@ -18,6 +18,7 @@ import { FoundryQueryParameter, NftQueryParameter, AliasQueryParameter, + GenericQueryParameter, } from '../types/client'; import type { INodeInfoWrapper } from '../types/client/nodeInfo'; import { @@ -94,6 +95,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 8e186a3f53..82f80393c6 100644 --- a/bindings/nodejs/lib/types/client/bridge/client.ts +++ b/bindings/nodejs/lib/types/client/bridge/client.ts @@ -12,6 +12,7 @@ import type { PreparedTransactionData } from '../prepared-transaction-data'; import type { AliasQueryParameter, FoundryQueryParameter, + GenericQueryParameter, NftQueryParameter, QueryParameter, } from '../query-parameters'; @@ -33,6 +34,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 215076871e..e09d70f71b 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__, @@ -72,6 +73,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 9cba7d8f79..7519d5ec2d 100644 --- a/bindings/nodejs/lib/types/client/query-parameters.ts +++ b/bindings/nodejs/lib/types/client/query-parameters.ts @@ -62,6 +62,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; @@ -155,7 +166,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/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 21acfb10f0..3572a4a646 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -26,6 +26,8 @@ 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; +- `Client::output_ids()` method; +- `QueryParameter::unlockable_by_address` field; ## 1.0.2 - 2023-09-12 diff --git a/bindings/python/examples/how_tos/alias/governance_transition.py b/bindings/python/examples/how_tos/alias/governance_transition.py index 252df985e8..a2596400d4 100644 --- a/bindings/python/examples/how_tos/alias/governance_transition.py +++ b/bindings/python/examples/how_tos/alias/governance_transition.py @@ -44,10 +44,12 @@ def update_state_controller(unlock_condition): + """ + Replace the address in the StateControllerAddressUnlockCondition + """ if unlock_condition.type == UnlockConditionType.StateControllerAddress: return StateControllerAddressUnlockCondition(new_state_controller) - else: - return unlock_condition + return unlock_condition updated_unlock_conditions = list(map( diff --git a/bindings/python/iota_sdk/client/_node_indexer_api.py b/bindings/python/iota_sdk/client/_node_indexer_api.py index 4baf4c2043..a65a22b194 100644 --- a/bindings/python/iota_sdk/client/_node_indexer_api.py +++ b/bindings/python/iota_sdk/client/_node_indexer_api.py @@ -66,6 +66,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 alias_address: Optional[str] = None @@ -90,6 +92,7 @@ class QueryParameters: tag: Optional[str] = None timelocked_after: Optional[int] = None timelocked_before: Optional[int] = None + unlockable_by_address: Optional[str] = None def as_dict(self): return humps.camelize( @@ -110,6 +113,22 @@ def __init__(self, dict: Dict): self.items = [OutputId.from_string( output_id) for output_id in 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/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/src/client/node_api/indexer/query_parameters.rs b/sdk/src/client/node_api/indexer/query_parameters.rs index 484f18509a..5849089e67 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(u32), /// Returns outputs that are timelocked before a certain Unix timestamp. TimelockedBefore(u32), + /// 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 81090976aa..35a825283c 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_alias_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/wallet/account/operations/syncing/addresses/output_ids/mod.rs b/sdk/src/wallet/account/operations/syncing/addresses/output_ids/mod.rs index db16f77e0d..6cbec6d5b5 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_alias() && 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_alias() && sync_options.alias.foundry_outputs { + // foundries + #[cfg(target_family = "wasm")] + { + results.push(Ok(self + .client() + .foundry_output_ids([QueryParameter::AliasAddress(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::AliasAddress(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 ec9e66900c..4c6c2ee37c 100644 --- a/sdk/src/wallet/account/operations/syncing/options.rs +++ b/sdk/src/wallet/account/operations/syncing/options.rs @@ -119,6 +119,12 @@ impl Default for AccountSyncOptions { } } +impl AccountSyncOptions { + pub(crate) fn all_outputs(&self) -> bool { + self.basic_outputs && self.nft_outputs && self.alias_outputs + } +} + /// Sync options for addresses from alias outputs #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -141,6 +147,12 @@ impl Default for AliasSyncOptions { } } +impl AliasSyncOptions { + pub(crate) fn all_outputs(&self) -> bool { + self.basic_outputs && self.nft_outputs && self.alias_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")] @@ -149,3 +161,9 @@ pub struct NftSyncOptions { pub nft_outputs: bool, pub alias_outputs: bool, } + +impl NftSyncOptions { + pub(crate) fn all_outputs(&self) -> bool { + self.basic_outputs && self.nft_outputs && self.alias_outputs + } +} From b9dd98477169e5343d01d3c12a37122eaea6621f Mon Sep 17 00:00:00 2001 From: /alex/ Date: Wed, 20 Sep 2023 09:49:06 +0200 Subject: [PATCH 05/11] feat(cli): `rustyline` instead of `dialoguer` (#1238) * rustyline dep * finish impl * rm cutom history impl * changelog * review 1 * review 2 * review 3 * review 4 * review 5 * &'static str --- Cargo.lock | 127 +++++++++- cli/CHANGELOG.md | 1 + cli/Cargo.toml | 7 +- cli/src/account.rs | 340 ++++++++++++++------------ cli/src/account_completion.rs | 63 ----- cli/src/account_history.rs | 53 ---- cli/src/command/account_completion.rs | 113 +++++++++ cli/src/command/mod.rs | 1 + cli/src/error.rs | 3 + cli/src/main.rs | 2 - 10 files changed, 431 insertions(+), 279 deletions(-) delete mode 100644 cli/src/account_completion.rs delete mode 100644 cli/src/account_history.rs create mode 100644 cli/src/command/account_completion.rs diff --git a/Cargo.lock b/Cargo.lock index 8fd9299b1b..da2d7bc738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,12 +519,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" @@ -922,6 +934,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" @@ -969,6 +987,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" @@ -1337,6 +1376,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" @@ -2025,6 +2073,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" @@ -2037,6 +2094,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" @@ -2463,6 +2531,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" @@ -2757,6 +2835,41 @@ dependencies = [ "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" @@ -3072,6 +3185,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" @@ -3094,7 +3213,7 @@ dependencies = [ "libc", "libsodium-sys", "log", - "nix", + "nix 0.24.3", "rand", "serde", "thiserror", @@ -3442,6 +3561,12 @@ 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" diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index fc2c6b59ac..db9b54a62e 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -27,6 +27,7 @@ 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; ### Changed diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f93dccd3bc..c7ef5e6de4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -33,16 +33,13 @@ 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 } humantime = { version = "2.1.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 aec3cc338e..83f9d32644 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_alias_outputs_command, create_native_token_command, - decrease_voting_power_command, destroy_alias_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_alias_outputs_command, create_native_token_command, + decrease_voting_power_command, destroy_alias_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,154 +65,174 @@ 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::CreateAliasOutput => create_alias_outputs_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::DestroyAlias { alias_id } => destroy_alias_command(account, alias_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.map(|e| e.as_secs() as u32), - 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::CreateAliasOutput => create_alias_outputs_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::DestroyAlias { alias_id } => destroy_alias_command(account, alias_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.map(|e| e.as_secs() as u32), + 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 98a000aae1..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-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 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_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; From c396be929554782756a39dffaa114d23cf85ef2d Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Wed, 20 Sep 2023 16:20:04 +0200 Subject: [PATCH 06/11] Fix potential problem with binary_search_by_key() (#1274) * Fix potential problem with binary_search_by_key() * Changelog entry --- cli/CHANGELOG.md | 6 ++++++ cli/src/command/account.rs | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index bba6480056..593c427f13 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security --> +## 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/src/command/account.rs b/cli/src/command/account.rs index eea411c7ae..3dac765145 100644 --- a/cli/src/command/account.rs +++ b/cli/src/command/account.rs @@ -878,11 +878,12 @@ async fn print_address(account: &Account, address: &AccountAddress) -> Result<() let addresses = account.addresses_with_unspent_outputs().await?; let current_time = iota_sdk::utils::unix_timestamp_now().as_secs() as u32; - if let Ok(index) = addresses.binary_search_by_key(&(address.key_index(), address.internal()), |a| { - (a.key_index(), a.internal()) - }) { + if let Some(address) = addresses + .iter() + .find(|a| a.key_index() == address.key_index() && a.internal() == address.internal()) + { let mut address_amount = 0; - for output_id in addresses[index].output_ids() { + for output_id in address.output_ids() { if let Some(output_data) = account.get_output(output_id).await { // Output might be associated with the address, but can't unlocked by it, so we check that here let (required_address, _) = @@ -905,7 +906,7 @@ async fn print_address(account: &Account, address: &AccountAddress) -> Result<() } log = format!( "{log}\nOutputs: {:#?}\nBase coin amount: {}\n", - addresses[index].output_ids(), + address.output_ids(), address_amount ); } else { From 610dc50583d69e7b672d02185b6cfd7de4edf108 Mon Sep 17 00:00:00 2001 From: DaughterOfMars Date: Wed, 20 Sep 2023 14:06:00 -0400 Subject: [PATCH 07/11] Python - Add `Irc27Metadata` and `Irc30Metadata` (#1260) * Add irc 27 * imports * add standard * defaults * Add irc 30 * cleanup * camelCase * more cleanup * changelog * lints * doc * type annotations * from_dict * default dict * default factory list too * Add as_feature * and to irc 30 * unused import --- bindings/python/CHANGELOG.md | 1 + .../python/examples/client/10_mint_nft.py | 11 +++- bindings/python/examples/client/build_nft.py | 32 +++++---- .../examples/how_tos/native_tokens/create.py | 8 ++- .../nft_collection/01_mint_collection_nft.py | 49 +++++++------- bindings/python/iota_sdk/__init__.py | 2 + bindings/python/iota_sdk/types/irc_27.py | 66 +++++++++++++++++++ bindings/python/iota_sdk/types/irc_30.py | 43 ++++++++++++ bindings/python/tests/test_offline.py | 48 +++++++++++++- 9 files changed, 217 insertions(+), 43 deletions(-) create mode 100644 bindings/python/iota_sdk/types/irc_27.py create mode 100644 bindings/python/iota_sdk/types/irc_30.py diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 3572a4a646..ba6b887760 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -26,6 +26,7 @@ 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; diff --git a/bindings/python/examples/client/10_mint_nft.py b/bindings/python/examples/client/10_mint_nft.py index c571c46584..a38e63aae2 100644 --- a/bindings/python/examples/client/10_mint_nft.py +++ b/bindings/python/examples/client/10_mint_nft.py @@ -4,7 +4,7 @@ from iota_sdk import (AddressUnlockCondition, Client, Ed25519Address, MetadataFeature, MnemonicSecretManager, Utils, - utf8_to_hex) + utf8_to_hex, Irc27Metadata) load_dotenv() @@ -18,6 +18,13 @@ secret_manager = MnemonicSecretManager(os.environ['MNEMONIC']) +metadata = Irc27Metadata( + "video/mp4", + "https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT", + "Shimmer OG NFT", + description="The original Shimmer NFT" +) + nft_output = client.build_nft_output( unlock_conditions=[ AddressUnlockCondition( @@ -28,7 +35,7 @@ nft_id='0x0000000000000000000000000000000000000000000000000000000000000000', amount=1000000, immutable_features=[ - MetadataFeature(utf8_to_hex('Hello, World!')) + metadata.as_feature() ], features=[ MetadataFeature(utf8_to_hex('Hello, World!')) diff --git a/bindings/python/examples/client/build_nft.py b/bindings/python/examples/client/build_nft.py index e0f3cb631e..bf74c2c1db 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/native_tokens/create.py b/bindings/python/examples/how_tos/native_tokens/create.py index d04c55d07d..964057a9b9 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 79e22b7008..4160daf2d1 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( - immutableMetadata=utf8_to_hex( - get_immutable_metadata(index, issuer_nft_id)), - issuer=issuer -), range(NFT_COLLECTION_SIZE))) +nft_mint_params = list( + map( + lambda index: MintNftParams( + immutableMetadata=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.retry_transaction_until_included( transaction.transactionId) - 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 8371099ee9..a443d691d7 100644 --- a/bindings/python/iota_sdk/__init__.py +++ b/bindings/python/iota_sdk/__init__.py @@ -19,6 +19,8 @@ from .types.common 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/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/tests/test_offline.py b/bindings/python/tests/test_offline.py index 9cb35d1ac7..0e7b2601ab 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 @@ -102,3 +102,49 @@ def test_block(): "0xd76cdb7acf228ecdad590a42b91acc077c1518c1a271411229e33e050fc19b44", "0xecef38d3af7e63da78a5e70128efe371f2191088b31879f7b0e81da657fa21c6"], "payload": {"type": 5, "tag": "0x68656c6c6f", "data": "0x68656c6c6f"}, "nonce": "6917529027641139843"} 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 From 121a22c8305eda580d09cb9bda92ca894a229371 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:53:59 +0200 Subject: [PATCH 08/11] Add WalletCommand::SetPow command (#1280) --- cli/CHANGELOG.md | 1 + cli/src/command/wallet.rs | 28 +++++++++++++++++++ cli/src/wallet.rs | 10 ++++++- sdk/src/client/node_api/core/routes.rs | 1 + .../operations/output_consolidation.rs | 1 + 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index db9b54a62e..8c42e0a2ba 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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 diff --git a/cli/src/command/wallet.rs b/cli/src/command/wallet.rs index 06e33286fc..605baee51e 100644 --- a/cli/src/command/wallet.rs +++ b/cli/src/command/wallet.rs @@ -95,6 +95,15 @@ pub enum WalletCommand { /// Node URL to use for all future operations. url: String, }, + /// Set the PoW options. + SetPow { + /// Whether the PoW should be done locally or remotely. + #[arg(short, long, action = clap::ArgAction::Set)] + local_pow: bool, + /// The amount of workers that should be used for PoW, default is num_cpus::get(). + #[arg(short, long)] + worker_count: Option, + }, /// Synchronize all accounts. Sync, } @@ -272,6 +281,25 @@ pub async fn set_node_url_command(storage_path: &Path, snapshot_path: &Path, url Ok(wallet) } +pub async fn set_pow_command( + storage_path: &Path, + snapshot_path: &Path, + local_pow: bool, + worker_count: Option, +) -> Result { + let password = get_password("Stronghold password", !snapshot_path.exists())?; + let wallet = unlock_wallet(storage_path, snapshot_path, password).await?; + // Need to get the current node, so it's not removed + let node = wallet.client().get_node().await?; + let client_options = ClientOptions::new() + .with_node(node.url.as_ref())? + .with_local_pow(local_pow) + .with_pow_worker_count(worker_count); + wallet.set_client_options(client_options).await?; + + Ok(wallet) +} + pub async fn sync_command(storage_path: &Path, snapshot_path: &Path) -> Result { let password = get_password("Stronghold password", !snapshot_path.exists())?; let wallet = unlock_wallet(storage_path, snapshot_path, password).await?; diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index e8ce5bdf4f..b098c9d675 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -9,7 +9,8 @@ use crate::{ command::wallet::{ accounts_command, add_account, backup_command, change_password_command, init_command, migrate_stronghold_snapshot_v2_to_v3_command, mnemonic_command, new_account_command, node_info_command, - restore_command, set_node_url_command, sync_command, unlock_wallet, InitParameters, WalletCli, WalletCommand, + restore_command, set_node_url_command, set_pow_command, sync_command, unlock_wallet, InitParameters, WalletCli, + WalletCommand, }, error::Error, helper::{get_account_alias, get_decision, get_password, pick_account}, @@ -54,6 +55,13 @@ pub async fn new_wallet(cli: WalletCli) -> Result<(Option, Option { + let wallet = set_pow_command(storage_path, snapshot_path, local_pow, worker_count).await?; + (Some(wallet), None) + } WalletCommand::Sync => { let wallet = sync_command(storage_path, snapshot_path).await?; (Some(wallet), None) diff --git a/sdk/src/client/node_api/core/routes.rs b/sdk/src/client/node_api/core/routes.rs index ec459f59d2..091365838c 100644 --- a/sdk/src/client/node_api/core/routes.rs +++ b/sdk/src/client/node_api/core/routes.rs @@ -124,6 +124,7 @@ impl ClientInner { if !self.get_fallback_to_local_pow().await { return Err(Error::Node(crate::client::node_api::error::Error::UnavailablePow)); } + log::debug!("[post_block] falling back to local PoW"); self.network_info.write().await.local_pow = true; diff --git a/sdk/src/wallet/account/operations/output_consolidation.rs b/sdk/src/wallet/account/operations/output_consolidation.rs index 071b2d1c11..b8a0f5bbeb 100644 --- a/sdk/src/wallet/account/operations/output_consolidation.rs +++ b/sdk/src/wallet/account/operations/output_consolidation.rs @@ -149,6 +149,7 @@ where drop(account_details); + #[allow(clippy::option_if_let_else)] let output_threshold = match params.output_threshold { Some(t) => t, None => { From e29475a1427269abbf5d89ecb3fd9b74bb66fab3 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Fri, 22 Sep 2023 10:41:26 +0200 Subject: [PATCH 09/11] Fix tungstenite audit (#1286) --- Cargo.lock | 60 +++++++++++++++++------------------ bindings/core/Cargo.toml | 2 +- cli/Cargo.toml | 4 +-- sdk/Cargo.toml | 2 +- sdk/src/types/fuzz/Cargo.toml | 2 +- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fefaa67e95..fd7c74eced 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", ] @@ -467,9 +467,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", @@ -477,9 +477,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", @@ -680,9 +680,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", @@ -936,7 +936,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", @@ -1363,9 +1363,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" @@ -2183,9 +2183,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", @@ -2766,9 +2766,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", @@ -2785,7 +2785,7 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", - "rustls-webpki 0.101.5", + "rustls-webpki 0.101.6", "sct", ] @@ -2822,9 +2822,9 @@ 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", @@ -3015,9 +3015,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", @@ -3068,9 +3068,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" @@ -3383,9 +3383,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", @@ -3517,9 +3517,9 @@ dependencies = [ [[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" @@ -3727,9 +3727,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 622989e74d..339198725d 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/cli/Cargo.toml b/cli/Cargo.toml index f93dccd3bc..ad1609fa72 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", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 08739e6769..35696679bc 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -43,7 +43,7 @@ iterator-sorted = { version = "0.1.0", default-features = false } itertools = { version = "0.11.0", default-features = false, features = [ "use_alloc", ] } -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 = [ 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] From 3347676e5ad84550ef0a2d7971e83b5d3c3dd9ee Mon Sep 17 00:00:00 2001 From: DaughterOfMars Date: Fri, 22 Sep 2023 05:30:41 -0400 Subject: [PATCH 10/11] Add feature sets CI (#1254) * Add a few common feature sets * Add another * add new workflow * add libudev * better message * fix gating * fix wasm * Revert breaking feature changes * missed reverts * Nit --------- Co-authored-by: Thibault Martinez --- .github/workflows/bindings-nodejs.yml | 4 +- .github/workflows/bindings-python.yml | 5 +- .github/workflows/bindings-wallet-nodejs.yml | 4 +- .github/workflows/bindings-wasm.yml | 4 +- .github/workflows/build-and-test.yml | 4 +- .github/workflows/common-features.yml | 66 +++++ .github/workflows/coverage.yml | 2 +- .github/workflows/lint.yml | 4 +- .github/workflows/private-tangle-tests.yml | 4 +- sdk/Cargo.toml | 4 +- sdk/src/client/core.rs | 6 + sdk/src/wallet/core/operations/storage.rs | 4 +- .../core/operations/stronghold_backup/mod.rs | 18 +- .../stronghold_backup/stronghold_snapshot.rs | 20 +- sdk/src/wallet/migration/chrysalis.rs | 273 ++++++++++-------- sdk/src/wallet/migration/migrate_4.rs | 1 + sdk/src/wallet/migration/mod.rs | 5 +- sdk/src/wallet/storage/constants.rs | 2 - 18 files changed, 260 insertions(+), 170 deletions(-) create mode 100644 .github/workflows/common-features.yml diff --git a/.github/workflows/bindings-nodejs.yml b/.github/workflows/bindings-nodejs.yml index 6f5ee91ed6..8168c8acc7 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, 1.1] + branches: [develop, production, "1.1"] 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, 1.1] + branches: [develop, production, "1.1"] paths: - ".github/actions/**" - "**.rs" # Include all rust files diff --git a/.github/workflows/bindings-python.yml b/.github/workflows/bindings-python.yml index 3103df0e6d..f7b5f09f47 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, 1.1] + branches: [develop, production, "1.1"] 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, 1.1] + branches: [develop, production, "1.1"] paths: - ".github/actions/**" - "**.rs" # Include all rust files @@ -120,4 +120,3 @@ jobs: - name: Run tests working-directory: bindings/python run: tox - diff --git a/.github/workflows/bindings-wallet-nodejs.yml b/.github/workflows/bindings-wallet-nodejs.yml index d20e2fadb9..750334f26c 100644 --- a/.github/workflows/bindings-wallet-nodejs.yml +++ b/.github/workflows/bindings-wallet-nodejs.yml @@ -2,7 +2,7 @@ name: Nodejs bindings checks (wallet) on: push: - branches: [develop, production, 1.1] + branches: [develop, production, "1.1"] paths: - ".github/actions/**" - "**.rs" # Include all rust files @@ -16,7 +16,7 @@ on: - ".github/workflows/bindings-wallet-nodejs.yml" - ".patches/*" pull_request: - branches: [develop, production, 1.1] + branches: [develop, production, "1.1"] paths: - ".github/actions/**" - "**.rs" # Include all rust files diff --git a/.github/workflows/bindings-wasm.yml b/.github/workflows/bindings-wasm.yml index fffbbf9e27..a2daad34d3 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, 1.1] + branches: [develop, production, "1.1"] 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, 1.1] + branches: [develop, production, "1.1"] 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 1c260a1603..5d35542b59 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, 1.1] + branches: [develop, production, "1.1"] 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, 1.1] + branches: [develop, production, "1.1"] 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 ba813b3826..16b9f9c9a8 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,7 @@ name: Test coverage on: push: - branches: [develop, production, 1.1] + branches: [develop, production, "1.1"] paths: - ".cargo/config.toml" - ".github/workflows/coverage.yml" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3f313274ce..1c7a3e346c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,7 @@ name: Linting on: push: - branches: [develop, production, 1.1] + branches: [develop, production, "1.1"] 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, 1.1] + branches: [develop, production, "1.1"] 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 60bad53595..c0ea59d176 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, 1.1] + branches: [develop, production, "1.1"] 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, 1.1] + branches: [develop, production, "1.1"] paths: - ".cargo/config.toml" - ".github/workflows/private-tangle-tests.yml" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 6295469169..fcb5341cb6 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -144,11 +144,11 @@ events = [] irc_27 = ["url", "serde"] irc_30 = ["url", "serde"] ledger_nano = ["iota-ledger-nano"] -mqtt = ["std", "regex", "rumqttc", "dep:once_cell"] +mqtt = ["std", "tls", "regex", "rumqttc", "dep:once_cell"] participation = ["storage"] pow = ["std", "num_cpus", "iota-crypto/curl-p"] rand = ["dep:rand"] -rocksdb = ["dep:rocksdb", "storage"] +rocksdb = ["storage", "dep:rocksdb"] serde = [ "serde_repr", "hashbrown/serde", diff --git a/sdk/src/client/core.rs b/sdk/src/client/core.rs index 0b7249865b..b28c639ce4 100644 --- a/sdk/src/client/core.rs +++ b/sdk/src/client/core.rs @@ -207,4 +207,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/wallet/core/operations/storage.rs b/sdk/src/wallet/core/operations/storage.rs index 282e421769..d50b42e135 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::{ @@ -13,7 +12,8 @@ mod storage_stub { }, wallet::{ core::builder::dto::WalletBuilderDto, - storage::constants::{CHRYSALIS_STORAGE_KEY, SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY}, + migration::chrysalis::CHRYSALIS_STORAGE_KEY, + storage::constants::{SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY}, Wallet, WalletBuilder, }, }; diff --git a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs index ca6ef165cd..840f6bb557 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs @@ -9,15 +9,17 @@ use futures::{future::try_join_all, FutureExt}; use self::stronghold_snapshot::read_data_from_stronghold_snapshot; #[cfg(feature = "storage")] -use crate::wallet::WalletBuilder; +use crate::{ + client::storage::StorageAdapter, + wallet::{migration::chrysalis::CHRYSALIS_STORAGE_KEY, WalletBuilder}, +}; use crate::{ client::{ secret::{stronghold::StrongholdSecretManager, SecretManager, SecretManagerConfig, SecretManagerDto}, - storage::StorageAdapter, utils::Password, }, types::block::address::Hrp, - wallet::{storage::constants::CHRYSALIS_STORAGE_KEY, Account, Wallet}, + wallet::{Account, Wallet}, }; impl Wallet { @@ -103,7 +105,10 @@ impl Wallet { .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_coin_type, read_secret_manager, read_accounts, chrysalis_data) = + #[cfg_attr(not(feature = "storage"), allow(unused))] + let chrysalis_data = stronghold_snapshot::migrate_snapshot_from_chrysalis_to_stardust(&new_stronghold).await?; + + let (read_client_options, read_coin_type, read_secret_manager, read_accounts) = read_data_from_stronghold_snapshot::(&new_stronghold).await?; // If the coin type is not matching the current one, then the addresses in the accounts will also not be @@ -280,7 +285,10 @@ impl Wallet { .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_coin_type, read_secret_manager, read_accounts, chrysalis_data) = + #[cfg_attr(not(feature = "storage"), allow(unused))] + let chrysalis_data = stronghold_snapshot::migrate_snapshot_from_chrysalis_to_stardust(&new_stronghold).await?; + + let (read_client_options, read_coin_type, read_secret_manager, read_accounts) = read_data_from_stronghold_snapshot::(&new_stronghold).await?; // If the coin type is not matching the current one, then the addresses in the accounts will also not be 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 3b4adbb6a0..010a0524eb 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs @@ -12,14 +12,14 @@ use crate::{ wallet::{ account::{AccountDetails, AccountDetailsDto}, migration::{ - chrysalis::{migrate_from_chrysalis_data, to_chrysalis_key}, + chrysalis::{migrate_from_chrysalis_data, to_chrysalis_key, CHRYSALIS_STORAGE_KEY}, latest_backup_migration_version, migrate, MigrationData, MIGRATION_VERSION_KEY, }, - storage::constants::{CHRYSALIS_STORAGE_KEY, WALLET_INDEXATION_KEY}, ClientOptions, Error as WalletError, Wallet, }, }; +pub(crate) const WALLET_INDEXATION_KEY: &str = "iota-wallet-account-manager"; pub(crate) const CLIENT_OPTIONS_KEY: &str = "client_options"; pub(crate) const COIN_TYPE_KEY: &str = "coin_type"; pub(crate) const SECRET_MANAGER_KEY: &str = "secret_manager"; @@ -62,10 +62,7 @@ pub(crate) async fn read_data_from_stronghold_snapshot, Option, Option>, - Option>, )> { - let chrysalis_data = migrate_snapshot_from_chrysalis_to_stardust(stronghold).await?; - migrate(stronghold).await?; // Get client_options @@ -77,7 +74,7 @@ pub(crate) async fn read_data_from_stronghold_snapshot crate::wallet::Result>> { log::debug!("migrate_snapshot_from_chrysalis_to_stardust"); + let stronghold = stronghold_adapter.inner().await; let stronghold_client = match stronghold.load_client(b"iota-wallet-records") { Ok(client) => client, diff --git a/sdk/src/wallet/migration/chrysalis.rs b/sdk/src/wallet/migration/chrysalis.rs index c4fc30e81b..db53b66bb8 100644 --- a/sdk/src/wallet/migration/chrysalis.rs +++ b/sdk/src/wallet/migration/chrysalis.rs @@ -5,7 +5,7 @@ use std::{ collections::{HashMap, HashSet}, convert::TryInto, io::Read, - path::{Path, PathBuf}, + path::Path, str::FromStr, }; @@ -13,27 +13,18 @@ use crypto::{ ciphers::{chacha::XChaCha20Poly1305, traits::Aead}, macs::hmac::HMAC_SHA512, }; -use rocksdb::{IteratorMode, DB}; use serde::Serialize; use serde_json::Value; use zeroize::Zeroizing; use crate::{ - client::{constants::IOTA_COIN_TYPE, storage::StorageAdapter, Password}, + client::{constants::IOTA_COIN_TYPE, Password}, types::block::address::Bech32Address, - wallet::{ - migration::{MigrationData, MIGRATION_VERSION_KEY}, - storage::{ - constants::{ - ACCOUNTS_INDEXATION_KEY, ACCOUNT_INDEXATION_KEY, CHRYSALIS_STORAGE_KEY, SECRET_MANAGER_KEY, - WALLET_INDEXATION_KEY, - }, - StorageManager, - }, - Error, Result, - }, + wallet::{Error, Result}, }; +pub(crate) const CHRYSALIS_STORAGE_KEY: &str = "chrysalis-data"; + #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct AccountAddress { @@ -61,86 +52,7 @@ pub(crate) struct AccountDetailsDto { native_token_foundries: HashMap, } -#[cfg(feature = "rocksdb")] -pub async fn migrate_db_chrysalis_to_stardust( - storage_path: impl Into + Send, - password: Option, - new_db_encryption_key: impl Into>> + Send, -) -> Result<()> { - let storage_path_string = storage_path.into(); - // `/db` will be appended to the chrysalis storage path, because that's how it was done in the chrysalis wallet - let chrysalis_storage_path = &(*storage_path_string).join("db"); - - if !chrysalis_storage_path.is_dir() { - return Err(crate::wallet::Error::Migration( - "no chrysalis data to migrate".to_string(), - )); - } - let chrysalis_data = get_chrysalis_data(chrysalis_storage_path, password)?; - - // create new accounts base on previous data - let (new_accounts, secret_manager_dto) = migrate_from_chrysalis_data(&chrysalis_data, &storage_path_string, false)?; - - // convert to string keys - let chrysalis_data_with_string_keys = chrysalis_data - .into_iter() - .map(|(k, v)| { - Ok(( - String::from_utf8(k).map_err(|_| Error::Migration("invalid utf8".into()))?, - v, - )) - }) - .collect::>>()?; - - log::debug!( - "Chrysalis data: {}", - serde_json::to_string_pretty(&chrysalis_data_with_string_keys)? - ); - - let stardust_db = crate::wallet::storage::adapter::rocksdb::RocksdbStorageAdapter::new(storage_path_string)?; - - let stardust_storage = StorageManager::new(stardust_db, new_db_encryption_key).await?; - - // store chrysalis data in a new key - stardust_storage - .set(CHRYSALIS_STORAGE_KEY, &chrysalis_data_with_string_keys) - .await?; - // write new accounts to db (with account indexation) - let accounts_indexation_data: Vec = new_accounts.iter().map(|account| account.index).collect(); - stardust_storage - .set(ACCOUNTS_INDEXATION_KEY, &accounts_indexation_data) - .await?; - for new_account in new_accounts { - stardust_storage - .set(&format!("{ACCOUNT_INDEXATION_KEY}{}", new_account.index), &new_account) - .await?; - } - - if let Some(secret_manager_dto) = secret_manager_dto { - // This is required for the secret manager to be loaded - stardust_storage - .set( - WALLET_INDEXATION_KEY, - &serde_json::from_str::(&format!("{{ \"coinType\": {IOTA_COIN_TYPE}}}"))?, - ) - .await?; - stardust_storage - .set(SECRET_MANAGER_KEY, &serde_json::from_str::(&secret_manager_dto)?) - .await?; - } - - // set db migration version - let migration_version = crate::wallet::migration::migrate_4::Migrate::version(); - stardust_storage.set(MIGRATION_VERSION_KEY, &migration_version).await?; - - drop(stardust_storage); - - // remove old db - std::fs::remove_dir_all(chrysalis_storage_path)?; - - Ok(()) -} - +#[allow(unused)] pub(crate) fn migrate_from_chrysalis_data( chrysalis_data: &HashMap, String>, storage_path: &Path, @@ -237,39 +149,7 @@ pub(crate) fn migrate_from_chrysalis_data( Ok((new_accounts, secret_manager_dto)) } -fn get_chrysalis_data(chrysalis_storage_path: &Path, password: Option) -> Result, String>> { - let encryption_key = password.map(storage_password_to_encryption_key); - let chrysalis_db = DB::open_default(chrysalis_storage_path)?; - let mut chrysalis_data = HashMap::new(); - // iterate over all rocksdb keys - for item in chrysalis_db.iterator(IteratorMode::Start) { - let (key, value) = item?; - - let key_utf8 = String::from_utf8(key.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))?; - let value = if let Some(encryption_key) = &encryption_key { - let value_utf8 = String::from_utf8(value.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))?; - // "iota-wallet-key-checksum_value" is never an encrypted value - if key_utf8 == "iota-wallet-key-checksum_value" { - value_utf8 - } else if let Ok(value) = serde_json::from_str::>(&value_utf8) { - decrypt_record(value, encryption_key)? - } else { - value_utf8 - } - } else { - String::from_utf8(value.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))? - }; - - chrysalis_data.insert(key.to_vec(), value); - } - if !chrysalis_data.contains_key(&b"iota-wallet-account-indexation".to_vec()) { - return Err(crate::wallet::Error::Migration( - "no chrysalis data to migrate".to_string(), - )); - } - Ok(chrysalis_data) -} - +#[allow(unused)] fn storage_password_to_encryption_key(password: Password) -> Zeroizing<[u8; 32]> { let mut dk = [0; 64]; // safe to unwrap (rounds > 0) @@ -283,6 +163,7 @@ fn storage_password_to_encryption_key(password: Password) -> Zeroizing<[u8; 32]> Zeroizing::new(key) } +#[allow(unused)] fn decrypt_record(record_bytes: Vec, encryption_key: &[u8; 32]) -> crate::wallet::Result { let mut record: &[u8] = &record_bytes; @@ -310,6 +191,7 @@ fn decrypt_record(record_bytes: Vec, encryption_key: &[u8; 32]) -> crate::wa String::from_utf8(pt).map_err(|e| Error::Migration(format!("{:?}", e))) } +#[allow(unused)] pub(crate) fn to_chrysalis_key(key: &[u8], stronghold: bool) -> Vec { // key only needs to be hashed for stronghold if stronghold { @@ -323,3 +205,140 @@ pub(crate) fn to_chrysalis_key(key: &[u8], stronghold: bool) -> Vec { key.into() } } + +#[cfg(not(target_family = "wasm"))] +#[cfg(feature = "rocksdb")] +pub(crate) mod rocksdb { + use ::rocksdb::{IteratorMode, DB}; + + use super::*; + use crate::{ + client::storage::StorageAdapter, + wallet::{ + migration::{MigrationData, MIGRATION_VERSION_KEY}, + storage::{ + constants::{ + ACCOUNTS_INDEXATION_KEY, ACCOUNT_INDEXATION_KEY, SECRET_MANAGER_KEY, WALLET_INDEXATION_KEY, + }, + StorageManager, + }, + }, + }; + + pub async fn migrate_db_chrysalis_to_stardust( + storage_path: impl Into + Send, + password: Option, + new_db_encryption_key: impl Into>> + Send, + ) -> Result<()> { + let storage_path_string = storage_path.into(); + // `/db` will be appended to the chrysalis storage path, because that's how it was done in the chrysalis wallet + let chrysalis_storage_path = &(*storage_path_string).join("db"); + + if !chrysalis_storage_path.is_dir() { + return Err(crate::wallet::Error::Migration( + "no chrysalis data to migrate".to_string(), + )); + } + let chrysalis_data = get_chrysalis_data(chrysalis_storage_path, password)?; + + // create new accounts base on previous data + let (new_accounts, secret_manager_dto) = + migrate_from_chrysalis_data(&chrysalis_data, &storage_path_string, false)?; + + // convert to string keys + let chrysalis_data_with_string_keys = chrysalis_data + .into_iter() + .map(|(k, v)| { + Ok(( + String::from_utf8(k).map_err(|_| Error::Migration("invalid utf8".into()))?, + v, + )) + }) + .collect::>>()?; + + log::debug!( + "Chrysalis data: {}", + serde_json::to_string_pretty(&chrysalis_data_with_string_keys)? + ); + + let stardust_db = crate::wallet::storage::adapter::rocksdb::RocksdbStorageAdapter::new(storage_path_string)?; + + let stardust_storage = StorageManager::new(stardust_db, new_db_encryption_key).await?; + + // store chrysalis data in a new key + stardust_storage + .set(CHRYSALIS_STORAGE_KEY, &chrysalis_data_with_string_keys) + .await?; + // write new accounts to db (with account indexation) + let accounts_indexation_data: Vec = new_accounts.iter().map(|account| account.index).collect(); + stardust_storage + .set(ACCOUNTS_INDEXATION_KEY, &accounts_indexation_data) + .await?; + for new_account in new_accounts { + stardust_storage + .set(&format!("{ACCOUNT_INDEXATION_KEY}{}", new_account.index), &new_account) + .await?; + } + + if let Some(secret_manager_dto) = secret_manager_dto { + // This is required for the secret manager to be loaded + stardust_storage + .set( + WALLET_INDEXATION_KEY, + &serde_json::from_str::(&format!("{{ \"coinType\": {IOTA_COIN_TYPE}}}"))?, + ) + .await?; + stardust_storage + .set(SECRET_MANAGER_KEY, &serde_json::from_str::(&secret_manager_dto)?) + .await?; + } + + // set db migration version + let migration_version = crate::wallet::migration::migrate_4::Migrate::version(); + stardust_storage.set(MIGRATION_VERSION_KEY, &migration_version).await?; + + drop(stardust_storage); + + // remove old db + std::fs::remove_dir_all(chrysalis_storage_path)?; + + Ok(()) + } + + fn get_chrysalis_data( + chrysalis_storage_path: &Path, + password: Option, + ) -> Result, String>> { + let encryption_key = password.map(storage_password_to_encryption_key); + let chrysalis_db = DB::open_default(chrysalis_storage_path)?; + let mut chrysalis_data = HashMap::new(); + // iterate over all rocksdb keys + for item in chrysalis_db.iterator(IteratorMode::Start) { + let (key, value) = item?; + + let key_utf8 = String::from_utf8(key.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))?; + let value = if let Some(encryption_key) = &encryption_key { + let value_utf8 = + String::from_utf8(value.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))?; + // "iota-wallet-key-checksum_value" is never an encrypted value + if key_utf8 == "iota-wallet-key-checksum_value" { + value_utf8 + } else if let Ok(value) = serde_json::from_str::>(&value_utf8) { + decrypt_record(value, encryption_key)? + } else { + value_utf8 + } + } else { + String::from_utf8(value.to_vec()).map_err(|_| Error::Migration("invalid utf8".into()))? + }; + + chrysalis_data.insert(key.to_vec(), value); + } + if !chrysalis_data.contains_key(&b"iota-wallet-account-indexation".to_vec()) { + return Err(crate::wallet::Error::Migration( + "no chrysalis data to migrate".to_string(), + )); + } + Ok(chrysalis_data) + } +} diff --git a/sdk/src/wallet/migration/migrate_4.rs b/sdk/src/wallet/migration/migrate_4.rs index 0aede8560c..b857638b51 100644 --- a/sdk/src/wallet/migration/migrate_4.rs +++ b/sdk/src/wallet/migration/migrate_4.rs @@ -69,6 +69,7 @@ impl Migration for Migrate { } } +#[cfg(feature = "storage")] fn migrate_wallet(wallet: &mut serde_json::Value) -> Result<()> { let wallet: &mut serde_json::Map = wallet .as_object_mut() diff --git a/sdk/src/wallet/migration/mod.rs b/sdk/src/wallet/migration/mod.rs index 139701d14d..ed08a8bf5a 100644 --- a/sdk/src/wallet/migration/mod.rs +++ b/sdk/src/wallet/migration/mod.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[cfg(not(target_family = "wasm"))] +#[cfg(any(feature = "storage", feature = "stronghold"))] pub(crate) mod chrysalis; mod migrate_0; mod migrate_1; @@ -14,7 +14,8 @@ use std::collections::HashMap; use anymap::Map; use async_trait::async_trait; #[cfg(not(target_family = "wasm"))] -pub use chrysalis::migrate_db_chrysalis_to_stardust; +#[cfg(feature = "rocksdb")] +pub use chrysalis::rocksdb::migrate_db_chrysalis_to_stardust; use once_cell::sync::Lazy; use serde::{de::DeserializeOwned, Deserialize, Serialize}; diff --git a/sdk/src/wallet/storage/constants.rs b/sdk/src/wallet/storage/constants.rs index 29d9fd973c..ae75d94881 100644 --- a/sdk/src/wallet/storage/constants.rs +++ b/sdk/src/wallet/storage/constants.rs @@ -31,5 +31,3 @@ pub(crate) const DATABASE_SCHEMA_VERSION_KEY: &str = "database-schema-version"; pub(crate) const PARTICIPATION_EVENTS: &str = "participation-events"; #[cfg(feature = "participation")] pub(crate) const PARTICIPATION_CACHED_OUTPUTS: &str = "participation-cached-outputs"; - -pub(crate) const CHRYSALIS_STORAGE_KEY: &str = "chrysalis-data"; From 8f1f79fe70bdcea68352044da95b32476dbf71e9 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Fri, 22 Sep 2023 14:02:27 +0200 Subject: [PATCH 11/11] Fix merge 1.1 nto 2.0 --- sdk/Cargo.toml | 53 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index ead6ac75ea..eda2106ca9 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -18,6 +18,30 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] # Mandatory dependencies +bech32 = { version = "0.9.1", default-features = false } +bitflags = { version = "2.4.0", default-features = false } +derive_more = { version = "0.99.17", default-features = false, features = [ + "from", + "from_str", + "as_ref", + "deref", + "deref_mut", + "display", + "add", + "add_assign", +] } +getset = { version = "0.1.2", default-features = false } +hashbrown = { version = "0.14.0", default-features = false, features = [ + "ahash", + "inline-more", +] } +hex = { version = "0.4.3", default-features = false } +iota-crypto = { version = "0.23.0", default-features = false, features = [ + "blake2b", + "ed25519", + "secp256k1", +] } +iterator-sorted = { version = "0.1.0", default-features = false } packable = { version = "0.8.3", default-features = false, features = [ "primitive-types", ] } @@ -78,30 +102,6 @@ tokio = { version = "1.32.0", default-features = false, features = [ "time", "sync", ], optional = true } -bech32 = { version = "0.9.1", default-features = false } -bitflags = { version = "2.4.0", default-features = false } -derive_more = { version = "0.99.17", default-features = false, features = [ - "from", - "from_str", - "as_ref", - "deref", - "deref_mut", - "display", - "add", - "add_assign", -] } -getset = { version = "0.1.2", default-features = false } -hashbrown = { version = "0.14.0", default-features = false, features = [ - "ahash", - "inline-more", -] } -hex = { version = "0.4.3", default-features = false } -iota-crypto = { version = "0.23.0", default-features = false, features = [ - "blake2b", - "ed25519", - "secp256k1", -] } -iterator-sorted = { version = "0.1.0", default-features = false } [target.'cfg(target_family = "wasm")'.dependencies] gloo-storage = { version = "0.3.0", default-features = false, optional = true } @@ -137,14 +137,12 @@ tokio = { version = "1.32.0", default-features = false, features = [ [features] default = ["client", "wallet", "tls"] -rocksdb = ["storage", "dep:rocksdb"] irc_27 = ["serde", "dep:url"] irc_30 = ["serde", "dep:url"] ledger_nano = ["dep:iota-ledger-nano"] mqtt = ["std", "tls", "dep:regex", "dep:rumqttc", "dep:once_cell"] participation = ["storage"] -rand = ["dep:rand"] -events = [] +rocksdb = ["storage", "dep:rocksdb"] serde = [ "hashbrown/serde", "packable/serde", @@ -174,6 +172,7 @@ stronghold = [ "dep:once_cell", ] tls = ["reqwest?/rustls-tls", "rumqttc?/use-rustls"] +events = [] private_key_secret_manager = ["dep:bs58"] client = [