From fc9f0f56bb5cfc146993e53aa9656ded220734e1 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 21 Feb 2024 09:46:02 +0100 Subject: [PATCH 1/4] ValidatorsResponse stakers to validators (#2037) * ValidatorsResponse stakers to validators * TODOs * python lint --- bindings/python/iota_sdk/client/responses.py | 4 ++-- bindings/python/tests/test_api_responses.py | 5 +++-- sdk/src/types/api/core.rs | 2 +- sdk/tests/types/api/core.rs | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bindings/python/iota_sdk/client/responses.py b/bindings/python/iota_sdk/client/responses.py index 8f15a75010..fbf3a76802 100644 --- a/bindings/python/iota_sdk/client/responses.py +++ b/bindings/python/iota_sdk/client/responses.py @@ -124,11 +124,11 @@ class ValidatorsResponse: Response of GET /api/core/v3/validators Attributes: - stakers: List of registered validators ready for the next epoch. + validators: List of registered validators ready for the next epoch. page_size: The number of validators returned per one API request with pagination. cursor: The cursor that needs to be provided as cursor query parameter to request the next page. If empty, this was the last page. """ - stakers: List[ValidatorResponse] + validators: List[ValidatorResponse] page_size: int cursor: Optional[str] = None diff --git a/bindings/python/tests/test_api_responses.py b/bindings/python/tests/test_api_responses.py index e7250c0c06..5a041fd28f 100644 --- a/bindings/python/tests/test_api_responses.py +++ b/bindings/python/tests/test_api_responses.py @@ -3,7 +3,7 @@ from typing import Generic, TypeVar from json import load, loads, dumps -from iota_sdk import RoutesResponse, CongestionResponse, ManaRewardsResponse, ValidatorResponse, ValidatorsResponse, CommitteeResponse, IssuanceBlockHeaderResponse, Block, OutputResponse, SlotCommitment +from iota_sdk import RoutesResponse, CongestionResponse, ManaRewardsResponse, ValidatorResponse, CommitteeResponse, IssuanceBlockHeaderResponse, Block, OutputResponse, SlotCommitment base_path = '../../sdk/tests/types/api/fixtures/' @@ -34,7 +34,8 @@ def test_api_response(cls_type: Generic[T], path: str): # GET /api/core/v3/rewards/{outputId} test_api_response(ManaRewardsResponse, "get-mana-rewards-example.json") # GET /api/core/v3/validators - test_api_response(ValidatorsResponse, "get-validators-example.json") + # TODO: enable when TIP is updated + # test_api_response(ValidatorsResponse, "get-validators-example.json") # GET /api/core/v3/validators/{bech32Address} test_api_response(ValidatorResponse, "get-validator-example.json") # GET /api/core/v3/committee diff --git a/sdk/src/types/api/core.rs b/sdk/src/types/api/core.rs index aee9a6e31c..5b58e2a13d 100644 --- a/sdk/src/types/api/core.rs +++ b/sdk/src/types/api/core.rs @@ -224,7 +224,7 @@ pub struct ValidatorResponse { #[serde(rename_all = "camelCase")] pub struct ValidatorsResponse { /// List of registered validators ready for the next epoch. - stakers: Vec, + validators: Vec, /// The number of validators returned per one API request with pagination. page_size: u32, /// The cursor that needs to be provided as cursor query parameter to request the next page. If empty, this was the diff --git a/sdk/tests/types/api/core.rs b/sdk/tests/types/api/core.rs index a9489a8761..ea03e1f84e 100644 --- a/sdk/tests/types/api/core.rs +++ b/sdk/tests/types/api/core.rs @@ -62,7 +62,8 @@ fn responses() { // GET /api/core/v3/rewards/{outputId} json_response::("get-mana-rewards-example.json").unwrap(); // GET /api/core/v3/validators - json_response::("get-validators-example.json").unwrap(); + // TODO reenable when TIP is updated + // json_response::("get-validators-example.json").unwrap(); // GET /api/core/v3/validators/{bech32Address} json_response::("get-validator-example.json").unwrap(); // GET /api/core/v3/committee From 4ca02ca3463e4105883b0f9d795ce4ecca4412e9 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 21 Feb 2024 16:29:22 +0100 Subject: [PATCH 2/4] Remove allow_additional_input_selection false for implicit account transition (#2043) --- sdk/src/wallet/operations/transaction/account.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/src/wallet/operations/transaction/account.rs b/sdk/src/wallet/operations/transaction/account.rs index cf19e07c7a..2141dd7dd4 100644 --- a/sdk/src/wallet/operations/transaction/account.rs +++ b/sdk/src/wallet/operations/transaction/account.rs @@ -100,7 +100,6 @@ where let transaction_options = TransactionOptions { required_inputs: [*output_id].into(), issuer_id: Some(account_id), - allow_additional_input_selection: false, ..Default::default() }; From 43cc073eb1896ca394d96844a0635f4e88d5b345 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Wed, 21 Feb 2024 16:31:18 +0100 Subject: [PATCH 3/4] Add anchorIdToBech32 utils (#2041) * Add anchorIdToBech32 utils * Optional * Remove ? * Optional * = None * sad cat --- bindings/core/src/method/client.rs | 8 +++++++ bindings/core/src/method/utils.rs | 5 ++++- bindings/core/src/method_handler/client.rs | 3 +++ bindings/core/src/method_handler/utils.rs | 3 +++ bindings/core/src/response.rs | 6 ++++- bindings/nodejs/lib/client/client.ts | 22 +++++++++++++++++++ .../nodejs/lib/types/client/bridge/client.ts | 8 +++++++ .../nodejs/lib/types/client/bridge/index.ts | 2 ++ .../nodejs/lib/types/utils/bridge/index.ts | 2 ++ .../nodejs/lib/types/utils/bridge/utils.ts | 10 ++++++++- bindings/nodejs/lib/utils/utils.ts | 21 ++++++++++++++++++ bindings/python/iota_sdk/client/_utils.py | 14 +++++++++--- bindings/python/iota_sdk/utils.py | 9 ++++++++ bindings/python/tests/test_api_responses.py | 3 ++- sdk/src/client/utils.rs | 14 +++++++++++- 15 files changed, 122 insertions(+), 8 deletions(-) diff --git a/bindings/core/src/method/client.rs b/bindings/core/src/method/client.rs index 63c07f61d4..6312b8ba87 100644 --- a/bindings/core/src/method/client.rs +++ b/bindings/core/src/method/client.rs @@ -425,6 +425,14 @@ pub enum ClientMethod { /// Human readable part bech32_hrp: Option, }, + /// Transforms an anchor id to a bech32 encoded address + #[serde(rename_all = "camelCase")] + AnchorIdToBech32 { + /// Anchor ID + anchor_id: AnchorId, + /// Human readable part + bech32_hrp: Option, + }, /// Transforms an nft id to a bech32 encoded address #[serde(rename_all = "camelCase")] NftIdToBech32 { diff --git a/bindings/core/src/method/utils.rs b/bindings/core/src/method/utils.rs index 246a2febd5..9bfd0789e3 100644 --- a/bindings/core/src/method/utils.rs +++ b/bindings/core/src/method/utils.rs @@ -8,7 +8,7 @@ use iota_sdk::{ client::secret::types::InputSigningData, types::block::{ address::{Bech32Address, Hrp}, - output::{AccountId, NftId, Output, OutputId, StorageScoreParameters}, + output::{AccountId, AnchorId, NftId, Output, OutputId, StorageScoreParameters}, payload::signed_transaction::{ dto::{SignedTransactionPayloadDto, TransactionDto}, TransactionId, @@ -39,6 +39,9 @@ pub enum UtilsMethod { /// Transforms an account id to a bech32 encoded address #[serde(rename_all = "camelCase")] AccountIdToBech32 { account_id: AccountId, bech32_hrp: Hrp }, + /// Transforms an anchor id to a bech32 encoded address + #[serde(rename_all = "camelCase")] + AnchorIdToBech32 { anchor_id: AnchorId, bech32_hrp: Hrp }, /// Transforms an nft id to a bech32 encoded address #[serde(rename_all = "camelCase")] NftIdToBech32 { nft_id: NftId, bech32_hrp: Hrp }, diff --git a/bindings/core/src/method_handler/client.rs b/bindings/core/src/method_handler/client.rs index d8a3e31564..d49c34f5e8 100644 --- a/bindings/core/src/method_handler/client.rs +++ b/bindings/core/src/method_handler/client.rs @@ -329,6 +329,9 @@ pub(crate) async fn call_client_method_internal(client: &Client, method: ClientM ClientMethod::AccountIdToBech32 { account_id, bech32_hrp } => { Response::Bech32Address(client.account_id_to_bech32(account_id, bech32_hrp).await?) } + ClientMethod::AnchorIdToBech32 { anchor_id, bech32_hrp } => { + Response::Bech32Address(client.anchor_id_to_bech32(anchor_id, bech32_hrp).await?) + } ClientMethod::NftIdToBech32 { nft_id, bech32_hrp } => { Response::Bech32Address(client.nft_id_to_bech32(nft_id, bech32_hrp).await?) } diff --git a/bindings/core/src/method_handler/utils.rs b/bindings/core/src/method_handler/utils.rs index 3b55de23a7..2a2ff07a15 100644 --- a/bindings/core/src/method_handler/utils.rs +++ b/bindings/core/src/method_handler/utils.rs @@ -28,6 +28,9 @@ pub(crate) fn call_utils_method_internal(method: UtilsMethod) -> Result { Response::Bech32Address(account_id.to_bech32(bech32_hrp)) } + UtilsMethod::AnchorIdToBech32 { anchor_id, bech32_hrp } => { + Response::Bech32Address(anchor_id.to_bech32(bech32_hrp)) + } UtilsMethod::NftIdToBech32 { nft_id, bech32_hrp } => Response::Bech32Address(nft_id.to_bech32(bech32_hrp)), UtilsMethod::HexPublicKeyToBech32Address { hex, bech32_hrp } => { Response::Bech32Address(hex_public_key_to_bech32_address(&hex, bech32_hrp)?) diff --git a/bindings/core/src/response.rs b/bindings/core/src/response.rs index 7e2d85b9f1..819620e025 100644 --- a/bindings/core/src/response.rs +++ b/bindings/core/src/response.rs @@ -247,9 +247,13 @@ pub enum Response { Output(Output), /// Response for: /// - [`AccountIdToBech32`](crate::method::ClientMethod::AccountIdToBech32) + /// - [`AccountIdToBech32`](crate::method::UtilsMethod::AccountIdToBech32) + /// - [`AnchorIdToBech32`](crate::method::ClientMethod::AnchorIdToBech32) + /// - [`AnchorIdToBech32`](crate::method::UtilsMethod::AnchorIdToBech32) + /// - [`NftIdToBech32`](crate::method::ClientMethod::NftIdToBech32) + /// - [`NftIdToBech32`](crate::method::UtilsMethod::NftIdToBech32) /// - [`HexPublicKeyToBech32Address`](crate::method::ClientMethod::HexPublicKeyToBech32Address) /// - [`HexToBech32`](crate::method::ClientMethod::HexToBech32) - /// - [`NftIdToBech32`](crate::method::ClientMethod::NftIdToBech32) /// - [`ImplicitAccountCreationAddress`](crate::method::WalletMethod::ImplicitAccountCreationAddress) Bech32Address(Bech32Address), /// - [`Faucet`](crate::method::ClientMethod::RequestFundsFromFaucet) diff --git a/bindings/nodejs/lib/client/client.ts b/bindings/nodejs/lib/client/client.ts index 86b5983084..f98f54b005 100644 --- a/bindings/nodejs/lib/client/client.ts +++ b/bindings/nodejs/lib/client/client.ts @@ -709,6 +709,28 @@ export class Client { return JSON.parse(response).payload; } + /** + * Transforms an anchor id to a bech32 encoded address. + * + * @param anchorId An anchor ID. + * @param bech32Hrp The Bech32 HRP (human readable part) to be used. + * @returns The corresponding Bech32 address. + */ + async anchorIdToBech32( + anchorId: AnchorId, + bech32Hrp?: string, + ): Promise { + const response = await this.methodHandler.callMethod({ + name: 'anchorIdToBech32', + data: { + anchorId, + bech32Hrp, + }, + }); + + return JSON.parse(response).payload; + } + /** * Convert an NFT ID to a Bech32 encoded address. * diff --git a/bindings/nodejs/lib/types/client/bridge/client.ts b/bindings/nodejs/lib/types/client/bridge/client.ts index 5bd79f7de2..f70d672fe0 100644 --- a/bindings/nodejs/lib/types/client/bridge/client.ts +++ b/bindings/nodejs/lib/types/client/bridge/client.ts @@ -279,6 +279,14 @@ export interface __AccountIdToBech32Method__ { }; } +export interface __AnchorIdToBech32Method__ { + name: 'anchorIdToBech32'; + data: { + anchorId: AnchorId; + bech32Hrp?: string; + }; +} + export interface __NftIdToBech32Method__ { name: 'nftIdToBech32'; data: { diff --git a/bindings/nodejs/lib/types/client/bridge/index.ts b/bindings/nodejs/lib/types/client/bridge/index.ts index 743c428eda..0f6c75ccff 100644 --- a/bindings/nodejs/lib/types/client/bridge/index.ts +++ b/bindings/nodejs/lib/types/client/bridge/index.ts @@ -39,6 +39,7 @@ import type { __GetUtxoChangesFullByIndexMethod__, __HexToBech32Method__, __AccountIdToBech32Method__, + __AnchorIdToBech32Method__, __NftIdToBech32Method__, __HexPublicKeyToBech32AddressMethod__, __AccountOutputIdsMethod__, @@ -105,6 +106,7 @@ export type __ClientMethods__ = | __GetUtxoChangesFullByIndexMethod__ | __HexToBech32Method__ | __AccountIdToBech32Method__ + | __AnchorIdToBech32Method__ | __NftIdToBech32Method__ | __HexPublicKeyToBech32AddressMethod__ | __AccountOutputIdsMethod__ diff --git a/bindings/nodejs/lib/types/utils/bridge/index.ts b/bindings/nodejs/lib/types/utils/bridge/index.ts index 31804d2b14..61d2a6b093 100644 --- a/bindings/nodejs/lib/types/utils/bridge/index.ts +++ b/bindings/nodejs/lib/types/utils/bridge/index.ts @@ -13,6 +13,7 @@ import type { __Bech32ToHexMethod__, __HexToBech32Method__, __AccountIdToBech32Method__, + __AnchorIdToBech32Method__, __NftIdToBech32Method__, __HexPublicKeyToBech32AddressMethod__, __IsAddressValidMethod__, @@ -48,6 +49,7 @@ export type __UtilsMethods__ = | __Bech32ToHexMethod__ | __HexToBech32Method__ | __AccountIdToBech32Method__ + | __AnchorIdToBech32Method__ | __NftIdToBech32Method__ | __HexPublicKeyToBech32AddressMethod__ | __IsAddressValidMethod__ diff --git a/bindings/nodejs/lib/types/utils/bridge/utils.ts b/bindings/nodejs/lib/types/utils/bridge/utils.ts index 8360aab79c..6660fccd85 100644 --- a/bindings/nodejs/lib/types/utils/bridge/utils.ts +++ b/bindings/nodejs/lib/types/utils/bridge/utils.ts @@ -14,7 +14,7 @@ import { Bech32Address, Unlock, } from '../../'; -import { AccountId } from '../../block/id'; +import { AccountId, AnchorId } from '../../block/id'; import { SlotCommitment } from '../../block/slot'; import { InputSigningData } from '../../client'; import { NumericString } from '../numeric'; @@ -123,6 +123,14 @@ export interface __AccountIdToBech32Method__ { }; } +export interface __AnchorIdToBech32Method__ { + name: 'anchorIdToBech32'; + data: { + anchorId: AnchorId; + bech32Hrp: string; + }; +} + export interface __NftIdToBech32Method__ { name: 'nftIdToBech32'; data: { diff --git a/bindings/nodejs/lib/utils/utils.ts b/bindings/nodejs/lib/utils/utils.ts index 881d4133b5..b92bfa3f80 100644 --- a/bindings/nodejs/lib/utils/utils.ts +++ b/bindings/nodejs/lib/utils/utils.ts @@ -24,6 +24,7 @@ import { } from '../types'; import { AccountId, + AnchorId, BlockId, FoundryId, NftId, @@ -313,6 +314,26 @@ export class Utils { }); } + /** + * Transforms an anchor id to a bech32 encoded address. + * + * @param anchorId An anchor ID. + * @param bech32Hrp The Bech32 HRP (human readable part) to use. + * @returns The Bech32-encoded address string. + */ + static anchorIdToBech32( + anchorId: AnchorId, + bech32Hrp: string, + ): Bech32Address { + return callUtilsMethod({ + name: 'anchorIdToBech32', + data: { + anchorId, + bech32Hrp, + }, + }); + } + /** * Convert an NFT ID to a Bech32-encoded address string. * diff --git a/bindings/python/iota_sdk/client/_utils.py b/bindings/python/iota_sdk/client/_utils.py index b9a956d2a1..efbf93bb45 100644 --- a/bindings/python/iota_sdk/client/_utils.py +++ b/bindings/python/iota_sdk/client/_utils.py @@ -36,7 +36,7 @@ def _call_method(self, name, data=None): """ # pylint: disable=redefined-builtin - def hex_to_bech32(self, hex_str: HexStr, bech32_hrp: str) -> str: + def hex_to_bech32(self, hex_str: HexStr, bech32_hrp: Optional[str] = None) -> str: """Transforms a hex encoded address to a bech32 encoded address. """ return self._call_method('hexToBech32', { @@ -44,7 +44,7 @@ def hex_to_bech32(self, hex_str: HexStr, bech32_hrp: str) -> str: 'bech32Hrp': bech32_hrp }) - def account_id_to_bech32(self, account_id: HexStr, bech32_hrp: str) -> str: + def account_id_to_bech32(self, account_id: HexStr, bech32_hrp: Optional[str] = None) -> str: """Transforms an account id to a bech32 encoded address. """ return self._call_method('accountIdToBech32', { @@ -52,7 +52,15 @@ def account_id_to_bech32(self, account_id: HexStr, bech32_hrp: str) -> str: 'bech32Hrp': bech32_hrp }) - def nft_id_to_bech32(self, nft_id: HexStr, bech32_hrp: str) -> str: + def anchor_id_to_bech32(self, anchor_id: HexStr, bech32_hrp: Optional[str] = None) -> str: + """Transforms an anchor id to a bech32 encoded address. + """ + return self._call_method('anchorIdToBech32', { + 'anchorId': anchor_id, + 'bech32Hrp': bech32_hrp + }) + + def nft_id_to_bech32(self, nft_id: HexStr, bech32_hrp: Optional[str] = None) -> str: """Transforms an nft id to a bech32 encoded address. """ return self._call_method('nftIdToBech32', { diff --git a/bindings/python/iota_sdk/utils.py b/bindings/python/iota_sdk/utils.py index 4c54fa24bb..60ce58ec9e 100644 --- a/bindings/python/iota_sdk/utils.py +++ b/bindings/python/iota_sdk/utils.py @@ -58,6 +58,15 @@ def account_id_to_bech32(account_id: HexStr, bech32_hrp: str) -> str: 'bech32Hrp': bech32_hrp }) + @staticmethod + def anchor_id_to_bech32(anchor_id: HexStr, bech32_hrp: str) -> str: + """Convert an anchor id to a Bech32 encoded address. + """ + return _call_method('anchorIdToBech32', { + 'anchorId': anchor_id, + 'bech32Hrp': bech32_hrp + }) + @staticmethod def nft_id_to_bech32(nft_id: HexStr, bech32_hrp: str) -> str: """Convert an NFT ID to a Bech32 encoded address. diff --git a/bindings/python/tests/test_api_responses.py b/bindings/python/tests/test_api_responses.py index 5a041fd28f..3b5fab3a18 100644 --- a/bindings/python/tests/test_api_responses.py +++ b/bindings/python/tests/test_api_responses.py @@ -17,7 +17,8 @@ def test_api_response(cls_type: Generic[T], path: str): fixture = load(payload) cls = cls_type.from_dict(fixture) - # We need to sort the keys because optional fields in classes must be last in Python + # We need to sort the keys because optional fields in classes must be + # last in Python fixture_str = dumps(fixture, sort_keys=True) recreated = dumps( loads(cls.to_json()), sort_keys=True) diff --git a/sdk/src/client/utils.rs b/sdk/src/client/utils.rs index 00805b66bb..4dd8ec509d 100644 --- a/sdk/src/client/utils.rs +++ b/sdk/src/client/utils.rs @@ -19,7 +19,7 @@ use crate::{ client::{Error, Result}, types::block::{ address::{Address, Bech32Address, Ed25519Address, Hrp, ToBech32Ext}, - output::{AccountId, NftId}, + output::{AccountId, AnchorId, NftId}, payload::TaggedDataPayload, Block, BlockId, }, @@ -129,6 +129,18 @@ impl ClientInner { } } + /// Transforms an anchor id to a bech32 encoded address + pub async fn anchor_id_to_bech32( + &self, + anchor_id: AnchorId, + bech32_hrp: Option>, + ) -> crate::client::Result { + match bech32_hrp { + Some(hrp) => Ok(anchor_id.to_bech32(hrp.convert()?)), + None => Ok(anchor_id.to_bech32(self.get_bech32_hrp().await?)), + } + } + /// Transforms an nft id to a bech32 encoded address pub async fn nft_id_to_bech32( &self, From 6b3e64f2952dc8d3dcb34e8ec3f747bea7d6da69 Mon Sep 17 00:00:00 2001 From: /alex/ Date: Wed, 21 Feb 2024 17:11:21 +0100 Subject: [PATCH 4/4] Improve `WalletBuilder` (#1941) * rename wallet data * remove awaits and update signatures * make it compile * rename more wallet data mentions * first impl * make it compile * unmut wallet * remove mut * remove more mut * update save load test * clean up * one more mut * refactor * error handling * fix tests * temporarily disable test * re-include test * review * some nits * revert backup rename; remove a function * revert restore rename * visibility; rename * set alias * ) Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * doc Co-authored-by: Thibault Martinez * doc fixes * fix * get rid of the enum * little refactor and CI fix * fix todo * fix test * nits * clean up * nice up * only verify ed25519 address at first wallet creation * review * fix comment Co-authored-by: Thibault Martinez * suggestions Co-authored-by: Thibault Martinez Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Co-authored-by: Thibault Martinez Co-authored-by: Thibault Martinez --- bindings/core/src/lib.rs | 12 +- bindings/core/tests/secrets_debug.rs | 2 +- cli/src/cli.rs | 11 +- .../offline_signing/1_prepare_transaction.rs | 2 +- sdk/examples/wallet/spammer.rs | 2 +- sdk/src/wallet/core/builder.rs | 180 ++++++++++-------- sdk/src/wallet/error.rs | 3 + sdk/tests/wallet/common/mod.rs | 11 +- sdk/tests/wallet/core.rs | 8 +- 9 files changed, 129 insertions(+), 102 deletions(-) diff --git a/bindings/core/src/lib.rs b/bindings/core/src/lib.rs index d7dc366cfa..bba53b0465 100644 --- a/bindings/core/src/lib.rs +++ b/bindings/core/src/lib.rs @@ -45,9 +45,9 @@ pub fn init_logger(config: String) -> std::result::Result<(), fern_logger::Error #[serde(rename_all = "camelCase")] pub struct WalletOptions { pub address: Option, - pub alias: Option, #[serde(with = "option_bip44", default)] pub bip_path: Option, + pub alias: Option, pub client_options: Option, #[derivative(Debug(format_with = "OmittedDebug::omitted_fmt"))] pub secret_manager: Option, @@ -60,13 +60,13 @@ impl WalletOptions { self } - pub fn with_alias(mut self, alias: impl Into>) -> Self { - self.alias = alias.into(); + pub fn with_bip_path(mut self, bip_path: impl Into>) -> Self { + self.bip_path = bip_path.into(); self } - pub fn with_bip_path(mut self, bip_path: impl Into>) -> Self { - self.bip_path = bip_path.into(); + pub fn with_alias(mut self, alias: impl Into>) -> Self { + self.alias = alias.into(); self } @@ -90,8 +90,8 @@ impl WalletOptions { log::debug!("wallet options: {self:?}"); let mut builder = Wallet::builder() .with_address(self.address) - .with_alias(self.alias) .with_bip_path(self.bip_path) + .with_alias(self.alias) .with_client_options(self.client_options); #[cfg(feature = "storage")] diff --git a/bindings/core/tests/secrets_debug.rs b/bindings/core/tests/secrets_debug.rs index 6f57d1eba5..84bf98556e 100644 --- a/bindings/core/tests/secrets_debug.rs +++ b/bindings/core/tests/secrets_debug.rs @@ -26,6 +26,6 @@ fn method_interface_secrets_debug() { let wallet_options = WalletOptions::default().with_secret_manager(SecretManagerDto::Placeholder); assert_eq!( format!("{:?}", wallet_options), - "WalletOptions { address: None, alias: None, bip_path: None, client_options: None, secret_manager: Some(), storage_path: None }" + "WalletOptions { address: None, bip_path: None, alias: None, client_options: None, secret_manager: Some(), storage_path: None }" ); } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 3527f10227..a0d52a0168 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -411,17 +411,18 @@ pub async fn init_command( } else { None }; - let address = init_params - .address - .map(|addr| Bech32Address::from_str(&addr)) - .transpose()?; Ok(Wallet::builder() .with_secret_manager(secret_manager) .with_client_options(ClientOptions::new().with_node(init_params.node_url.as_str())?) .with_storage_path(storage_path.to_str().expect("invalid unicode")) + .with_address( + init_params + .address + .map(|addr| Bech32Address::from_str(&addr)) + .transpose()?, + ) .with_bip_path(init_params.bip_path) - .with_address(address) .with_alias(alias) .finish() .await?) diff --git a/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs b/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs index 67738e46eb..0a1f2b29b8 100644 --- a/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs +++ b/sdk/examples/wallet/offline_signing/1_prepare_transaction.rs @@ -44,8 +44,8 @@ async fn main() -> Result<()> { .with_secret_manager(SecretManager::Placeholder) .with_storage_path(ONLINE_WALLET_DB_PATH) .with_client_options(client_options.clone()) - .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .with_address(address) + .with_bip_path(Bip44::new(SHIMMER_COIN_TYPE)) .finish() .await?; diff --git a/sdk/examples/wallet/spammer.rs b/sdk/examples/wallet/spammer.rs index 9accfd388d..931c6962db 100644 --- a/sdk/examples/wallet/spammer.rs +++ b/sdk/examples/wallet/spammer.rs @@ -60,8 +60,8 @@ async fn main() -> Result<()> { let wallet = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(secret_manager)) .with_client_options(client_options) - .with_bip_path(bip_path) .with_address(address) + .with_bip_path(bip_path) .finish() .await?; diff --git a/sdk/src/wallet/core/builder.rs b/sdk/src/wallet/core/builder.rs index 214cde1722..7ad9f10fc6 100644 --- a/sdk/src/wallet/core/builder.rs +++ b/sdk/src/wallet/core/builder.rs @@ -17,7 +17,7 @@ use crate::wallet::storage::adapter::memory::Memory; use crate::wallet::storage::{StorageManager, StorageOptions}; use crate::{ client::secret::{GenerateAddressOptions, SecretManage, SecretManager}, - types::block::address::{Address, Bech32Address}, + types::block::address::{Bech32Address, Ed25519Address}, wallet::{ core::{operations::background_syncing::BackgroundSyncStatus, Bip44, WalletInner, WalletLedger}, operations::syncing::SyncOptions, @@ -29,8 +29,8 @@ use crate::{ #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct WalletBuilder { - pub(crate) bip_path: Option, pub(crate) address: Option, + pub(crate) bip_path: Option, pub(crate) alias: Option, pub(crate) client_options: Option, #[cfg(feature = "storage")] @@ -42,8 +42,8 @@ pub struct WalletBuilder { impl Default for WalletBuilder { fn default() -> Self { Self { - bip_path: Default::default(), address: Default::default(), + bip_path: Default::default(), alias: Default::default(), client_options: Default::default(), #[cfg(feature = "storage")] @@ -59,21 +59,18 @@ where { /// Initialises a new instance of the wallet builder with the default storage adapter. pub fn new() -> Self { - Self { - secret_manager: None, - ..Default::default() - } + Self::default() } - /// Set the BIP44 path of the wallet. - pub fn with_bip_path(mut self, bip_path: impl Into>) -> Self { - self.bip_path = bip_path.into(); + /// Set the address of the wallet. + pub fn with_address(mut self, address: impl Into>) -> Self { + self.address = address.into(); self } - /// Set the wallet address. - pub fn with_address(mut self, address: impl Into>) -> Self { - self.address = address.into(); + /// Set the BIP44 path of the wallet. + pub fn with_bip_path(mut self, bip_path: impl Into>) -> Self { + self.bip_path = bip_path.into(); self } @@ -178,46 +175,78 @@ where self.secret_manager = secret_manager; } - let restored_bip_path = loaded_wallet_builder.as_ref().and_then(|builder| builder.bip_path); + let mut verify_address = false; + let loaded_address = loaded_wallet_builder + .as_ref() + .and_then(|builder| builder.address.clone()); + + // May use a previously stored address if it wasn't provided + if let Some(address) = &self.address { + if let Some(loaded_address) = &loaded_address { + if address != loaded_address { + return Err(crate::wallet::Error::WalletAddressMismatch(address.clone())); + } + } else { + verify_address = true; + } + } else { + self.address = loaded_address; + } + + let loaded_bip_path = loaded_wallet_builder.as_ref().and_then(|builder| builder.bip_path); // May use a previously stored BIP path if it wasn't provided if let Some(bip_path) = self.bip_path { - if let Some(restored_bip_path) = restored_bip_path { - if bip_path != restored_bip_path { + if let Some(loaded_bip_path) = loaded_bip_path { + if bip_path != loaded_bip_path { return Err(crate::wallet::Error::BipPathMismatch { new_bip_path: Some(bip_path), - old_bip_path: Some(restored_bip_path), + old_bip_path: Some(loaded_bip_path), }); } + } else { + verify_address = true; } } else { - self.bip_path = restored_bip_path; + self.bip_path = loaded_bip_path; } + // Create the node client. + let client = self + .client_options + .clone() + .ok_or(crate::wallet::Error::MissingParameter("client_options"))? + .finish() + .await?; + + match (self.address.as_ref(), self.bip_path.as_ref()) { + (Some(address), Some(bip_path)) => { + if verify_address { + // verify that the address is derived from the provided bip path. + if let Some(backing_ed25519_address) = address.inner.backing_ed25519() { + self.verify_ed25519_address(backing_ed25519_address, bip_path).await?; + } else { + return Err(crate::wallet::Error::InvalidParameter("address/bip_path mismatch")); + } + } + } + (Some(_address), None) => {} + (None, Some(bip_path)) => { + self.address.replace(Bech32Address::new( + client.get_bech32_hrp().await?, + self.generate_ed25519_address(bip_path).await?, + )); + } + (None, None) => { + return Err(crate::wallet::Error::MissingParameter("address or bip_path")); + } + }; + // May use a previously stored wallet alias if it wasn't provided if self.alias.is_none() { self.alias = loaded_wallet_builder.as_ref().and_then(|builder| builder.alias.clone()); } - // May use a previously stored wallet address if it wasn't provided - if self.address.is_none() { - self.address = loaded_wallet_builder - .as_ref() - .and_then(|builder| builder.address.clone()); - } - - // May create a default Ed25519 wallet address if there's a secret manager. - if self.address.is_none() { - if self.secret_manager.is_some() { - let address = self.create_default_wallet_address().await?; - self.address = Some(address); - } else { - return Err(crate::wallet::Error::MissingParameter("address")); - } - } - // Panic: can be safely unwrapped now - let wallet_address = self.address.as_ref().unwrap().clone(); - #[cfg(feature = "storage")] let mut wallet_ledger = storage_manager.load_wallet_ledger().await?; @@ -232,14 +261,6 @@ where unlock_unused_inputs(wallet_ledger)?; } - // Create the node client. - let client = self - .client_options - .clone() - .ok_or(crate::wallet::Error::MissingParameter("client_options"))? - .finish() - .await?; - let background_syncing_status = tokio::sync::watch::channel(BackgroundSyncStatus::Stopped); let background_syncing_status = (Arc::new(background_syncing_status.0), background_syncing_status.1); @@ -263,7 +284,8 @@ where let wallet_ledger = WalletLedger::default(); let wallet = Wallet { - address: Arc::new(RwLock::new(wallet_address)), + // Panic: it's invalid to build a wallet without an address. + address: Arc::new(RwLock::new(self.address.unwrap())), bip_path: Arc::new(RwLock::new(self.bip_path)), alias: Arc::new(RwLock::new(self.alias)), inner: Arc::new(wallet_inner), @@ -279,41 +301,6 @@ where Ok(wallet) } - /// Generate the wallet address. - /// - /// Note: make sure to only call it after `self.secret_manager` and `self.bip_path` has been set. - pub(crate) async fn create_default_wallet_address(&self) -> crate::wallet::Result { - let bech32_hrp = self - .client_options - .as_ref() - .unwrap() - .network_info - .protocol_parameters - .bech32_hrp; - let bip_path = self.bip_path.as_ref().unwrap(); - - Ok(Bech32Address::new( - bech32_hrp, - Address::Ed25519( - self.secret_manager - .as_ref() - .unwrap() - .read() - .await - .generate_ed25519_addresses( - bip_path.coin_type, - bip_path.account, - bip_path.address_index..bip_path.address_index + 1, - GenerateAddressOptions { - internal: bip_path.change != 0, - ledger_nano_prompt: false, - }, - ) - .await?[0], - ), - )) - } - #[cfg(feature = "storage")] pub(crate) async fn from_wallet(wallet: &Wallet) -> Self { Self { @@ -325,6 +312,37 @@ where secret_manager: Some(wallet.secret_manager.clone()), } } + + #[inline(always)] + async fn verify_ed25519_address( + &self, + ed25519_address: &Ed25519Address, + bip_path: &Bip44, + ) -> crate::wallet::Result<()> { + (ed25519_address == &self.generate_ed25519_address(bip_path).await?) + .then_some(()) + .ok_or(crate::wallet::Error::InvalidParameter("address/bip_path mismatch")) + } + + async fn generate_ed25519_address(&self, bip_path: &Bip44) -> crate::wallet::Result { + if let Some(secret_manager) = &self.secret_manager { + let secret_manager = &*secret_manager.read().await; + Ok(secret_manager + .generate_ed25519_addresses( + bip_path.coin_type, + bip_path.account, + bip_path.address_index..bip_path.address_index + 1, + GenerateAddressOptions { + internal: bip_path.change != 0, + ledger_nano_prompt: false, + }, + ) + // Panic: if it didn't return an Err, then there must be at least one address + .await?[0]) + } else { + Err(crate::wallet::Error::MissingParameter("secret_manager")) + } + } } // Check if any of the locked inputs is not used in a transaction and unlock them, so they get available for new diff --git a/sdk/src/wallet/error.rs b/sdk/src/wallet/error.rs index 2cd452ab7e..201a66bf52 100644 --- a/sdk/src/wallet/error.rs +++ b/sdk/src/wallet/error.rs @@ -56,6 +56,9 @@ pub enum Error { /// Invalid output kind. #[error("invalid output kind: {0}")] InvalidOutputKind(String), + /// Invalid parameter. + #[error("invalid parameter: {0}")] + InvalidParameter(&'static str), /// Invalid Voting Power #[cfg(feature = "participation")] #[cfg_attr(docsrs, doc(cfg(feature = "participation")))] diff --git a/sdk/tests/wallet/common/mod.rs b/sdk/tests/wallet/common/mod.rs index e73a4b2174..7575c6a6c0 100644 --- a/sdk/tests/wallet/common/mod.rs +++ b/sdk/tests/wallet/common/mod.rs @@ -29,10 +29,15 @@ pub use self::constants::*; /// /// A Wallet #[allow(dead_code, unused_variables)] -pub(crate) async fn make_wallet(storage_path: &str, mnemonic: Option, node: Option<&str>) -> Result { +pub(crate) async fn make_wallet( + storage_path: &str, + mnemonic: impl Into>, + node: Option<&str>, +) -> Result { let client_options = ClientOptions::new().with_node(node.unwrap_or(NODE_LOCAL))?; - let secret_manager = - MnemonicSecretManager::try_from_mnemonic(mnemonic.unwrap_or_else(|| Client::generate_mnemonic().unwrap()))?; + let secret_manager = MnemonicSecretManager::try_from_mnemonic( + mnemonic.into().unwrap_or_else(|| Client::generate_mnemonic().unwrap()), + )?; #[allow(unused_mut)] let mut wallet_builder = Wallet::builder() diff --git a/sdk/tests/wallet/core.rs b/sdk/tests/wallet/core.rs index be49f758aa..b651655747 100644 --- a/sdk/tests/wallet/core.rs +++ b/sdk/tests/wallet/core.rs @@ -30,7 +30,8 @@ async fn update_client_options() -> Result<()> { let storage_path = "test-storage/update_client_options"; setup(storage_path)?; - let wallet = make_wallet(storage_path, None, Some(NODE_OTHER)).await?; + let mnemonic = Mnemonic::from(DEFAULT_MNEMONIC.to_owned()); + let wallet = make_wallet(storage_path, mnemonic.clone(), Some(NODE_OTHER)).await?; let node_dto_old = NodeDto::Node(Node::from(Url::parse(NODE_OTHER).unwrap())); let node_dto_new = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); @@ -49,7 +50,7 @@ async fn update_client_options() -> Result<()> { // The client options are also updated in the database and available the next time drop(wallet); - let wallet = make_wallet(storage_path, None, None).await?; + let wallet = make_wallet(storage_path, mnemonic, None).await?; let client_options = wallet.client_options().await; assert!(client_options.node_manager_builder.nodes.contains(&node_dto_new)); assert!(!client_options.node_manager_builder.nodes.contains(&node_dto_old)); @@ -83,8 +84,7 @@ async fn changed_bip_path() -> Result<()> { setup(storage_path)?; let mnemonic = Mnemonic::from(DEFAULT_MNEMONIC.to_owned()); - - let wallet = make_wallet(storage_path, Some(mnemonic.clone()), None).await?; + let wallet = make_wallet(storage_path, mnemonic.clone(), None).await?; drop(wallet);