diff --git a/bindings/core/src/method_handler/wallet.rs b/bindings/core/src/method_handler/wallet.rs index 17979f26e6..bc53e77ff4 100644 --- a/bindings/core/src/method_handler/wallet.rs +++ b/bindings/core/src/method_handler/wallet.rs @@ -18,7 +18,7 @@ use crate::{method::WalletMethod, response::Response}; /// Call a wallet method. pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletMethod) -> crate::Result { let response = match method { - WalletMethod::Accounts => Response::OutputsData(wallet.data().await.accounts().cloned().collect()), + WalletMethod::Accounts => Response::OutputsData(wallet.ledger().await.accounts().cloned().collect()), #[cfg(feature = "stronghold")] WalletMethod::Backup { destination, password } => { wallet.backup(destination, password).await?; @@ -144,17 +144,14 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM // wallet.deregister_participation_event(&event_id).await?; // Response::Ok // } - WalletMethod::GetAddress => { - let address = wallet.address().await; - Response::Address(address) - } + WalletMethod::GetAddress => Response::Address(wallet.address().await), WalletMethod::GetBalance => Response::Balance(wallet.balance().await?), WalletMethod::GetFoundryOutput { token_id } => { let output = wallet.get_foundry_output(token_id).await?; Response::Output(output) } WalletMethod::GetIncomingTransaction { transaction_id } => wallet - .data() + .ledger() .await .get_incoming_transaction(&transaction_id) .map_or_else( @@ -162,7 +159,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM |transaction| Response::Transaction(Some(Box::new(TransactionWithMetadataDto::from(transaction)))), ), WalletMethod::GetOutput { output_id } => { - Response::OutputData(wallet.data().await.get_output(&output_id).cloned().map(Box::new)) + Response::OutputData(wallet.ledger().await.get_output(&output_id).cloned().map(Box::new)) } // #[cfg(feature = "participation")] // WalletMethod::GetParticipationEvent { event_id } => { @@ -191,7 +188,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM // } WalletMethod::GetTransaction { transaction_id } => Response::Transaction( wallet - .data() + .ledger() .await .get_transaction(&transaction_id) .map(TransactionWithMetadataDto::from) @@ -227,11 +224,11 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM Response::PreparedTransaction(data) } WalletMethod::ImplicitAccounts => { - Response::OutputsData(wallet.data().await.implicit_accounts().cloned().collect()) + Response::OutputsData(wallet.ledger().await.implicit_accounts().cloned().collect()) } WalletMethod::IncomingTransactions => Response::Transactions( wallet - .data() + .ledger() .await .incoming_transactions() .values() @@ -239,16 +236,16 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM .collect(), ), WalletMethod::Outputs { filter_options } => { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; Response::OutputsData(if let Some(filter) = filter_options { - wallet_data.filtered_outputs(filter).cloned().collect() + wallet_ledger.filtered_outputs(filter).cloned().collect() } else { - wallet_data.outputs().values().cloned().collect() + wallet_ledger.outputs().values().cloned().collect() }) } WalletMethod::PendingTransactions => Response::Transactions( wallet - .data() + .ledger() .await .pending_transactions() .map(TransactionWithMetadataDto::from) @@ -444,7 +441,7 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM WalletMethod::Sync { options } => Response::Balance(wallet.sync(options).await?), WalletMethod::Transactions => Response::Transactions( wallet - .data() + .ledger() .await .transactions() .values() @@ -452,11 +449,11 @@ pub(crate) async fn call_wallet_method_internal(wallet: &Wallet, method: WalletM .collect(), ), WalletMethod::UnspentOutputs { filter_options } => { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; Response::OutputsData(if let Some(filter) = filter_options { - wallet_data.filtered_unspent_outputs(filter).cloned().collect() + wallet_ledger.filtered_unspent_outputs(filter).cloned().collect() } else { - wallet_data.unspent_outputs().values().cloned().collect() + wallet_ledger.unspent_outputs().values().cloned().collect() }) } }; diff --git a/bindings/nodejs/examples/client/11-build-output.ts b/bindings/nodejs/examples/client/11-build-output.ts index fb44f9e71f..187d4dc64b 100644 --- a/bindings/nodejs/examples/client/11-build-output.ts +++ b/bindings/nodejs/examples/client/11-build-output.ts @@ -55,7 +55,9 @@ async function run() { const basicOutputWithMetadata = await client.buildBasicOutput({ amount: BigInt(1000000), unlockConditions: [addressUnlockCondition], - features: [new MetadataFeature(utf8ToHex('Hello World!'))], + features: [ + new MetadataFeature({ data: utf8ToHex('Hello World!') }), + ], }); console.log(JSON.stringify(basicOutputWithMetadata, null, 2)); diff --git a/bindings/nodejs/examples/client/13-build-account-output.ts b/bindings/nodejs/examples/client/13-build-account-output.ts index f1dfcf2992..0b37a76302 100644 --- a/bindings/nodejs/examples/client/13-build-account-output.ts +++ b/bindings/nodejs/examples/client/13-build-account-output.ts @@ -44,11 +44,11 @@ async function run() { ], features: [ new SenderFeature(new Ed25519Address(hexAddress)), - new MetadataFeature(utf8ToHex('hello')), + new MetadataFeature({ data: utf8ToHex('hello') }), ], immutableFeatures: [ new IssuerFeature(new Ed25519Address(hexAddress)), - new MetadataFeature(utf8ToHex('hello')), + new MetadataFeature({ data: utf8ToHex('hello') }), ], }); diff --git a/bindings/nodejs/examples/client/15-build-nft-output.ts b/bindings/nodejs/examples/client/15-build-nft-output.ts index 1cd9ab3f80..494695a5d7 100644 --- a/bindings/nodejs/examples/client/15-build-nft-output.ts +++ b/bindings/nodejs/examples/client/15-build-nft-output.ts @@ -56,7 +56,9 @@ async function run() { ], features: [ new SenderFeature(new Ed25519Address(hexAddress)), - new MetadataFeature(utf8ToHex('mutable metadata')), + new MetadataFeature({ + data: utf8ToHex('mutable metadata'), + }), new TagFeature(utf8ToHex('my tag')), ], }); diff --git a/bindings/nodejs/examples/how_tos/outputs/features.ts b/bindings/nodejs/examples/how_tos/outputs/features.ts index c6bb77e9ac..996261c13b 100644 --- a/bindings/nodejs/examples/how_tos/outputs/features.ts +++ b/bindings/nodejs/examples/how_tos/outputs/features.ts @@ -53,7 +53,9 @@ async function run() { const nftOutputWithMetadata = await client.buildNftOutput({ nftId: '0x0000000000000000000000000000000000000000000000000000000000000000', unlockConditions: [addressUnlockCondition], - features: [new MetadataFeature(utf8ToHex('Hello, World!'))], + features: [ + new MetadataFeature({ data: utf8ToHex('Hello, World!') }), + ], }); // Output with immutable metadata feature @@ -61,7 +63,7 @@ async function run() { nftId: '0x0000000000000000000000000000000000000000000000000000000000000000', unlockConditions: [addressUnlockCondition], immutableFeatures: [ - new MetadataFeature(utf8ToHex('Hello, World!')), + new MetadataFeature({ data: utf8ToHex('Hello, World!') }), ], }); diff --git a/bindings/nodejs/lib/types/block/output/feature.ts b/bindings/nodejs/lib/types/block/output/feature.ts index 1d56e6d08a..1f1e1ade19 100644 --- a/bindings/nodejs/lib/types/block/output/feature.ts +++ b/bindings/nodejs/lib/types/block/output/feature.ts @@ -13,6 +13,11 @@ import { EpochIndex } from '../../block/slot'; import { NativeToken } from '../../models/native-token'; import { HexEncodedString } from '../../utils/hex-encoding'; +/** + * Printable ASCII characters. + */ +export declare type PrintableASCII = string; + /** * All of the feature block types. */ @@ -88,14 +93,30 @@ class IssuerFeature extends Feature { */ class MetadataFeature extends Feature { /** Defines metadata (arbitrary binary data) that will be stored in the output. */ - readonly data: string; + readonly entries: { [key: PrintableASCII]: HexEncodedString }; /** - * @param data The metadata stored with the feature. + * @param entries The metadata stored with the feature. */ - constructor(data: string) { + constructor(entries: { [key: PrintableASCII]: HexEncodedString }) { super(FeatureType.Metadata); - this.data = data; + this.entries = entries; + } +} + +/** + * A Metadata Feature that can only be changed by the State Controller. + */ +class StateMetadataFeature extends Feature { + /** Defines metadata (arbitrary binary data) that will be stored in the output. */ + readonly entries: { [key: PrintableASCII]: HexEncodedString }; + + /** + * @param entries The metadata stored with the feature. + */ + constructor(entries: { [key: PrintableASCII]: HexEncodedString }) { + super(FeatureType.StateMetadata); + this.entries = entries; } } @@ -213,6 +234,7 @@ const FeatureDiscriminator = { { value: SenderFeature, name: FeatureType.Sender as any }, { value: IssuerFeature, name: FeatureType.Issuer as any }, { value: MetadataFeature, name: FeatureType.Metadata as any }, + { value: StateMetadataFeature, name: FeatureType.StateMetadata as any }, { value: TagFeature, name: FeatureType.Tag as any }, { value: NativeTokenFeature, name: FeatureType.NativeToken as any }, { value: BlockIssuerFeature, name: FeatureType.BlockIssuer as any }, @@ -227,6 +249,7 @@ export { SenderFeature, IssuerFeature, MetadataFeature, + StateMetadataFeature, TagFeature, NativeTokenFeature, BlockIssuerFeature, diff --git a/bindings/nodejs/lib/types/block/output/irc-27.ts b/bindings/nodejs/lib/types/block/output/irc-27.ts index 19825e3197..86f2cf20c9 100644 --- a/bindings/nodejs/lib/types/block/output/irc-27.ts +++ b/bindings/nodejs/lib/types/block/output/irc-27.ts @@ -88,7 +88,7 @@ class Irc27Metadata { } asFeature(): MetadataFeature { - return new MetadataFeature(this.asHex()); + return new MetadataFeature({ 'irc-27': this.asHex() }); } } diff --git a/bindings/nodejs/lib/types/block/output/irc-30.ts b/bindings/nodejs/lib/types/block/output/irc-30.ts index daba7224a7..ea9e40868d 100644 --- a/bindings/nodejs/lib/types/block/output/irc-30.ts +++ b/bindings/nodejs/lib/types/block/output/irc-30.ts @@ -61,7 +61,7 @@ class Irc30Metadata { } asFeature(): MetadataFeature { - return new MetadataFeature(this.asHex()); + return new MetadataFeature({ 'irc-30': this.asHex() }); } } diff --git a/bindings/nodejs/lib/types/wallet/transaction-options.ts b/bindings/nodejs/lib/types/wallet/transaction-options.ts index 5ce1e5b2a9..cbe3f7385b 100644 --- a/bindings/nodejs/lib/types/wallet/transaction-options.ts +++ b/bindings/nodejs/lib/types/wallet/transaction-options.ts @@ -4,6 +4,7 @@ import { AccountAddress, AccountId, + Address, Bech32Address, ContextInput, OutputId, @@ -11,7 +12,6 @@ import { import { TaggedDataPayload } from '../block/payload/tagged'; import { Burn } from '../client'; import { u256, HexEncodedString, NumericString, u64 } from '../utils'; -import { Bip44Address } from './address'; /** Options for creating a transaction. */ export interface TransactionOptions { @@ -56,7 +56,7 @@ export type ReuseAddress = { export type CustomAddress = { /** The name of the strategy. */ strategy: 'CustomAddress'; - value: Bip44Address; + value: Address; }; /** Options for creating Native Tokens. */ diff --git a/bindings/python/examples/client/build_account.py b/bindings/python/examples/client/build_account.py index 95efa4f88d..8a0132a40a 100644 --- a/bindings/python/examples/client/build_account.py +++ b/bindings/python/examples/client/build_account.py @@ -24,11 +24,11 @@ ] features = [ SenderFeature(Ed25519Address(hexAddress)), - MetadataFeature(utf8_to_hex('Hello, World!')) + MetadataFeature({'data': utf8_to_hex('Hello, World!')}) ] immutable_features = [ IssuerFeature(Ed25519Address(hexAddress)), - MetadataFeature(utf8_to_hex('Hello, World!')) + MetadataFeature({'data': utf8_to_hex('Hello, World!')}) ] # Build account output diff --git a/bindings/python/examples/client/build_basic.py b/bindings/python/examples/client/build_basic.py index 455ec49084..8f6916ba67 100644 --- a/bindings/python/examples/client/build_basic.py +++ b/bindings/python/examples/client/build_basic.py @@ -37,7 +37,7 @@ address_unlock_condition, ], features=[ - MetadataFeature(utf8_to_hex('Hello, World!')) + MetadataFeature({'data': utf8_to_hex('Hello, World!')}) ], amount=1000000, ) diff --git a/bindings/python/examples/how_tos/outputs/features.py b/bindings/python/examples/how_tos/outputs/features.py index c8fc009baa..463d1b8c03 100644 --- a/bindings/python/examples/how_tos/outputs/features.py +++ b/bindings/python/examples/how_tos/outputs/features.py @@ -57,7 +57,7 @@ address_unlock_condition, ], features=[ - MetadataFeature(utf8_to_hex('Hello, World!')) + MetadataFeature({'data': utf8_to_hex('Hello, World!')}) ], ) outputs.append(nft_output) @@ -69,7 +69,7 @@ address_unlock_condition, ], immutable_features=[ - MetadataFeature(utf8_to_hex('Hello, World!')) + MetadataFeature({'data': utf8_to_hex('Hello, World!')}) ], ) outputs.append(nft_output) diff --git a/bindings/python/iota_sdk/types/balance.py b/bindings/python/iota_sdk/types/balance.py index 1b9a3d4a35..baeae1d460 100644 --- a/bindings/python/iota_sdk/types/balance.py +++ b/bindings/python/iota_sdk/types/balance.py @@ -18,7 +18,7 @@ class BaseCoinBalance: Attributes: total: The total balance. available: The available amount of the total balance. - voting_power: The voting power of the wallet. + # voting_power: The voting power of the wallet. """ total: int = field(metadata=config( encoder=str @@ -26,9 +26,10 @@ class BaseCoinBalance: available: int = field(metadata=config( encoder=str )) - voting_power: int = field(metadata=config( - encoder=str - )) + # TODO https://github.com/iotaledger/iota-sdk/issues/1822 + # voting_power: int = field(metadata=config( + # encoder=str + # )) @json @@ -39,9 +40,13 @@ class ManaBalance: Attributes: total: The total balance. available: The available amount of the total balance. + rewards: Mana rewards of account and delegation outputs. """ total: DecayedMana available: DecayedMana + rewards: int = field(metadata=config( + encoder=str + )) @json diff --git a/bindings/python/iota_sdk/types/feature.py b/bindings/python/iota_sdk/types/feature.py index 5dc4cfb5f5..bb7f6cf78b 100644 --- a/bindings/python/iota_sdk/types/feature.py +++ b/bindings/python/iota_sdk/types/feature.py @@ -81,6 +81,20 @@ class MetadataFeature: entries: Dict[str, HexStr] +@json +@dataclass +class StateMetadataFeature: + """A Metadata Feature that can only be changed by the State Controller. + Attributes: + entries: A key-value map where the keys are graphic ASCII strings and the values hex-encoded binary data. + """ + type: int = field( + default_factory=lambda: int( + FeatureType.StateMetadata), + init=False) + entries: Dict[str, HexStr] + + @json @dataclass class TagFeature: @@ -149,7 +163,7 @@ class StakingFeature: Feature: TypeAlias = Union[SenderFeature, IssuerFeature, - MetadataFeature, TagFeature, NativeTokenFeature, BlockIssuerFeature, StakingFeature] + MetadataFeature, StateMetadataFeature, TagFeature, NativeTokenFeature, BlockIssuerFeature, StakingFeature] # pylint: disable=too-many-return-statements @@ -167,6 +181,8 @@ def deserialize_feature(d: Dict[str, Any]) -> Feature: return IssuerFeature.from_dict(d) if feature_type == FeatureType.Metadata: return MetadataFeature.from_dict(d) + if feature_type == FeatureType.StateMetadata: + return StateMetadataFeature.from_dict(d) if feature_type == FeatureType.Tag: return TagFeature.from_dict(d) if feature_type == FeatureType.NativeToken: diff --git a/bindings/python/iota_sdk/types/irc_27.py b/bindings/python/iota_sdk/types/irc_27.py index e66de06bb1..3ebed849ed 100644 --- a/bindings/python/iota_sdk/types/irc_27.py +++ b/bindings/python/iota_sdk/types/irc_27.py @@ -70,4 +70,4 @@ def as_hex(self): def as_feature(self): """Turns this schema into a MetadataFeature type """ - MetadataFeature(self.as_hex()) + MetadataFeature({'irc-27': self.as_hex()}) diff --git a/bindings/python/iota_sdk/types/irc_30.py b/bindings/python/iota_sdk/types/irc_30.py index 3c3341f435..afa3166df6 100644 --- a/bindings/python/iota_sdk/types/irc_30.py +++ b/bindings/python/iota_sdk/types/irc_30.py @@ -47,4 +47,4 @@ def as_hex(self): def as_feature(self): """Turns this schema into a MetadataFeature type """ - MetadataFeature(self.as_hex()) + MetadataFeature({'irc-30': self.as_hex()}) diff --git a/bindings/python/iota_sdk/types/node_info.py b/bindings/python/iota_sdk/types/node_info.py index 4875cf80a3..a54aaf386e 100644 --- a/bindings/python/iota_sdk/types/node_info.py +++ b/bindings/python/iota_sdk/types/node_info.py @@ -279,6 +279,7 @@ class ProtocolParameters: bech32_hrp: str storage_score_parameters: StorageScoreParameters work_score_parameters: WorkScoreParameters + mana_parameters: ManaParameters token_supply: int = field(metadata=config( encoder=str )) @@ -288,7 +289,6 @@ class ProtocolParameters: )) slot_duration_in_seconds: int slots_per_epoch_exponent: int - mana_parameters: ManaParameters staking_unbonding_period: int validation_blocks_per_slot: int punishment_epochs: int diff --git a/bindings/python/iota_sdk/types/transaction_options.py b/bindings/python/iota_sdk/types/transaction_options.py index 9ae1002a2a..8aedbc7283 100644 --- a/bindings/python/iota_sdk/types/transaction_options.py +++ b/bindings/python/iota_sdk/types/transaction_options.py @@ -3,7 +3,8 @@ from enum import Enum from typing import Optional, List, Union -from dataclasses import dataclass +from dataclasses import dataclass, field +from iota_sdk.types.address import Address from iota_sdk.types.burn import Burn from iota_sdk.types.common import HexStr, json from iota_sdk.types.context_input import ContextInput @@ -17,30 +18,10 @@ class RemainderValueStrategyCustomAddress: """Remainder value strategy for custom addresses. Attributes: - address: An address to move the remainder value to. - key_index: The address key index. - internal: Determines if an address is a public or an internal (change) address. - used: Indicates whether an address has been used already. + value: An address to move the remainder value to. """ - - address: str - key_index: int - internal: bool - used: bool - - def to_dict(self) -> dict: - """Custom dict conversion. - """ - - return { - 'strategy': 'CustomAddress', - 'value': { - 'address': self.address, - 'keyIndex': self.key_index, - 'internal': self.internal, - 'used': self.used - } - } + strategy: str = field(default_factory=lambda: 'CustomAddress', init=False) + value: Address class RemainderValueStrategy(Enum): @@ -57,7 +38,6 @@ def to_dict(self) -> dict: return { 'strategy': self.name, - 'value': self.value[0] } @@ -79,28 +59,15 @@ class TransactionOptions: mana_allotments: Mana allotments for the transaction. issuer_id: Optional block issuer to which the transaction will have required mana allotted. """ - - def __init__(self, remainder_value_strategy: Optional[Union[RemainderValueStrategy, RemainderValueStrategyCustomAddress]] = None, - tagged_data_payload: Optional[TaggedDataPayload] = None, - context_inputs: Optional[List[ContextInput]] = None, - required_inputs: Optional[List[OutputId]] = None, - burn: Optional[Burn] = None, - note: Optional[str] = None, - allow_micro_amount: Optional[bool] = None, - allow_additional_input_selection: Optional[bool] = None, - capabilities: Optional[HexStr] = None, - mana_allotments: Optional[dict[HexStr, int]] = None, - issuer_id: Optional[HexStr] = None): - """Initialize transaction options. - """ - self.remainder_value_strategy = remainder_value_strategy - self.tagged_data_payload = tagged_data_payload - self.context_inputs = context_inputs - self.required_inputs = required_inputs - self.burn = burn - self.note = note - self.allow_micro_amount = allow_micro_amount - self.allow_additional_input_selection = allow_additional_input_selection - self.capabilities = capabilities - self.mana_allotments = mana_allotments - self.issuer_id = issuer_id + remainder_value_strategy: Optional[Union[RemainderValueStrategy, + RemainderValueStrategyCustomAddress]] = None + tagged_data_payload: Optional[TaggedDataPayload] = None + context_inputs: Optional[List[ContextInput]] = None + required_inputs: Optional[List[OutputId]] = None + burn: Optional[Burn] = None + note: Optional[str] = None + allow_micro_amount: Optional[bool] = None + allow_additional_input_selection: Optional[bool] = None + capabilities: Optional[HexStr] = None + mana_allotments: Optional[dict[HexStr, int]] = None + issuer_id: Optional[HexStr] = None diff --git a/bindings/python/tests/test_api_responses.py b/bindings/python/tests/test_api_responses.py new file mode 100644 index 0000000000..e7250c0c06 --- /dev/null +++ b/bindings/python/tests/test_api_responses.py @@ -0,0 +1,91 @@ +# Copyright 2024 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +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 + + +base_path = '../../sdk/tests/types/api/fixtures/' +T = TypeVar("T") + + +def test_api_responses(): + def test_api_response(cls_type: Generic[T], path: str): + fixture_str = '' + with open(base_path + path, "r", encoding="utf-8") as payload: + 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 + fixture_str = dumps(fixture, sort_keys=True) + recreated = dumps( + loads(cls.to_json()), sort_keys=True) + assert fixture_str == recreated + + # GET /api/routes + test_api_response(RoutesResponse, "get-routes-response-example.json") + # GET /api/core/v3/info + # TODO: enable when the fixture is updated https://github.com/iotaledger/iota-sdk/issues/2015 + # test_api_response(InfoResponse, "get-info-response-example.json") + # GET /api/core/v3/accounts/{bech32Address}/congestion + test_api_response(CongestionResponse, + "get-congestion-estimate-response-example.json") + # 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") + # GET /api/core/v3/validators/{bech32Address} + test_api_response(ValidatorResponse, "get-validator-example.json") + # GET /api/core/v3/committee + test_api_response(CommitteeResponse, "get-committee-example.json") + # GET /api/core/v3/blocks/issuance + test_api_response(IssuanceBlockHeaderResponse, + "get-buildingBlock-response-example.json") + # GET /api/core/v3/blocks/{blockId} + test_api_response(Block, "get-block-by-id-empty-response-example.json") + test_api_response(Block, "tagged-data-block-example.json") + test_api_response(Block, "transaction-block-example.json") + test_api_response( + Block, "get-block-by-id-validation-response-example.json") + # GET /api/core/v3/blocks/{blockId}/metadata + # TODO enable when Block and Tx State enums are fixed https://github.com/iotaledger/iota-sdk/issues/2019 + # test_api_response(BlockMetadataResponse, + # "get-block-by-id-response-example-new-transaction.json") + # test_api_response(BlockMetadataResponse, + # "get-block-by-id-response-example-new.json") + # test_api_response(BlockMetadataResponse, + # "get-block-by-id-response-example-confirmed-transaction.json") + # test_api_response(BlockMetadataResponse, + # "get-block-by-id-response-example-confirmed.json") + # test_api_response(BlockMetadataResponse, + # "get-block-by-id-response-example-conflicting-transaction.json") + # # GET /api/core/v3/blocks/{blockId}/full + # test_api_response(BlockWithMetadataResponse, + # "get-full-block-by-id-tagged-data-response-example.json") + # GET /api/core/v3/outputs/{outputId} + test_api_response( + OutputResponse, "get-outputs-by-id-response-example.json") + # GET /api/core/v3/outputs/{outputId}/metadata + # TODO: enable when https://github.com/iotaledger/iota-sdk/issues/2020 is fixed + # test_api_response( + # OutputMetadata, "get-output-metadata-by-id-response-unspent-example.json") + # test_api_response( + # OutputMetadata, "get-output-metadata-by-id-response-spent-example.json") + # GET /api/core/v3/outputs/{outputId}/full + # TODO: enable when OutputWithMetadata is updated with OutputIdProof https://github.com/iotaledger/iota-sdk/issues/2021 + # test_api_response(OutputWithMetadata, + # "get-full-output-metadata-example.json") + # GET /api/core/v3/transactions/{transactionId}/metadata + # TODO enable when Tx State enum is fixed https://github.com/iotaledger/iota-sdk/issues/2019 + # test_api_response(TransactionMetadataResponse, + # "get-transaction-metadata-by-id-response-example.json") + # GET /api/core/v3/commitments/{commitmentId} + test_api_response(SlotCommitment, "get-commitment-response-example.json") + # GET /api/core/v3/commitments/{commitmentId}/utxo-changes + # TODO: enable when https://github.com/iotaledger/iota-sdk/issues/2020 is fixed + # test_api_response(UtxoChangesResponse, + # "get-utxo-changes-response-example.json") + # GET /api/core/v3/commitments/{commitmentId}/utxo-changes/full + # test_api_response(UtxoChangesFullResponse, + # "get-utxo-changes-full-response-example.json") diff --git a/cli/src/cli.rs b/cli/src/cli.rs index c4a5f6c8c2..3527f10227 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -497,7 +497,7 @@ pub async fn restore_command_stronghold( if let Err(e) = wallet.restore_backup(backup_path.into(), password, None, None).await { // Clean up the file system after a failed restore (typically produces a wallet without a secret manager). // TODO: a better way would be to not create any files/dirs in the first place when it's not clear yet whether - // the restore will be successful. + // the restore will be successful. https://github.com/iotaledger/iota-sdk/issues/2018 if storage_path.is_dir() && !restore_into_existing_wallet { std::fs::remove_dir_all(storage_path)?; } diff --git a/cli/src/wallet_cli/mod.rs b/cli/src/wallet_cli/mod.rs index 49fdc8853c..5b7ff66755 100644 --- a/cli/src/wallet_cli/mod.rs +++ b/cli/src/wallet_cli/mod.rs @@ -391,8 +391,8 @@ impl FromStr for OutputSelector { // `accounts` command pub async fn accounts_command(wallet: &Wallet) -> Result<(), Error> { - let wallet_data = wallet.data().await; - let accounts = wallet_data.accounts(); + let wallet_ledger = wallet.ledger().await; + let accounts = wallet_ledger.accounts(); let hrp = wallet.client().get_bech32_hrp().await?; println_log_info!("Accounts:\n"); @@ -430,9 +430,9 @@ pub async fn address_command(wallet: &Wallet) -> Result<(), Error> { // `allot-mana` command pub async fn allot_mana_command(wallet: &Wallet, mana: u64, account_id: Option) -> Result<(), Error> { let account_id = { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; account_id - .or_else(|| wallet_data.first_account_id()) + .or_else(|| wallet_ledger.first_account_id()) .ok_or(WalletError::AccountNotFound)? }; @@ -565,9 +565,9 @@ pub async fn claim_command(wallet: &Wallet, output_id: Option) -> Resu /// `claimable-outputs` command pub async fn claimable_outputs_command(wallet: &Wallet) -> Result<(), Error> { for output_id in wallet.claimable_outputs(OutputsToClaim::All).await? { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; // Unwrap: for the iterated `OutputId`s this call will always return `Some(...)`. - let output = &wallet_data.get_output(&output_id).unwrap().output; + let output = &wallet_ledger.get_output(&output_id).unwrap().output; let kind = match output { Output::Nft(_) => "Nft", Output::Basic(_) => "Basic", @@ -613,9 +613,9 @@ pub async fn congestion_command( work_score: Option, ) -> Result<(), Error> { let account_id = { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; account_id - .or_else(|| wallet_data.first_account_id()) + .or_else(|| wallet_ledger.first_account_id()) .ok_or(WalletError::AccountNotFound)? }; @@ -868,8 +868,8 @@ pub async fn implicit_account_transition_command(wallet: &Wallet, output_id: Out // `implicit-accounts` command pub async fn implicit_accounts_command(wallet: &Wallet) -> Result<(), Error> { - let wallet_data = wallet.data().await; - let implicit_accounts = wallet_data.implicit_accounts(); + let wallet_ledger = wallet.ledger().await; + let implicit_accounts = wallet_ledger.implicit_accounts(); let hrp = wallet.client().get_bech32_hrp().await?; println_log_info!("Implicit accounts:\n"); @@ -974,11 +974,11 @@ pub async fn node_info_command(wallet: &Wallet) -> Result<(), Error> { /// `output` command pub async fn output_command(wallet: &Wallet, selector: OutputSelector, metadata: bool) -> Result<(), Error> { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; let output = match selector { - OutputSelector::Id(id) => wallet_data.get_output(&id), + OutputSelector::Id(id) => wallet_ledger.get_output(&id), OutputSelector::Index(index) => { - let mut outputs = wallet_data.outputs().values().collect::>(); + let mut outputs = wallet_ledger.outputs().values().collect::>(); outputs.sort_unstable_by_key(|o| o.output_id); outputs.into_iter().nth(index) } @@ -999,7 +999,7 @@ pub async fn output_command(wallet: &Wallet, selector: OutputSelector, metadata: /// `outputs` command pub async fn outputs_command(wallet: &Wallet) -> Result<(), Error> { - print_outputs(wallet.data().await.outputs().values().cloned().collect(), "Outputs:") + print_outputs(wallet.ledger().await.outputs().values().cloned().collect(), "Outputs:") } // `send` command @@ -1104,11 +1104,11 @@ pub async fn sync_command(wallet: &Wallet) -> Result<(), Error> { /// `transaction` command pub async fn transaction_command(wallet: &Wallet, selector: TransactionSelector) -> Result<(), Error> { - let wallet_data = wallet.data().await; + let wallet_ledger = wallet.ledger().await; let transaction = match selector { - TransactionSelector::Id(id) => wallet_data.get_transaction(&id), + TransactionSelector::Id(id) => wallet_ledger.get_transaction(&id), TransactionSelector::Index(index) => { - let mut transactions = wallet_data.transactions().values().collect::>(); + let mut transactions = wallet_ledger.transactions().values().collect::>(); transactions.sort_unstable_by(|a, b| b.timestamp.cmp(&a.timestamp)); transactions.into_iter().nth(index) } @@ -1125,8 +1125,8 @@ pub async fn transaction_command(wallet: &Wallet, selector: TransactionSelector) /// `transactions` command pub async fn transactions_command(wallet: &Wallet, show_details: bool) -> Result<(), Error> { - let wallet_data = wallet.data().await; - let mut transactions = wallet_data.transactions().values().collect::>(); + let wallet_ledger = wallet.ledger().await; + let mut transactions = wallet_ledger.transactions().values().collect::>(); transactions.sort_unstable_by(|a, b| b.timestamp.cmp(&a.timestamp)); if transactions.is_empty() { @@ -1150,7 +1150,7 @@ pub async fn transactions_command(wallet: &Wallet, show_details: bool) -> Result /// `unspent-outputs` command pub async fn unspent_outputs_command(wallet: &Wallet) -> Result<(), Error> { print_outputs( - wallet.data().await.unspent_outputs().values().cloned().collect(), + wallet.ledger().await.unspent_outputs().values().cloned().collect(), "Unspent outputs:", ) } @@ -1253,7 +1253,7 @@ async fn print_wallet_address(wallet: &Wallet) -> Result<(), Error> { let mut delegations = Vec::new(); let mut anchors = Vec::new(); - for output_data in wallet.data().await.unspent_outputs().values() { + for output_data in wallet.ledger().await.unspent_outputs().values() { let output_id = output_data.output_id; output_ids.push(output_id); diff --git a/sdk/examples/how_tos/wallet/consolidate_outputs.rs b/sdk/examples/how_tos/wallet/consolidate_outputs.rs index 8b9df45733..f630c99161 100644 --- a/sdk/examples/how_tos/wallet/consolidate_outputs.rs +++ b/sdk/examples/how_tos/wallet/consolidate_outputs.rs @@ -49,7 +49,7 @@ async fn main() -> Result<()> { // output. println!("Outputs BEFORE consolidation:"); wallet - .data() + .ledger() .await .unspent_outputs() .values() @@ -89,7 +89,7 @@ async fn main() -> Result<()> { // Outputs after consolidation println!("Outputs AFTER consolidation:"); wallet - .data() + .ledger() .await .unspent_outputs() .values() diff --git a/sdk/examples/how_tos/wallet/create_wallet.rs b/sdk/examples/how_tos/wallet/create_wallet.rs index 8d9acb5a92..ca14347a08 100644 --- a/sdk/examples/how_tos/wallet/create_wallet.rs +++ b/sdk/examples/how_tos/wallet/create_wallet.rs @@ -48,7 +48,7 @@ async fn main() -> Result<()> { let client_options = ClientOptions::new().with_node(&std::env::var("NODE_URL").unwrap())?; // Create the wallet - let wallet = Wallet::builder() + Wallet::builder() .with_secret_manager(SecretManager::Stronghold(secret_manager)) .with_storage_path(&std::env::var("WALLET_DB_PATH").unwrap()) .with_client_options(client_options) diff --git a/sdk/examples/how_tos/wallet/list_outputs.rs b/sdk/examples/how_tos/wallet/list_outputs.rs index fc5a77bde9..111d3b9989 100644 --- a/sdk/examples/how_tos/wallet/list_outputs.rs +++ b/sdk/examples/how_tos/wallet/list_outputs.rs @@ -29,13 +29,13 @@ async fn main() -> Result<()> { // Print output ids println!("Output ids:"); - for output in wallet.data().await.outputs().values() { + for output in wallet.ledger().await.outputs().values() { println!("{}", output.output_id); } // Print unspent output ids println!("Unspent output ids:"); - for output in wallet.data().await.unspent_outputs().values() { + for output in wallet.ledger().await.unspent_outputs().values() { println!("{}", output.output_id); } diff --git a/sdk/examples/how_tos/wallet/list_transactions.rs b/sdk/examples/how_tos/wallet/list_transactions.rs index fea3b5685c..f2566a7e70 100644 --- a/sdk/examples/how_tos/wallet/list_transactions.rs +++ b/sdk/examples/how_tos/wallet/list_transactions.rs @@ -37,13 +37,13 @@ async fn main() -> Result<()> { // Print transaction ids println!("Sent transactions:"); - for transaction_id in wallet.data().await.transactions().keys() { + for transaction_id in wallet.ledger().await.transactions().keys() { println!("{}", transaction_id); } // Print received transaction ids println!("Received transactions:"); - for transaction_id in wallet.data().await.incoming_transactions().keys() { + for transaction_id in wallet.ledger().await.incoming_transactions().keys() { println!("{}", transaction_id); } diff --git a/sdk/examples/wallet/spammer.rs b/sdk/examples/wallet/spammer.rs index 25f0ece4d4..9accfd388d 100644 --- a/sdk/examples/wallet/spammer.rs +++ b/sdk/examples/wallet/spammer.rs @@ -74,7 +74,7 @@ async fn main() -> Result<()> { // We make sure that for all threads there are always inputs available to // fund the transaction, otherwise we create enough unspent outputs. let num_unspent_basic_outputs_with_send_amount = wallet - .data() + .ledger() .await .filtered_unspent_outputs(FilterOptions { output_types: Some(vec![BasicOutput::KIND]), diff --git a/sdk/examples/wallet/storage.rs b/sdk/examples/wallet/storage.rs index c901a155e3..cd22c6db89 100644 --- a/sdk/examples/wallet/storage.rs +++ b/sdk/examples/wallet/storage.rs @@ -49,7 +49,6 @@ async fn main() -> Result<()> { } async fn sync_print_balance(wallet: &Wallet) -> Result<()> { - let alias = wallet.alias().await; let now = tokio::time::Instant::now(); let balance = wallet.sync(None).await?; println!("Wallet synced in: {:.2?}", now.elapsed()); diff --git a/sdk/examples/wallet/wallet.rs b/sdk/examples/wallet/wallet.rs index a9d8bae81e..87de681822 100644 --- a/sdk/examples/wallet/wallet.rs +++ b/sdk/examples/wallet/wallet.rs @@ -65,11 +65,6 @@ async fn create_wallet() -> Result { .await } -async fn print_address(wallet: &Wallet) -> Result<()> { - println!("Wallet address: {}", wallet.address().await); - Ok(()) -} - async fn sync_print_balance(wallet: &Wallet, full_report: bool) -> Result<()> { let now = tokio::time::Instant::now(); let balance = wallet.sync(None).await?; diff --git a/sdk/src/client/api/block_builder/input_selection/mod.rs b/sdk/src/client/api/block_builder/input_selection/mod.rs index 5a827c72d7..b00e5dbd4e 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -202,8 +202,8 @@ impl InputSelection { if !OUTPUT_COUNT_RANGE.contains(&(self.provided_outputs.len() as u16)) { // If burn or mana allotments are provided, outputs will be added later, in the other cases it will just // create remainder outputs. - if !(self.provided_outputs.is_empty() - && (self.burn.is_some() || !self.mana_allotments.is_empty() || !self.required_inputs.is_empty())) + if !self.provided_outputs.is_empty() + || self.burn.is_none() && self.mana_allotments.is_empty() && self.required_inputs.is_empty() { return Err(Error::InvalidOutputCount(self.provided_outputs.len())); } diff --git a/sdk/src/client/api/block_builder/input_selection/remainder.rs b/sdk/src/client/api/block_builder/input_selection/remainder.rs index 1e9442ac7a..c2cfcc4d67 100644 --- a/sdk/src/client/api/block_builder/input_selection/remainder.rs +++ b/sdk/src/client/api/block_builder/input_selection/remainder.rs @@ -142,15 +142,13 @@ impl InputSelection { .ok_or(Error::MissingInputWithEd25519Address)?; // If there is a mana remainder, try to fit it in an existing output - if input_mana > output_mana { - if self.output_for_added_mana_exists(&remainder_address) { - log::debug!("Allocating {mana_diff} excess input mana for output with address {remainder_address}"); - self.remainders.added_mana = std::mem::take(&mut mana_diff); - // If we have no other remainders, we are done - if input_amount == output_amount && native_tokens_diff.is_none() { - log::debug!("No more remainder required"); - return Ok((storage_deposit_returns, Vec::new())); - } + if input_mana > output_mana && self.output_for_added_mana_exists(&remainder_address) { + log::debug!("Allocating {mana_diff} excess input mana for output with address {remainder_address}"); + self.remainders.added_mana = std::mem::take(&mut mana_diff); + // If we have no other remainders, we are done + if input_amount == output_amount && native_tokens_diff.is_none() { + log::debug!("No more remainder required"); + return Ok((storage_deposit_returns, Vec::new())); } } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs b/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs index 95e4ae81f0..646954d0af 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/mana.rs @@ -101,10 +101,8 @@ impl InputSelection { } self.reduce_account_output()?; - } else { - if !self.requirements.contains(&Requirement::Mana) { - self.requirements.push(Requirement::Mana); - } + } else if !self.requirements.contains(&Requirement::Mana) { + self.requirements.push(Requirement::Mana); } // Remainders can only be calculated when the input mana is >= the output mana diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 3df3d1711c..7f1089e13a 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -473,8 +473,7 @@ impl AnchorOutput { } else if next_state.state_index == current_state.state_index { // Governance transition. if current_state.amount != next_state.amount - // TODO https://github.com/iotaledger/iota-sdk/issues/1650 - // || current_state.state_metadata != next_state.state_metadata + || current_state.features().state_metadata() != next_state.features().state_metadata() { return Err(TransactionFailureReason::AnchorInvalidGovernanceTransition); } diff --git a/sdk/src/types/block/output/feature/state_metadata.rs b/sdk/src/types/block/output/feature/state_metadata.rs index de5c806d05..5bb8a3bd6c 100644 --- a/sdk/src/types/block/output/feature/state_metadata.rs +++ b/sdk/src/types/block/output/feature/state_metadata.rs @@ -23,7 +23,7 @@ use super::{ }; use crate::types::block::{output::StorageScore, protocol::WorkScore, Error}; -/// Defines metadata, arbitrary binary data, that will be stored in the output. +/// A Metadata Feature that can only be changed by the State Controller. #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct StateMetadataFeature(pub(crate) MetadataBTreeMapPrefix); diff --git a/sdk/src/types/block/protocol/mod.rs b/sdk/src/types/block/protocol/mod.rs index f167ca14e9..c9ddae04cb 100644 --- a/sdk/src/types/block/protocol/mod.rs +++ b/sdk/src/types/block/protocol/mod.rs @@ -273,6 +273,11 @@ impl ProtocolParameters { slot_commitment_id.past_bounded_slot(self.max_committable_age()) } + /// Calculates the past bounded epoch for the given slot of the SlotCommitment. + pub fn past_bounded_epoch(&self, slot_commitment_id: SlotCommitmentId) -> EpochIndex { + self.epoch_index_of(self.past_bounded_slot(slot_commitment_id)) + } + /// Calculates the future bounded slot for the given slot of the SlotCommitment. /// Given any slot index of a commitment input, the result of this function is a slot index /// that is at most equal to the slot of the block in which it was issued, or lower. @@ -282,6 +287,11 @@ impl ProtocolParameters { slot_commitment_id.future_bounded_slot(self.min_committable_age()) } + /// Calculates the future bounded epoch for the given slot of the SlotCommitment. + pub fn future_bounded_epoch(&self, slot_commitment_id: SlotCommitmentId) -> EpochIndex { + self.epoch_index_of(self.future_bounded_slot(slot_commitment_id)) + } + /// Returns the slot at the end of which the validator and delegator registration ends and the voting power /// for the epoch is calculated. pub fn registration_slot(&self, epoch_index: EpochIndex) -> SlotIndex { @@ -294,29 +304,29 @@ impl ProtocolParameters { /// Gets the start epoch for a delegation with the given slot commitment id. pub fn delegation_start_epoch(&self, slot_commitment_id: SlotCommitmentId) -> EpochIndex { - let past_bounded_slot_index = self.past_bounded_slot(slot_commitment_id); - let past_bounded_epoch_index = self.epoch_index_of(past_bounded_slot_index); + let past_bounded_slot = self.past_bounded_slot(slot_commitment_id); + let past_bounded_epoch = self.epoch_index_of(past_bounded_slot); - let registration_slot = self.registration_slot(past_bounded_epoch_index + 1); + let registration_slot = self.registration_slot(past_bounded_epoch + 1); - if past_bounded_slot_index <= registration_slot { - past_bounded_epoch_index + 1 + if past_bounded_slot <= registration_slot { + past_bounded_epoch + 1 } else { - past_bounded_epoch_index + 2 + past_bounded_epoch + 2 } } /// Gets the end epoch for a delegation with the given slot commitment id pub fn delegation_end_epoch(&self, slot_commitment_id: SlotCommitmentId) -> EpochIndex { - let future_bounded_slot_index = self.future_bounded_slot(slot_commitment_id); - let future_bounded_epoch_index = self.epoch_index_of(future_bounded_slot_index); + let future_bounded_slot = self.future_bounded_slot(slot_commitment_id); + let future_bounded_epoch = self.epoch_index_of(future_bounded_slot); - let registration_slot = self.registration_slot(future_bounded_epoch_index + 1); + let registration_slot = self.registration_slot(future_bounded_epoch + 1); - if future_bounded_slot_index <= registration_slot { - future_bounded_epoch_index + if future_bounded_slot <= registration_slot { + future_bounded_epoch } else { - future_bounded_epoch_index + 1 + future_bounded_epoch + 1 } } diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index 5475267949..3191a3af6c 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -160,6 +160,9 @@ impl<'a> SemanticValidationContext<'a> { )?) .ok_or(Error::ConsumedManaOverflow)?; } + if output.features().staking().is_some() && self.commitment_context_input.is_none() { + return Ok(Some(TransactionFailureReason::StakingCommitmentInputMissing)); + } (output.amount(), None, output.unlock_conditions()) } @@ -303,6 +306,14 @@ impl<'a> SemanticValidationContext<'a> { .ok_or(Error::CreatedManaOverflow)?; } } + if output.features().staking().is_some() { + if self.commitment_context_input.is_none() { + return Ok(Some(TransactionFailureReason::StakingCommitmentInputMissing)); + } + if output.features().block_issuer().is_none() { + return Ok(Some(TransactionFailureReason::StakingBlockIssuerFeatureMissing)); + } + } (output.amount(), output.mana(), None, Some(output.features())) } @@ -323,11 +334,11 @@ impl<'a> SemanticValidationContext<'a> { if let Address::Account(account_address) = address.address() { if let Some(entry) = self.block_issuer_mana.get_mut(account_address.account_id()) { if let Some(commitment_context_input) = self.commitment_context_input { - let past_bounded_slot_index = + let past_bounded_slot = self.protocol_parameters.past_bounded_slot(commitment_context_input); if timelock.slot_index() - >= past_bounded_slot_index + self.protocol_parameters.max_committable_age() + >= past_bounded_slot + self.protocol_parameters.max_committable_age() { entry.1 = entry .1 diff --git a/sdk/src/types/block/semantic/state_transition.rs b/sdk/src/types/block/semantic/state_transition.rs index d5d440c589..17f4b56e4e 100644 --- a/sdk/src/types/block/semantic/state_transition.rs +++ b/sdk/src/types/block/semantic/state_transition.rs @@ -151,14 +151,26 @@ impl StateTransitionVerifier for AccountOutput { } if let Some(block_issuer) = next_state.features().block_issuer() { - let past_bounded_slot_index = context + let past_bounded_slot = context .protocol_parameters .past_bounded_slot(context.commitment_context_input.unwrap()); - if block_issuer.expiry_slot() < past_bounded_slot_index { + if block_issuer.expiry_slot() < past_bounded_slot { return Err(TransactionFailureReason::BlockIssuerExpiryTooEarly); } } + if let Some(staking) = next_state.features().staking() { + let past_bounded_epoch = context + .protocol_parameters + .past_bounded_epoch(context.commitment_context_input.unwrap()); + + if staking.start_epoch() != past_bounded_epoch { + return Err(TransactionFailureReason::StakingStartEpochInvalid); + } + if staking.end_epoch() < past_bounded_epoch + context.protocol_parameters.staking_unbonding_period { + return Err(TransactionFailureReason::StakingEndEpochTooEarly); + } + } if let Some(issuer) = next_state.immutable_features().issuer() { if !context.unlocked_addresses.contains(issuer.address()) { @@ -170,7 +182,7 @@ impl StateTransitionVerifier for AccountOutput { } fn transition( - _current_output_id: &OutputId, + current_output_id: &OutputId, current_state: &Self, _next_output_id: &OutputId, next_state: &Self, @@ -181,11 +193,11 @@ impl StateTransitionVerifier for AccountOutput { next_state.features().block_issuer(), ) { (None, Some(block_issuer_output)) => { - let past_bounded_slot_index = context + let past_bounded_slot = context .protocol_parameters .past_bounded_slot(context.commitment_context_input.unwrap()); - if block_issuer_output.expiry_slot() < past_bounded_slot_index { + if block_issuer_output.expiry_slot() < past_bounded_slot { return Err(TransactionFailureReason::BlockIssuerExpiryTooEarly); } } @@ -198,21 +210,85 @@ impl StateTransitionVerifier for AccountOutput { } (Some(block_issuer_input), Some(block_issuer_output)) => { let commitment_index = context.commitment_context_input.unwrap(); - let past_bounded_slot_index = context.protocol_parameters.past_bounded_slot(commitment_index); + let past_bounded_slot = context.protocol_parameters.past_bounded_slot(commitment_index); if block_issuer_input.expiry_slot() >= commitment_index.slot_index() { if block_issuer_input.expiry_slot() != block_issuer_output.expiry_slot() - && block_issuer_input.expiry_slot() < past_bounded_slot_index + && block_issuer_input.expiry_slot() < past_bounded_slot { return Err(TransactionFailureReason::BlockIssuerNotExpired); } - } else if block_issuer_output.expiry_slot() < past_bounded_slot_index { + } else if block_issuer_output.expiry_slot() < past_bounded_slot { return Err(TransactionFailureReason::BlockIssuerExpiryTooEarly); } } _ => {} } + match (current_state.features().staking(), next_state.features().staking()) { + (None, Some(staking_output)) => { + let past_bounded_epoch = context + .protocol_parameters + .past_bounded_epoch(context.commitment_context_input.unwrap()); + + if staking_output.start_epoch() != past_bounded_epoch { + return Err(TransactionFailureReason::StakingStartEpochInvalid); + } + if staking_output.end_epoch() + < past_bounded_epoch + context.protocol_parameters.staking_unbonding_period + { + return Err(TransactionFailureReason::StakingEndEpochTooEarly); + } + } + (Some(staking_input), None) => { + let future_bounded_epoch = context + .protocol_parameters + .future_bounded_epoch(context.commitment_context_input.unwrap()); + + if staking_input.end_epoch() >= future_bounded_epoch { + return Err(TransactionFailureReason::StakingFeatureRemovedBeforeUnbonding); + } else if !context.mana_rewards.contains_key(current_output_id) + || !context.reward_context_inputs.contains_key(current_output_id) + { + return Err(TransactionFailureReason::StakingRewardClaimingInvalid); + } + } + (Some(staking_input), Some(staking_output)) => { + let past_bounded_epoch = context + .protocol_parameters + .past_bounded_epoch(context.commitment_context_input.unwrap()); + let future_bounded_epoch = context + .protocol_parameters + .future_bounded_epoch(context.commitment_context_input.unwrap()); + + if staking_input.end_epoch() >= future_bounded_epoch { + if staking_input.staked_amount() != staking_output.staked_amount() + || staking_input.start_epoch() != staking_output.start_epoch() + || staking_input.fixed_cost() != staking_output.fixed_cost() + { + return Err(TransactionFailureReason::StakingFeatureModifiedBeforeUnbonding); + } + if staking_input.end_epoch() != staking_output.end_epoch() + && staking_input.end_epoch() + < past_bounded_epoch + context.protocol_parameters.staking_unbonding_period + { + return Err(TransactionFailureReason::StakingEndEpochTooEarly); + } + } else if (staking_input.staked_amount() != staking_output.staked_amount() + || staking_input.start_epoch() != staking_output.start_epoch() + || staking_input.fixed_cost() != staking_output.fixed_cost()) + && (staking_input.start_epoch() != past_bounded_epoch + || staking_input.end_epoch() + < past_bounded_epoch + context.protocol_parameters.staking_unbonding_period + || !context.mana_rewards.contains_key(current_output_id) + || !context.reward_context_inputs.contains_key(current_output_id)) + { + return Err(TransactionFailureReason::StakingRewardClaimingInvalid); + } + } + _ => {} + } + Self::transition_inner( current_state, next_state, @@ -222,7 +298,7 @@ impl StateTransitionVerifier for AccountOutput { } fn destruction( - _output_id: &OutputId, + output_id: &OutputId, current_state: &Self, context: &SemanticValidationContext<'_>, ) -> Result<(), TransactionFailureReason> { @@ -238,6 +314,19 @@ impl StateTransitionVerifier for AccountOutput { return Err(TransactionFailureReason::BlockIssuerNotExpired); } } + if let Some(staking) = current_state.features().staking() { + let future_bounded_epoch = context + .protocol_parameters + .future_bounded_epoch(context.commitment_context_input.unwrap()); + + if staking.end_epoch() >= future_bounded_epoch { + return Err(TransactionFailureReason::StakingFeatureRemovedBeforeUnbonding); + } else if !context.mana_rewards.contains_key(output_id) + || !context.reward_context_inputs.contains_key(output_id) + { + return Err(TransactionFailureReason::StakingRewardClaimingInvalid); + } + } Ok(()) } diff --git a/sdk/src/wallet/core/builder.rs b/sdk/src/wallet/core/builder.rs index ae8df31326..214cde1722 100644 --- a/sdk/src/wallet/core/builder.rs +++ b/sdk/src/wallet/core/builder.rs @@ -19,7 +19,7 @@ use crate::{ client::secret::{GenerateAddressOptions, SecretManage, SecretManager}, types::block::address::{Address, Bech32Address}, wallet::{ - core::{operations::background_syncing::BackgroundSyncStatus, Bip44, WalletData, WalletInner}, + core::{operations::background_syncing::BackgroundSyncStatus, Bip44, WalletInner, WalletLedger}, operations::syncing::SyncOptions, ClientOptions, Wallet, }, @@ -178,9 +178,20 @@ where self.secret_manager = secret_manager; } + let restored_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 self.bip_path.is_none() { - self.bip_path = loaded_wallet_builder.as_ref().and_then(|builder| builder.bip_path); + if let Some(bip_path) = self.bip_path { + if let Some(restored_bip_path) = restored_bip_path { + if bip_path != restored_bip_path { + return Err(crate::wallet::Error::BipPathMismatch { + new_bip_path: Some(bip_path), + old_bip_path: Some(restored_bip_path), + }); + } + } + } else { + self.bip_path = restored_bip_path; } // May use a previously stored wallet alias if it wasn't provided @@ -205,23 +216,10 @@ where } } // Panic: can be safely unwrapped now - let address = self.address.as_ref().unwrap().clone(); - - #[cfg(feature = "storage")] - let mut wallet_data = storage_manager.load_wallet_data().await?; + let wallet_address = self.address.as_ref().unwrap().clone(); - // The bip path must not change. #[cfg(feature = "storage")] - if let Some(wallet_data) = &wallet_data { - let new_bip_path = self.bip_path; - let old_bip_path = wallet_data.bip_path; - if new_bip_path != old_bip_path { - return Err(crate::wallet::Error::BipPathMismatch { - new_bip_path, - old_bip_path, - }); - } - } + let mut wallet_ledger = storage_manager.load_wallet_ledger().await?; // Store the wallet builder (for convenience reasons) #[cfg(feature = "storage")] @@ -230,8 +228,8 @@ where // It happened that inputs got locked, the transaction failed, but they weren't unlocked again, so we do this // here #[cfg(feature = "storage")] - if let Some(wallet_data) = &mut wallet_data { - unlock_unused_inputs(wallet_data)?; + if let Some(wallet_ledger) = &mut wallet_ledger { + unlock_unused_inputs(wallet_ledger)?; } // Create the node client. @@ -260,24 +258,30 @@ where storage_manager, }; #[cfg(feature = "storage")] - let wallet_data = wallet_data.unwrap_or_else(|| WalletData::new(self.bip_path, address, self.alias.clone())); + let wallet_ledger = wallet_ledger.unwrap_or_default(); #[cfg(not(feature = "storage"))] - let wallet_data = WalletData::new(self.bip_path, address, self.alias.clone()); + let wallet_ledger = WalletLedger::default(); + let wallet = Wallet { + address: Arc::new(RwLock::new(wallet_address)), + bip_path: Arc::new(RwLock::new(self.bip_path)), + alias: Arc::new(RwLock::new(self.alias)), inner: Arc::new(wallet_inner), - data: Arc::new(RwLock::new(wallet_data)), + ledger: Arc::new(RwLock::new(wallet_ledger)), }; // If the wallet builder is not set, it means the user provided it and we need to update the addresses. // In the other case it was loaded from the database and addresses are up to date. if provided_client_options { - wallet.update_bech32_hrp().await?; + wallet.update_address_hrp().await?; } 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 @@ -313,8 +317,8 @@ where #[cfg(feature = "storage")] pub(crate) async fn from_wallet(wallet: &Wallet) -> Self { Self { - bip_path: wallet.bip_path().await, address: Some(wallet.address().await), + bip_path: wallet.bip_path().await, alias: wallet.alias().await, client_options: Some(wallet.client_options().await), storage_options: Some(wallet.storage_options.clone()), @@ -326,17 +330,17 @@ where // Check if any of the locked inputs is not used in a transaction and unlock them, so they get available for new // transactions #[cfg(feature = "storage")] -fn unlock_unused_inputs(wallet_data: &mut WalletData) -> crate::wallet::Result<()> { +fn unlock_unused_inputs(wallet_ledger: &mut WalletLedger) -> crate::wallet::Result<()> { log::debug!("[unlock_unused_inputs]"); let mut used_inputs = HashSet::new(); - for transaction_id in &wallet_data.pending_transactions { - if let Some(tx) = wallet_data.transactions.get(transaction_id) { + for transaction_id in &wallet_ledger.pending_transactions { + if let Some(tx) = wallet_ledger.transactions.get(transaction_id) { for input in &tx.inputs { used_inputs.insert(*input.metadata.output_id()); } } } - wallet_data.locked_outputs.retain(|input| { + wallet_ledger.locked_outputs.retain(|input| { let used = used_inputs.contains(input); if !used { log::debug!("unlocking unused input {input}"); @@ -357,11 +361,11 @@ pub(crate) mod dto { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WalletBuilderDto { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) bip_path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub(crate) address: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) bip_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub(crate) alias: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub(crate) client_options: Option, @@ -373,8 +377,8 @@ pub(crate) mod dto { impl From for WalletBuilder { fn from(value: WalletBuilderDto) -> Self { Self { - bip_path: value.bip_path, address: value.address, + bip_path: value.bip_path, alias: value.alias, client_options: value.client_options, #[cfg(feature = "storage")] diff --git a/sdk/src/wallet/core/mod.rs b/sdk/src/wallet/core/mod.rs index 59f5b92e1d..30414fb3da 100644 --- a/sdk/src/wallet/core/mod.rs +++ b/sdk/src/wallet/core/mod.rs @@ -46,15 +46,21 @@ use crate::{ /// The stateful wallet used to interact with an IOTA network. #[derive(Debug)] pub struct Wallet { + pub(crate) address: Arc>, + pub(crate) bip_path: Arc>>, + pub(crate) alias: Arc>>, pub(crate) inner: Arc>, - pub(crate) data: Arc>, + pub(crate) ledger: Arc>, } impl Clone for Wallet { fn clone(&self) -> Self { Self { + address: self.address.clone(), + bip_path: self.bip_path.clone(), + alias: self.alias.clone(), inner: self.inner.clone(), - data: self.data.clone(), + ledger: self.ledger.clone(), } } } @@ -100,15 +106,9 @@ pub struct WalletInner { pub(crate) storage_manager: StorageManager, } -/// Wallet data. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct WalletData { - /// The wallet BIP44 path. - pub(crate) bip_path: Option, - /// The wallet address. - pub(crate) address: Bech32Address, - /// The wallet alias. - pub(crate) alias: Option, +/// Wallet ledger. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct WalletLedger { /// Outputs // stored separated from the wallet for performance? pub(crate) outputs: HashMap, @@ -137,23 +137,7 @@ pub struct WalletData { pub(crate) native_token_foundries: HashMap, } -impl WalletData { - pub(crate) fn new(bip_path: Option, address: Bech32Address, alias: Option) -> Self { - Self { - bip_path, - address, - alias, - outputs: HashMap::new(), - locked_outputs: HashSet::new(), - unspent_outputs: HashMap::new(), - transactions: HashMap::new(), - pending_transactions: HashSet::new(), - incoming_transactions: HashMap::new(), - inaccessible_incoming_transactions: HashSet::new(), - native_token_foundries: HashMap::new(), - } - } - +impl WalletLedger { fn filter_outputs<'a>( outputs: impl Iterator, filter: FilterOptions, @@ -360,37 +344,12 @@ where crate::wallet::Error: From, crate::client::Error: From, { - /// Create a new wallet. - pub(crate) async fn new(inner: Arc>, data: WalletData) -> Result { - #[cfg(feature = "storage")] - let default_sync_options = inner - .storage_manager - .get_default_sync_options() - .await? - .unwrap_or_default(); - #[cfg(not(feature = "storage"))] - let default_sync_options = Default::default(); - - // TODO: maybe move this into a `reset` fn or smth to avoid this kinda-weird block. - { - let mut last_synced = inner.last_synced.lock().await; - *last_synced = Default::default(); - let mut sync_options = inner.default_sync_options.lock().await; - *sync_options = default_sync_options; - } - - Ok(Self { - inner, - data: Arc::new(RwLock::new(data)), - }) - } - /// Get the [`Output`] that minted a native token by the token ID. First try to get it /// from the wallet, if it isn't in the wallet try to get it from the node pub async fn get_foundry_output(&self, native_token_id: TokenId) -> Result { let foundry_id = FoundryId::from(native_token_id); - for output_data in self.data.read().await.outputs.values() { + for output_data in self.ledger.read().await.outputs.values() { if let Output::Foundry(foundry_output) = &output_data.output { if foundry_output.id() == foundry_id { return Ok(output_data.output.clone()); @@ -410,32 +369,55 @@ where self.inner.emit(wallet_event).await } - pub async fn data(&self) -> tokio::sync::RwLockReadGuard<'_, WalletData> { - self.data.read().await + /// Get the wallet address. + pub async fn address(&self) -> Bech32Address { + self.address.read().await.clone() } - pub(crate) async fn data_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, WalletData> { - self.data.write().await + pub(crate) async fn address_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, Bech32Address> { + self.address.write().await } - #[cfg(feature = "storage")] - pub(crate) fn storage_manager(&self) -> &StorageManager { - &self.storage_manager + /// Get the wallet's Bech32 HRP. + pub async fn bech32_hrp(&self) -> Hrp { + self.address.read().await.hrp + } + + /// Get the wallet's bip path. + pub async fn bip_path(&self) -> Option { + *self.bip_path.read().await + } + + pub(crate) async fn bip_path_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, Option> { + self.bip_path.write().await } /// Get the alias of the wallet if one was set. pub async fn alias(&self) -> Option { - self.data().await.alias.clone() + self.alias.read().await.clone() } - /// Get the wallet address. - pub async fn address(&self) -> Bech32Address { - self.data().await.address.clone() + pub(crate) async fn alias_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, Option> { + self.alias.write().await + } + + /// Get the wallet's ledger state. + pub async fn ledger(&self) -> tokio::sync::RwLockReadGuard<'_, WalletLedger> { + self.ledger.read().await + } + + pub(crate) async fn ledger_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, WalletLedger> { + self.ledger.write().await + } + + #[cfg(feature = "storage")] + pub(crate) fn storage_manager(&self) -> &StorageManager { + &self.storage_manager } /// Returns the implicit account creation address of the wallet if it is Ed25519 based. pub async fn implicit_account_creation_address(&self) -> Result { - let bech32_address = &self.data().await.address; + let bech32_address = &self.address().await; if let Address::Ed25519(address) = bech32_address.inner() { Ok(Bech32Address::new( @@ -446,16 +428,6 @@ where Err(Error::NonEd25519Address) } } - - /// Get the wallet's configured Bech32 HRP. - pub async fn bech32_hrp(&self) -> Hrp { - self.data().await.address.hrp - } - - /// Get the wallet's configured bip path. - pub async fn bip_path(&self) -> Option { - self.data().await.bip_path - } } impl WalletInner { @@ -523,13 +495,10 @@ impl Drop for WalletInner { } } -/// Dto for the wallet data. +/// Dto for the wallet ledger. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WalletDataDto { - pub bip_path: Option, - pub address: Bech32Address, - pub alias: Option, +pub struct WalletLedgerDto { pub outputs: HashMap, pub locked_outputs: HashSet, pub unspent_outputs: HashMap, @@ -540,17 +509,14 @@ pub struct WalletDataDto { pub native_token_foundries: HashMap, } -impl TryFromDto for WalletData { +impl TryFromDto for WalletLedger { type Error = crate::wallet::Error; fn try_from_dto_with_params_inner( - dto: WalletDataDto, + dto: WalletLedgerDto, params: Option<&ProtocolParameters>, ) -> core::result::Result { Ok(Self { - bip_path: dto.bip_path, - address: dto.address, - alias: dto.alias, outputs: dto.outputs, locked_outputs: dto.locked_outputs, unspent_outputs: dto.unspent_outputs, @@ -571,12 +537,9 @@ impl TryFromDto for WalletData { } } -impl From<&WalletData> for WalletDataDto { - fn from(value: &WalletData) -> Self { +impl From<&WalletLedger> for WalletLedgerDto { + fn from(value: &WalletLedger) -> Self { Self { - bip_path: value.bip_path, - address: value.address.clone(), - alias: value.alias.clone(), outputs: value.outputs.clone(), locked_outputs: value.locked_outputs.clone(), unspent_outputs: value.unspent_outputs.clone(), @@ -686,13 +649,7 @@ mod test { incoming_transaction, ); - let wallet_data = WalletData { - bip_path: Some(Bip44::new(4218)), - address: crate::types::block::address::Bech32Address::from_str( - "rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy", - ) - .unwrap(), - alias: Some("Alice".to_string()), + let wallet_ledger = WalletLedger { outputs: HashMap::new(), locked_outputs: HashSet::new(), unspent_outputs: HashMap::new(), @@ -703,29 +660,22 @@ mod test { native_token_foundries: HashMap::new(), }; - let deser_wallet_data = WalletData::try_from_dto( - serde_json::from_str::(&serde_json::to_string(&WalletDataDto::from(&wallet_data)).unwrap()) - .unwrap(), + let deser_wallet_ledger = WalletLedger::try_from_dto( + serde_json::from_str::( + &serde_json::to_string(&WalletLedgerDto::from(&wallet_ledger)).unwrap(), + ) + .unwrap(), ) .unwrap(); - assert_eq!(wallet_data, deser_wallet_data); + assert_eq!(wallet_ledger, deser_wallet_ledger); } - impl WalletData { - /// Returns a mock of this type with the following values: - /// index: 0, coin_type: 4218, alias: "Alice", address: - /// rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy, all other fields are set to their Rust - /// defaults. + impl WalletLedger { + // TODO: use something non-empty #[cfg(feature = "storage")] - pub(crate) fn mock() -> Self { + pub(crate) fn test_instance() -> Self { Self { - bip_path: Some(Bip44::new(4218)), - address: crate::types::block::address::Bech32Address::from_str( - "rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy", - ) - .unwrap(), - alias: Some("Alice".to_string()), outputs: HashMap::new(), locked_outputs: HashSet::new(), unspent_outputs: HashMap::new(), diff --git a/sdk/src/wallet/core/operations/address_generation.rs b/sdk/src/wallet/core/operations/address_generation.rs index 50427dd324..42e51294fa 100644 --- a/sdk/src/wallet/core/operations/address_generation.rs +++ b/sdk/src/wallet/core/operations/address_generation.rs @@ -25,7 +25,7 @@ impl Wallet { address_index: u32, options: impl Into> + Send, ) -> crate::wallet::Result { - // TODO #1279: not sure yet whether we also should allow this method to generate addresses for different bip + // TODO: not sure yet whether we also should allow this method to generate addresses for different bip // paths. let coin_type = self.bip_path().await.ok_or(Error::MissingBipPath)?.coin_type; diff --git a/sdk/src/wallet/core/operations/client.rs b/sdk/src/wallet/core/operations/client.rs index 1955b3e7f5..2f6d513b10 100644 --- a/sdk/src/wallet/core/operations/client.rs +++ b/sdk/src/wallet/core/operations/client.rs @@ -67,7 +67,7 @@ where } *self.client.network_info.write().await = network_info; - self.update_bech32_hrp().await?; + self.update_address_hrp().await?; } #[cfg(feature = "storage")] @@ -139,7 +139,7 @@ where .update_node_manager(node_manager_builder.build(HashSet::new())) .await?; - self.update_bech32_hrp().await?; + self.update_address_hrp().await?; Ok(()) } diff --git a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs index 3435397b5e..acbf5b8366 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/mod.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod stronghold_snapshot; use std::{fs, path::PathBuf}; -use self::stronghold_snapshot::read_wallet_data_from_stronghold_snapshot; +use self::stronghold_snapshot::restore_from_stronghold_snapshot; #[cfg(feature = "storage")] use crate::wallet::WalletBuilder; use crate::{ @@ -14,11 +14,12 @@ use crate::{ utils::Password, }, types::block::address::Hrp, - wallet::Wallet, + wallet::{core::WalletLedgerDto, Wallet}, }; impl Wallet { - /// Backup the wallet data in a Stronghold file. + /// Backup the wallet in a Stronghold snapshot file. + /// /// `stronghold_password` must be the current one when Stronghold is used as SecretManager. pub async fn backup( &self, @@ -34,7 +35,7 @@ impl Wallet { // Backup with existing stronghold SecretManager::Stronghold(stronghold) => { stronghold.set_password(stronghold_password).await?; - self.store_data_to_stronghold(stronghold).await?; + self.backup_to_stronghold_snapshot(stronghold).await?; // Write snapshot to backup path stronghold.write_stronghold_snapshot(Some(&backup_path)).await?; } @@ -45,7 +46,7 @@ impl Wallet { .password(stronghold_password) .build(backup_path)?; - self.store_data_to_stronghold(&backup_stronghold).await?; + self.backup_to_stronghold_snapshot(&backup_stronghold).await?; // Write snapshot to backup path backup_stronghold.write_stronghold_snapshot(None).await?; @@ -55,7 +56,8 @@ impl Wallet { Ok(()) } - /// Restore a backup from a Stronghold file + /// Restore a backup from a Stronghold snapshot file. + /// /// Replaces client_options, bip_path, secret_manager and wallet. Returns an error if the wallet was already /// created If Stronghold is used as secret_manager, the existing Stronghold file will be overwritten. If a /// mnemonic was stored, it will be gone. @@ -79,34 +81,32 @@ impl Wallet { return Err(crate::wallet::Error::Backup("backup path doesn't exist")); } - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; // We don't want to overwrite a possible existing wallet - if !wallet_data.outputs.is_empty() { + if !wallet_ledger.outputs.is_empty() { return Err(crate::wallet::Error::Backup( "can't restore backup when there is already a wallet", )); } - let curr_bip_path = wallet_data.bip_path; + let curr_bip_path = self.bip_path().await; // Explicitly drop the data to avoid contention - drop(wallet_data); + drop(wallet_ledger); // We'll create a new stronghold to load the backup let new_stronghold = StrongholdSecretManager::builder() .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_secret_manager, read_wallet_data) = - read_wallet_data_from_stronghold_snapshot::(&new_stronghold).await?; - - let read_bip_path = read_wallet_data.as_ref().and_then(|data| data.bip_path); + let (read_address, read_bip_path, read_alias, read_client_options, read_secret_manager, read_wallet_ledger) = + restore_from_stronghold_snapshot::(&new_stronghold).await?; // If the bip path is not matching the current one, we may ignore the backup let ignore_backup_values = ignore_if_bip_path_mismatch.map_or(false, |ignore| { if ignore { - // TODO: #1279 okay that if both are none we always load the backup values? + // TODO: is it okay that if both are none we always load the backup values? curr_bip_path != read_bip_path } else { false @@ -114,7 +114,7 @@ impl Wallet { }); if !ignore_backup_values { - self.data_mut().await.bip_path = read_bip_path; + *self.bip_path_mut().await = read_bip_path; } // Get the current snapshot path if set @@ -154,14 +154,17 @@ impl Wallet { } if !ignore_backup_values { - if let Some(read_wallet_data) = read_wallet_data { + if let Some(read_wallet_ledger) = read_wallet_ledger { let restore_wallet = ignore_if_bech32_hrp_mismatch.map_or(true, |expected_bech32_hrp| { // Only restore if bech32 hrps match - read_wallet_data.address.hrp() == &expected_bech32_hrp + read_address.hrp() == &expected_bech32_hrp }); if restore_wallet { - *self.data_mut().await = read_wallet_data; + *self.address_mut().await = read_address; + *self.bip_path_mut().await = read_bip_path; + *self.alias_mut().await = read_alias; + *self.ledger_mut().await = read_wallet_ledger; } } } @@ -182,12 +185,16 @@ impl Wallet { .expect("can't convert os string"), ) .with_client_options(self.client_options().await) - .with_bip_path(self.data().await.bip_path); + .with_address(self.address().await) + .with_bip_path(self.bip_path().await) + .with_alias(self.alias().await); wallet_builder.save(self.storage_manager()).await?; - // also save wallet data to db - self.storage_manager().save_wallet_data(&*self.data().await).await?; + // also save wallet ledger to db + self.storage_manager() + .save_wallet_ledger(&WalletLedgerDto::from(&*self.ledger().await)) + .await?; } Ok(()) @@ -195,8 +202,9 @@ impl Wallet { } impl Wallet { - /// Backup the wallet data in a Stronghold file - /// stronghold_password must be the current one when Stronghold is used as SecretManager. + /// Backup the wallet in a Stronghold snapshot file. + /// + /// `stronghold_password` must be the current one when Stronghold is used as SecretManager. pub async fn backup( &self, backup_path: PathBuf, @@ -207,7 +215,7 @@ impl Wallet { secret_manager.set_password(stronghold_password).await?; - self.store_data_to_stronghold(&secret_manager).await?; + self.backup_to_stronghold_snapshot(&secret_manager).await?; // Write snapshot to backup path secret_manager.write_stronghold_snapshot(Some(&backup_path)).await?; @@ -215,7 +223,8 @@ impl Wallet { Ok(()) } - /// Restore a backup from a Stronghold file + /// Restore a backup from a Stronghold file. + /// /// Replaces client_options, bip path, secret_manager and wallet. Returns an error if the wallet was already /// created If Stronghold is used as secret_manager, the existing Stronghold file will be overwritten. If a /// mnemonic was stored, it will be gone. @@ -239,34 +248,32 @@ impl Wallet { return Err(crate::wallet::Error::Backup("backup path doesn't exist")); } - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; // We don't want to overwrite a possible existing wallet - if !wallet_data.outputs.is_empty() { + if !wallet_ledger.outputs.is_empty() { return Err(crate::wallet::Error::Backup( "can't restore backup when there is already a wallet", )); } - let curr_bip_path = wallet_data.bip_path; + let curr_bip_path = self.bip_path().await; // Explicitly drop the data to avoid contention - drop(wallet_data); + drop(wallet_ledger); // We'll create a new stronghold to load the backup let new_stronghold = StrongholdSecretManager::builder() .password(stronghold_password.clone()) .build(backup_path.clone())?; - let (read_client_options, read_secret_manager, read_wallet_data) = - read_wallet_data_from_stronghold_snapshot::(&new_stronghold).await?; - - let read_bip_path = read_wallet_data.as_ref().and_then(|data| data.bip_path); + let (read_address, read_bip_path, read_alias, read_client_options, read_secret_manager, read_wallet_ledger) = + restore_from_stronghold_snapshot::(&new_stronghold).await?; // If the bip path is not matching the current one, we may ignore the backup let ignore_backup_values = ignore_if_bip_path_mismatch.map_or(false, |ignore| { if ignore { - // TODO: #1279 okay that if both are none we always load the backup values? + // TODO: is it okay that if both are none we always load the backup values? curr_bip_path != read_bip_path } else { false @@ -274,7 +281,7 @@ impl Wallet { }); if !ignore_backup_values { - self.data_mut().await.bip_path = read_bip_path; + *self.bip_path_mut().await = read_bip_path; } if let Some(mut read_secret_manager) = read_secret_manager { @@ -303,14 +310,17 @@ impl Wallet { } if !ignore_backup_values { - if let Some(read_wallet_data) = read_wallet_data { + if let Some(read_wallet_ledger) = read_wallet_ledger { let restore_wallet = ignore_if_bech32_hrp_mismatch.map_or(true, |expected_bech32_hrp| { // Only restore if bech32 hrps match - read_wallet_data.address.hrp() == &expected_bech32_hrp + read_address.hrp() == &expected_bech32_hrp }); if restore_wallet { - *self.data_mut().await = read_wallet_data; + *self.address_mut().await = read_address; + *self.bip_path_mut().await = read_bip_path; + *self.alias_mut().await = read_alias; + *self.ledger_mut().await = read_wallet_ledger; } } } @@ -331,12 +341,16 @@ impl Wallet { .expect("can't convert os string"), ) .with_client_options(self.client_options().await) - .with_bip_path(self.data().await.bip_path); + .with_address(self.address().await) + .with_bip_path(self.bip_path().await) + .with_alias(self.alias().await); wallet_builder.save(self.storage_manager()).await?; - // also save wallet data to db - self.storage_manager().save_wallet_data(&*self.data().await).await?; + // also save wallet ledger to db + self.storage_manager() + .save_wallet_ledger(&WalletLedgerDto::from(&*self.ledger().await)) + .await?; } Ok(()) 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 83632aaca1..3d40b556be 100644 --- a/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs +++ b/sdk/src/wallet/core/operations/stronghold_backup/stronghold_snapshot.rs @@ -1,11 +1,14 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use crypto::keys::bip44::Bip44; + use crate::{ client::{secret::SecretManagerConfig, storage::StorageAdapter, stronghold::StrongholdAdapter}, - types::TryFromDto, + types::{block::address::Bech32Address, TryFromDto}, wallet::{ - core::{WalletData, WalletDataDto}, + self, + core::{WalletLedger, WalletLedgerDto}, migration::{latest_backup_migration_version, migrate, MIGRATION_VERSION_KEY}, ClientOptions, Wallet, }, @@ -13,61 +16,95 @@ use crate::{ pub(crate) const CLIENT_OPTIONS_KEY: &str = "client_options"; pub(crate) const SECRET_MANAGER_KEY: &str = "secret_manager"; -pub(crate) const WALLET_DATA_KEY: &str = "wallet_data"; +pub(crate) const WALLET_LEDGER_KEY: &str = "wallet_ledger"; +pub(crate) const WALLET_ADDRESS_KEY: &str = "wallet_address"; +pub(crate) const WALLET_BIP_PATH_KEY: &str = "wallet_bip_path"; +pub(crate) const WALLET_ALIAS_KEY: &str = "wallet_alias"; -impl Wallet { - pub(crate) async fn store_data_to_stronghold(&self, stronghold: &StrongholdAdapter) -> crate::wallet::Result<()> { +impl Wallet +where + crate::wallet::Error: From, + crate::client::Error: From, +{ + pub(crate) async fn backup_to_stronghold_snapshot( + &self, + stronghold: &StrongholdAdapter, + ) -> crate::wallet::Result<()> { // Set migration version stronghold .set(MIGRATION_VERSION_KEY, &latest_backup_migration_version()) .await?; + // Store the client options let client_options = self.client_options().await; stronghold.set(CLIENT_OPTIONS_KEY, &client_options).await?; + // Store the secret manager if let Some(secret_manager_dto) = self.secret_manager.read().await.to_config() { stronghold.set(SECRET_MANAGER_KEY, &secret_manager_dto).await?; } - let serialized_wallet_data = serde_json::to_value(&WalletDataDto::from(&*self.data.read().await))?; - stronghold.set(WALLET_DATA_KEY, &serialized_wallet_data).await?; + // Store the wallet address + stronghold + .set(WALLET_ADDRESS_KEY, self.address().await.as_ref()) + .await?; + + // Store the wallet bip path + stronghold.set(WALLET_BIP_PATH_KEY, &self.bip_path().await).await?; + + // Store the wallet alias + stronghold.set(WALLET_ALIAS_KEY, &self.alias().await).await?; + + let serialized_wallet_ledger = serde_json::to_value(&WalletLedgerDto::from(&*self.ledger.read().await))?; + stronghold.set(WALLET_LEDGER_KEY, &serialized_wallet_ledger).await?; Ok(()) } } -pub(crate) async fn read_wallet_data_from_stronghold_snapshot( +pub(crate) async fn restore_from_stronghold_snapshot( stronghold: &StrongholdAdapter, -) -> crate::wallet::Result<(Option, Option, Option)> { +) -> crate::wallet::Result<( + Bech32Address, + Option, + Option, + Option, + Option, + Option, +)> { migrate(stronghold).await?; // Get client_options let client_options = stronghold.get(CLIENT_OPTIONS_KEY).await?; - // TODO #1279: remove - // // Get coin_type - // let coin_type_bytes = stronghold.get_bytes(COIN_TYPE_KEY).await?; - // let coin_type = if let Some(coin_type_bytes) = coin_type_bytes { - // let coin_type = u32::from_le_bytes( - // coin_type_bytes - // .try_into() - // .map_err(|_| WalletError::Backup("invalid coin_type"))?, - // ); - // log::debug!("[restore_backup] restored coin_type: {coin_type}"); - // Some(coin_type) - // } else { - // None - // }; - // Get secret_manager - let restored_secret_manager = stronghold.get(SECRET_MANAGER_KEY).await?; + let secret_manager = stronghold.get(SECRET_MANAGER_KEY).await?; + + // Get the wallet address + let wallet_address = stronghold + .get(WALLET_ADDRESS_KEY) + .await? + .ok_or(wallet::Error::Backup("missing non-optional wallet address"))?; + + // Get the wallet bip path + let wallet_bip_path = stronghold.get(WALLET_BIP_PATH_KEY).await?; + + // Get the wallet alias + let wallet_alias = stronghold.get(WALLET_ALIAS_KEY).await?; - // Get wallet data - let restored_wallet_data = stronghold - .get::(WALLET_DATA_KEY) + // Get wallet ledger + let wallet_ledger = stronghold + .get::(WALLET_LEDGER_KEY) .await? - .map(WalletData::try_from_dto) + .map(WalletLedger::try_from_dto) .transpose()?; - Ok((client_options, restored_secret_manager, restored_wallet_data)) + Ok(( + wallet_address, + wallet_bip_path, + wallet_alias, + client_options, + secret_manager, + wallet_ledger, + )) } diff --git a/sdk/src/wallet/operations/balance.rs b/sdk/src/wallet/operations/balance.rs index e9efdbfd80..400eacd08a 100644 --- a/sdk/src/wallet/operations/balance.rs +++ b/sdk/src/wallet/operations/balance.rs @@ -27,7 +27,8 @@ where let protocol_parameters = self.client().get_protocol_parameters().await?; let slot_index = self.client().get_slot_index().await?; - let wallet_data = self.data().await.clone(); + let wallet_address = self.address().await; + let wallet_ledger = self.ledger().await.clone(); let network_id = protocol_parameters.network_id(); let storage_score_params = protocol_parameters.storage_score_parameters(); @@ -36,20 +37,21 @@ where let mut total_native_tokens = NativeTokensBuilder::default(); #[cfg(feature = "participation")] - let voting_output = wallet_data.get_voting_output()?; + let voting_output = wallet_ledger.get_voting_output()?; - let claimable_outputs = wallet_data.claimable_outputs(OutputsToClaim::All, slot_index, &protocol_parameters)?; + let claimable_outputs = + wallet_ledger.claimable_outputs(&wallet_address, OutputsToClaim::All, slot_index, &protocol_parameters)?; #[cfg(feature = "participation")] { if let Some(voting_output) = &voting_output { - if voting_output.output.as_basic().address() == wallet_data.address.inner() { + if voting_output.output.as_basic().address() == wallet_address.inner() { balance.base_coin.voting_power = voting_output.output.amount(); } } } - for (output_id, output_data) in &wallet_data.unspent_outputs { + for (output_id, output_data) in &wallet_ledger.unspent_outputs { // Check if output is from the network we're currently connected to if output_data.network_id != network_id { continue; @@ -76,7 +78,7 @@ where // Add storage deposit balance.required_storage_deposit.account += storage_cost; - if !wallet_data.locked_outputs.contains(output_id) { + if !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } @@ -88,7 +90,7 @@ where balance.base_coin.total += foundry.amount(); // Add storage deposit balance.required_storage_deposit.foundry += storage_cost; - if !wallet_data.locked_outputs.contains(output_id) { + if !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } @@ -108,7 +110,7 @@ where } // Add storage deposit balance.required_storage_deposit.delegation += storage_cost; - if !wallet_data.locked_outputs.contains(output_id) { + if !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } @@ -141,12 +143,12 @@ where // Add storage deposit if output.is_basic() { balance.required_storage_deposit.basic += storage_cost; - if output.native_token().is_some() && !wallet_data.locked_outputs.contains(output_id) { + if output.native_token().is_some() && !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } } else if output.is_nft() { balance.required_storage_deposit.nft += storage_cost; - if !wallet_data.locked_outputs.contains(output_id) { + if !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } } @@ -172,7 +174,7 @@ where // We use the addresses with unspent outputs, because other addresses of // the account without unspent // outputs can't be related to this output - wallet_data.address.inner(), + wallet_address.inner(), output, slot_index, protocol_parameters.committable_age_range(), @@ -188,7 +190,7 @@ where .map_or_else( || output.amount(), |sdr| { - if wallet_data.address.inner() == sdr.return_address() { + if wallet_address.inner() == sdr.return_address() { // sending to ourself, we get the full amount output.amount() } else { @@ -219,13 +221,13 @@ where // Amount for basic outputs isn't added to total storage cost if there aren't native // tokens, since we can spend it without burning. if output.native_token().is_some() - && !wallet_data.locked_outputs.contains(output_id) + && !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } } else if output.is_nft() { balance.required_storage_deposit.nft += storage_cost; - if !wallet_data.locked_outputs.contains(output_id) { + if !wallet_ledger.locked_outputs.contains(output_id) { total_storage_cost += storage_cost; } } @@ -259,18 +261,18 @@ where } // for `available` get locked_outputs, sum outputs amount and subtract from total_amount - log::debug!("[BALANCE] locked outputs: {:#?}", wallet_data.locked_outputs); + log::debug!("[BALANCE] locked outputs: {:#?}", wallet_ledger.locked_outputs); let mut locked_amount = 0; let mut locked_mana = DecayedMana::default(); let mut locked_native_tokens = NativeTokensBuilder::default(); - for locked_output in &wallet_data.locked_outputs { + for locked_output in &wallet_ledger.locked_outputs { // Skip potentially_locked_outputs, as their amounts aren't added to the balance if balance.potentially_locked_outputs.contains_key(locked_output) { continue; } - if let Some(output_data) = wallet_data.unspent_outputs.get(locked_output) { + if let Some(output_data) = wallet_ledger.unspent_outputs.get(locked_output) { // Only check outputs that are in this network if output_data.network_id == network_id { locked_amount += output_data.output.amount(); @@ -309,7 +311,7 @@ where } }); - let metadata = wallet_data + let metadata = wallet_ledger .native_token_foundries .get(&FoundryId::from(*native_token.token_id())) .and_then(|foundry| foundry.immutable_features().metadata()) diff --git a/sdk/src/wallet/operations/block.rs b/sdk/src/wallet/operations/block.rs index 491bd78ae6..20273a047e 100644 --- a/sdk/src/wallet/operations/block.rs +++ b/sdk/src/wallet/operations/block.rs @@ -22,7 +22,7 @@ where // If an issuer ID is provided, use it; otherwise, use the first available account or implicit account. let issuer_id = match issuer_id.into() { Some(id) => id, - None => self.data().await.first_account_id().ok_or(Error::AccountNotFound)?, + None => self.ledger().await.first_account_id().ok_or(Error::AccountNotFound)?, }; let unsigned_block = self.client().build_basic_block(issuer_id, payload).await?; diff --git a/sdk/src/wallet/operations/output_claiming.rs b/sdk/src/wallet/operations/output_claiming.rs index eaec534a7c..7d063c7e88 100644 --- a/sdk/src/wallet/operations/output_claiming.rs +++ b/sdk/src/wallet/operations/output_claiming.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ client::{api::PreparedTransactionData, secret::SecretManage}, types::block::{ - address::{Address, Ed25519Address}, + address::{Address, Bech32Address, Ed25519Address}, output::{ unlock_condition::AddressUnlockCondition, BasicOutput, NftOutputBuilder, Output, OutputId, UnlockCondition, }, @@ -16,7 +16,7 @@ use crate::{ slot::SlotIndex, }, wallet::{ - core::WalletData, + core::WalletLedger, operations::{helpers::time::can_output_be_unlocked_now, transaction::TransactionOptions}, types::{OutputData, TransactionWithMetadata}, Wallet, @@ -34,7 +34,7 @@ pub enum OutputsToClaim { All, } -impl WalletData { +impl WalletLedger { /// Get basic and nft outputs that have /// [`ExpirationUnlockCondition`](crate::types::block::output::unlock_condition::ExpirationUnlockCondition), /// [`StorageDepositReturnUnlockCondition`](crate::types::block::output::unlock_condition::StorageDepositReturnUnlockCondition) or @@ -43,6 +43,7 @@ impl WalletData { /// additional inputs pub(crate) fn claimable_outputs( &self, + wallet_address: &Bech32Address, outputs_to_claim: OutputsToClaim, slot_index: SlotIndex, protocol_parameters: &ProtocolParameters, @@ -66,7 +67,7 @@ impl WalletData { && can_output_be_unlocked_now( // We use the addresses with unspent outputs, because other addresses of the // account without unspent outputs can't be related to this output - self.address.inner(), + wallet_address.inner(), output_data, slot_index, protocol_parameters.committable_age_range(), @@ -142,12 +143,17 @@ where /// unlocked now and also get basic outputs with only an [`AddressUnlockCondition`] unlock condition, for /// additional inputs pub async fn claimable_outputs(&self, outputs_to_claim: OutputsToClaim) -> crate::wallet::Result> { - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; let slot_index = self.client().get_slot_index().await?; let protocol_parameters = self.client().get_protocol_parameters().await?; - wallet_data.claimable_outputs(outputs_to_claim, slot_index, &protocol_parameters) + wallet_ledger.claimable_outputs( + &self.address().await, + outputs_to_claim, + slot_index, + &protocol_parameters, + ) } /// Get basic outputs that have only one unlock condition which is [AddressUnlockCondition], so they can be used as @@ -156,11 +162,11 @@ where log::debug!("[OUTPUT_CLAIMING] get_basic_outputs_for_additional_inputs"); #[cfg(feature = "participation")] let voting_output = self.get_voting_output().await?; - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; // Get basic outputs only with AddressUnlockCondition and no other unlock condition let mut basic_outputs: Vec = Vec::new(); - for (output_id, output_data) in &wallet_data.unspent_outputs { + for (output_id, output_data) in &wallet_ledger.unspent_outputs { #[cfg(feature = "participation")] if let Some(ref voting_output) = voting_output { // Remove voting output from inputs, because we don't want to spent it to claim something else. @@ -169,8 +175,8 @@ where } } // Don't use outputs that are locked for other transactions - if !wallet_data.locked_outputs.contains(output_id) { - if let Some(output) = wallet_data.outputs.get(output_id) { + if !wallet_ledger.locked_outputs.contains(output_id) { + if let Some(output) = wallet_ledger.outputs.get(output_id) { if let Output::Basic(basic_output) = &output.output { if let [UnlockCondition::Address(a)] = basic_output.unlock_conditions().as_ref() { // Implicit accounts can't be used @@ -239,12 +245,12 @@ where let storage_score_params = self.client().get_storage_score_parameters().await?; - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; let mut outputs_to_claim = Vec::new(); for output_id in output_ids_to_claim { - if let Some(output_data) = wallet_data.unspent_outputs.get(&output_id) { - if !wallet_data.locked_outputs.contains(&output_id) { + if let Some(output_data) = wallet_ledger.unspent_outputs.get(&output_id) { + if !wallet_ledger.locked_outputs.contains(&output_id) { outputs_to_claim.push(output_data.clone()); } } @@ -256,8 +262,8 @@ where )); } - let wallet_address = wallet_data.address.clone(); - drop(wallet_data); + let wallet_address = self.address().await; + drop(wallet_ledger); let mut nft_outputs_to_send = Vec::new(); @@ -279,13 +285,13 @@ where // deposit for the remaining amount and possible native tokens NftOutputBuilder::from(nft_output) .with_nft_id(nft_output.nft_id_non_null(&output_data.output_id)) - .with_unlock_conditions([AddressUnlockCondition::new(wallet_address.clone())]) + .with_unlock_conditions([AddressUnlockCondition::new(&wallet_address)]) .finish_output()? } else { NftOutputBuilder::from(nft_output) .with_minimum_amount(storage_score_params) .with_nft_id(nft_output.nft_id_non_null(&output_data.output_id)) - .with_unlock_conditions([AddressUnlockCondition::new(wallet_address.clone())]) + .with_unlock_conditions([AddressUnlockCondition::new(&wallet_address)]) .finish_output()? }; diff --git a/sdk/src/wallet/operations/output_consolidation.rs b/sdk/src/wallet/operations/output_consolidation.rs index e4fe56c5ed..e9e970fcd1 100644 --- a/sdk/src/wallet/operations/output_consolidation.rs +++ b/sdk/src/wallet/operations/output_consolidation.rs @@ -91,7 +91,7 @@ where /// Prepares the transaction for [Wallet::consolidate_outputs()]. pub async fn prepare_consolidate_outputs(&self, params: ConsolidationParams) -> Result { log::debug!("[OUTPUT_CONSOLIDATION] prepare consolidating outputs if needed"); - let wallet_address = self.data().await.address.clone(); + let wallet_address = self.address().await; let outputs_to_consolidate = self.get_outputs_to_consolidate(¶ms).await?; @@ -160,24 +160,24 @@ where // let voting_output = self.get_voting_output().await?; let slot_index = self.client().get_slot_index().await?; let storage_score_parameters = self.client().get_protocol_parameters().await?.storage_score_parameters; - let wallet_data = self.data().await; - let wallet_address = wallet_data.address.clone(); + let wallet_ledger = self.ledger().await; + let wallet_address = self.address().await; let mut outputs_to_consolidate = Vec::new(); let mut native_token_inputs = HashMap::new(); - for (output_id, output_data) in &wallet_data.unspent_outputs { + for (output_id, output_data) in &wallet_ledger.unspent_outputs { // #[cfg(feature = "participation")] // if let Some(ref voting_output) = voting_output { // // Remove voting output from inputs, because we want to keep its features and not consolidate it. // if output_data.output_id == voting_output.output_id { // continue; - // } + // }.await // } - let is_locked_output = wallet_data.locked_outputs.contains(output_id); + let is_locked_output = wallet_ledger.locked_outputs.contains(output_id); let should_consolidate_output = self - .should_consolidate_output(output_data, slot_index, &wallet_address) + .should_consolidate_output(output_data, slot_index, wallet_address.as_ref()) .await?; if !is_locked_output && should_consolidate_output { outputs_to_consolidate.push(output_data.clone()); @@ -210,7 +210,7 @@ where }) }); - drop(wallet_data); + drop(wallet_ledger); let output_threshold = self.get_output_consolidation_threshold(params).await?; diff --git a/sdk/src/wallet/operations/participation/mod.rs b/sdk/src/wallet/operations/participation/mod.rs index 4b83fa1b69..bd05b35560 100644 --- a/sdk/src/wallet/operations/participation/mod.rs +++ b/sdk/src/wallet/operations/participation/mod.rs @@ -25,7 +25,7 @@ use crate::{ }, block::output::{unlock_condition::UnlockCondition, Output, OutputId}, }, - wallet::{core::WalletData, types::OutputData, Result, Wallet}, + wallet::{core::WalletLedger, types::OutputData, Result, Wallet}, }; /// An object containing an account's entire participation overview. @@ -66,8 +66,8 @@ where // "[get_participation_overview] restored_spent_cached_outputs_len: {}", // restored_spent_cached_outputs_len // ); - // let wallet_data = self.data().await; - // let participation_outputs = wallet_data.outputs().values().filter(|output_data| { + // let wallet_ledger = self.data().await; + // let participation_outputs = wallet_ledger.outputs().values().filter(|output_data| { // is_valid_participation_output(&output_data.output) // // Check that the metadata exists, because otherwise we aren't participating for anything // && output_data.output.features().and_then(|f| f.metadata()).is_some() @@ -218,7 +218,7 @@ where /// /// If multiple outputs with this tag exist, the one with the largest amount will be returned. pub async fn get_voting_output(&self) -> Result> { - self.data().await.get_voting_output() + self.ledger().await.get_voting_output() } // /// Gets client for an event. @@ -271,7 +271,7 @@ where // } } -impl WalletData { +impl WalletLedger { /// Returns the voting output ("PARTICIPATION" tag). /// /// If multiple outputs with this tag exist, the one with the largest amount will be returned. diff --git a/sdk/src/wallet/operations/syncing/foundries.rs b/sdk/src/wallet/operations/syncing/foundries.rs index 278ceca9a9..ce72321869 100644 --- a/sdk/src/wallet/operations/syncing/foundries.rs +++ b/sdk/src/wallet/operations/syncing/foundries.rs @@ -20,7 +20,7 @@ where ) -> crate::wallet::Result<()> { log::debug!("[SYNC] request_and_store_foundry_outputs"); - let mut foundries = self.data().await.native_token_foundries.clone(); + let mut foundries = self.ledger().await.native_token_foundries.clone(); let results = futures::future::try_join_all(foundry_ids.into_iter().filter(|f| !foundries.contains_key(f)).map( |foundry_id| { @@ -46,8 +46,8 @@ where } } - let mut wallet_data = self.data_mut().await; - wallet_data.native_token_foundries = foundries; + let mut wallet_ledger = self.ledger_mut().await; + wallet_ledger.native_token_foundries = foundries; Ok(()) } diff --git a/sdk/src/wallet/operations/syncing/mod.rs b/sdk/src/wallet/operations/syncing/mod.rs index 4dde220f37..22e51a81f1 100644 --- a/sdk/src/wallet/operations/syncing/mod.rs +++ b/sdk/src/wallet/operations/syncing/mod.rs @@ -96,7 +96,7 @@ where let wallet_address_with_unspent_outputs = AddressWithUnspentOutputs { address: self.address().await, - output_ids: self.data().await.unspent_outputs().keys().copied().collect(), + output_ids: self.ledger().await.unspent_outputs().keys().copied().collect(), internal: false, key_index: 0, }; diff --git a/sdk/src/wallet/operations/syncing/outputs.rs b/sdk/src/wallet/operations/syncing/outputs.rs index c317953024..f6832c6ba1 100644 --- a/sdk/src/wallet/operations/syncing/outputs.rs +++ b/sdk/src/wallet/operations/syncing/outputs.rs @@ -30,14 +30,14 @@ where log::debug!("[SYNC] convert output_responses"); // store outputs with network_id let network_id = self.client().get_network_id().await?; - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; Ok(outputs_with_meta .into_iter() .map(|output_with_meta| { // check if we know the transaction that created this output and if we created it (if we store incoming // transactions separated, then this check wouldn't be required) - let remainder = wallet_data + let remainder = wallet_ledger .transactions .get(output_with_meta.metadata().output_id().transaction_id()) .map_or(false, |tx| !tx.incoming); @@ -65,10 +65,10 @@ where let mut outputs = Vec::new(); let mut unknown_outputs = Vec::new(); let mut unspent_outputs = Vec::new(); - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; for output_id in output_ids { - match wallet_data.outputs.get_mut(&output_id) { + match wallet_ledger.outputs.get_mut(&output_id) { // set unspent if not already Some(output_data) => { if output_data.is_spent() { @@ -88,10 +88,10 @@ where // known output is unspent, so insert it to the unspent outputs again, because if it was an // account/nft/foundry output it could have been removed when syncing without them for (output_id, output_data) in unspent_outputs { - wallet_data.unspent_outputs.insert(output_id, output_data); + wallet_ledger.unspent_outputs.insert(output_id, output_data); } - drop(wallet_data); + drop(wallet_ledger); if !unknown_outputs.is_empty() { outputs.extend(self.client().get_outputs_with_metadata(&unknown_outputs).await?); @@ -114,13 +114,15 @@ where ) -> crate::wallet::Result<()> { log::debug!("[SYNC] request_incoming_transaction_data"); - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; transaction_ids.retain(|transaction_id| { - !(wallet_data.transactions.contains_key(transaction_id) - || wallet_data.incoming_transactions.contains_key(transaction_id) - || wallet_data.inaccessible_incoming_transactions.contains(transaction_id)) + !(wallet_ledger.transactions.contains_key(transaction_id) + || wallet_ledger.incoming_transactions.contains_key(transaction_id) + || wallet_ledger + .inaccessible_incoming_transactions + .contains(transaction_id)) }); - drop(wallet_data); + drop(wallet_ledger); // Limit parallel requests to 100, to avoid timeouts let results = @@ -176,15 +178,15 @@ where .await?; // Update account with new transactions - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; for (transaction_id, txn) in results.into_iter().flatten() { if let Some(transaction) = txn { - wallet_data.incoming_transactions.insert(transaction_id, transaction); + wallet_ledger.incoming_transactions.insert(transaction_id, transaction); } else { log::debug!("[SYNC] adding {transaction_id} to inaccessible_incoming_transactions"); // Save transactions that weren't found by the node to avoid requesting them endlessly. // Will be cleared when new client options are provided. - wallet_data.inaccessible_incoming_transactions.insert(transaction_id); + wallet_ledger.inaccessible_incoming_transactions.insert(transaction_id); } } diff --git a/sdk/src/wallet/operations/syncing/transactions.rs b/sdk/src/wallet/operations/syncing/transactions.rs index b5ab3a80ee..947da1d539 100644 --- a/sdk/src/wallet/operations/syncing/transactions.rs +++ b/sdk/src/wallet/operations/syncing/transactions.rs @@ -8,7 +8,7 @@ use crate::{ block::{input::Input, output::OutputId, BlockId}, }, wallet::{ - core::WalletData, + core::WalletLedger, types::{InclusionState, TransactionWithMetadata}, Wallet, }, @@ -30,13 +30,13 @@ where /// be synced again pub(crate) async fn sync_pending_transactions(&self) -> crate::wallet::Result { log::debug!("[SYNC] sync pending transactions"); - let wallet_data = self.data().await; + let wallet_ledger = self.ledger().await; // only set to true if a transaction got confirmed for which we don't have an output // (transaction_output.is_none()) let mut confirmed_unknown_output = false; - if wallet_data.pending_transactions.is_empty() { + if wallet_ledger.pending_transactions.is_empty() { return Ok(confirmed_unknown_output); } @@ -48,9 +48,9 @@ where // are available again let mut output_ids_to_unlock = Vec::new(); - for transaction_id in &wallet_data.pending_transactions { + for transaction_id in &wallet_ledger.pending_transactions { log::debug!("[SYNC] sync pending transaction {transaction_id}"); - let mut transaction = wallet_data + let mut transaction = wallet_ledger .transactions .get(transaction_id) // panic during development to easier detect if something is wrong, should be handled different later @@ -64,14 +64,14 @@ where // check if we have an output (remainder, if not sending to an own address) that got created by this // transaction, if that's the case, then the transaction got confirmed - let transaction_output = wallet_data + let transaction_output = wallet_ledger .outputs .keys() .find(|o| o.transaction_id() == transaction_id); if let Some(transaction_output) = transaction_output { // Save to unwrap, we just got the output - let confirmed_output_data = wallet_data.outputs.get(transaction_output).expect("output exists"); + let confirmed_output_data = wallet_ledger.outputs.get(transaction_output).expect("output exists"); log::debug!( "[SYNC] confirmed transaction {transaction_id} in block {}", confirmed_output_data.metadata.block_id() @@ -90,7 +90,7 @@ where let mut input_got_spent = false; for input in transaction.payload.transaction().inputs() { let Input::Utxo(input) = input; - if let Some(input) = wallet_data.outputs.get(input.output_id()) { + if let Some(input) = wallet_ledger.outputs.get(input.output_id()) { if input.metadata.is_spent() { input_got_spent = true; } @@ -152,7 +152,7 @@ where } } else if input_got_spent { process_transaction_with_unknown_state( - &wallet_data, + &wallet_ledger, transaction, &mut updated_transactions, &mut output_ids_to_unlock, @@ -162,7 +162,7 @@ where Err(crate::client::Error::Node(crate::client::node_api::error::Error::NotFound(_))) => { if input_got_spent { process_transaction_with_unknown_state( - &wallet_data, + &wallet_ledger, transaction, &mut updated_transactions, &mut output_ids_to_unlock, @@ -173,7 +173,7 @@ where } } else if input_got_spent { process_transaction_with_unknown_state( - &wallet_data, + &wallet_ledger, transaction, &mut updated_transactions, &mut output_ids_to_unlock, @@ -188,7 +188,7 @@ where updated_transactions.push(transaction); } } - drop(wallet_data); + drop(wallet_ledger); // updates account with balances, output ids, outputs self.update_with_transactions(updated_transactions, spent_output_ids, output_ids_to_unlock) @@ -219,7 +219,7 @@ fn updated_transaction_and_outputs( // When a transaction got pruned, the inputs and outputs are also not available, then this could mean that it was // confirmed and the created outputs got also already spent and pruned or the inputs got spent in another transaction fn process_transaction_with_unknown_state( - wallet_data: &WalletData, + wallet_ledger: &WalletLedger, mut transaction: TransactionWithMetadata, updated_transactions: &mut Vec, output_ids_to_unlock: &mut Vec, @@ -227,7 +227,7 @@ fn process_transaction_with_unknown_state( let mut all_inputs_spent = true; for input in transaction.payload.transaction().inputs() { let Input::Utxo(input) = input; - if let Some(output_data) = wallet_data.outputs.get(input.output_id()) { + if let Some(output_data) = wallet_ledger.outputs.get(input.output_id()) { if !output_data.is_spent() { // unspent output needs to be made available again output_ids_to_unlock.push(*input.output_id()); diff --git a/sdk/src/wallet/operations/transaction/account.rs b/sdk/src/wallet/operations/transaction/account.rs index b7b6c5b048..cf19e07c7a 100644 --- a/sdk/src/wallet/operations/transaction/account.rs +++ b/sdk/src/wallet/operations/transaction/account.rs @@ -52,8 +52,8 @@ where where crate::wallet::Error: From, { - let wallet_data = self.data().await; - let implicit_account_data = wallet_data + let wallet_ledger = self.ledger().await; + let implicit_account_data = wallet_ledger .unspent_outputs .get(output_id) .ok_or(Error::ImplicitAccountNotFound)?; @@ -95,7 +95,7 @@ where )?]) .finish_output()?; - drop(wallet_data); + drop(wallet_ledger); let transaction_options = TransactionOptions { required_inputs: [*output_id].into(), diff --git a/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs index c09dbb8d57..3bdbe8f11b 100644 --- a/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/burning_melting/melt_native_token.rs @@ -93,7 +93,7 @@ where let mut existing_account_output_data = None; let mut existing_foundry_output = None; - for (output_id, output_data) in self.data().await.unspent_outputs.iter() { + for (output_id, output_data) in self.ledger().await.unspent_outputs.iter() { match &output_data.output { Output::Account(output) => { if output.account_id_non_null(output_id) == account_id { diff --git a/sdk/src/wallet/operations/transaction/high_level/create_account.rs b/sdk/src/wallet/operations/transaction/high_level/create_account.rs index b57d5db157..652da2da0d 100644 --- a/sdk/src/wallet/operations/transaction/high_level/create_account.rs +++ b/sdk/src/wallet/operations/transaction/high_level/create_account.rs @@ -110,7 +110,7 @@ where ) -> Option<(AccountId, OutputData)> { log::debug!("[get_account_output]"); let account_id = account_id.into(); - self.data() + self.ledger() .await .unspent_outputs .values() diff --git a/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs b/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs index 625336b6bf..46aa8eace6 100644 --- a/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs +++ b/sdk/src/wallet/operations/transaction/high_level/delegation/create.rs @@ -91,7 +91,7 @@ where self.client().bech32_hrp_matches(bech32_address.hrp()).await?; bech32_address.inner().clone() } - None => self.address().await.inner().clone(), + None => self.address().await.into_inner(), }; let output = DelegationOutputBuilder::new_with_amount( diff --git a/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs b/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs index 83ba6f38b7..ca4f9ea80a 100644 --- a/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs +++ b/sdk/src/wallet/operations/transaction/high_level/delegation/delay.rs @@ -39,7 +39,7 @@ where reclaim_excess: bool, ) -> crate::wallet::Result { let delegation_output = self - .data() + .ledger() .await .unspent_delegation_output(&delegation_id) .ok_or(crate::wallet::Error::MissingDelegation(delegation_id))? diff --git a/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs b/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs index 6f94e1e941..99b3a23df0 100644 --- a/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs +++ b/sdk/src/wallet/operations/transaction/high_level/minting/mint_native_token.rs @@ -56,8 +56,8 @@ where log::debug!("[TRANSACTION] mint_native_token"); let mint_amount = mint_amount.into(); - let wallet_data = self.data().await; - let existing_foundry_output = wallet_data.unspent_outputs.values().find(|output_data| { + let wallet_ledger = self.ledger().await; + let existing_foundry_output = wallet_ledger.unspent_outputs.values().find(|output_data| { if let Output::Foundry(output) = &output_data.output { TokenId::new(*output.id()) == token_id } else { @@ -80,7 +80,7 @@ where } // Get the account output that controls the foundry output - let existing_account_output = wallet_data.unspent_outputs.values().find(|output_data| { + let existing_account_output = wallet_ledger.unspent_outputs.values().find(|output_data| { if let Output::Account(output) = &output_data.output { output.account_id_non_null(&output_data.output_id) == **foundry_output.account_address() } else { @@ -94,7 +94,7 @@ where return Err(Error::MintingFailed("account output is not available".to_string())); }; - drop(wallet_data); + drop(wallet_ledger); let account_output = if let Output::Account(account_output) = existing_account_output.output { account_output diff --git a/sdk/src/wallet/operations/transaction/high_level/send_nft.rs b/sdk/src/wallet/operations/transaction/high_level/send_nft.rs index 8675a8659b..ffc86ba9ae 100644 --- a/sdk/src/wallet/operations/transaction/high_level/send_nft.rs +++ b/sdk/src/wallet/operations/transaction/high_level/send_nft.rs @@ -96,7 +96,7 @@ where self.client().bech32_hrp_matches(address.hrp()).await?; // Find nft output from the inputs - if let Some(nft_output_data) = self.data().await.unspent_nft_output(&nft_id) { + if let Some(nft_output_data) = self.ledger().await.unspent_nft_output(&nft_id) { if let Output::Nft(nft_output) = &nft_output_data.output { // Set the nft id and new address unlock condition let nft_builder = NftOutputBuilder::from(nft_output) diff --git a/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs b/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs index 1b7b844dc0..e8f70dc5a0 100644 --- a/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs +++ b/sdk/src/wallet/operations/transaction/high_level/staking/begin.rs @@ -52,7 +52,7 @@ where let account_id = params.account_id; let account_output_data = self - .data() + .ledger() .await .unspent_account_output(&account_id) .cloned() diff --git a/sdk/src/wallet/operations/transaction/high_level/staking/end.rs b/sdk/src/wallet/operations/transaction/high_level/staking/end.rs index fd991ae8fa..115d1a6efb 100644 --- a/sdk/src/wallet/operations/transaction/high_level/staking/end.rs +++ b/sdk/src/wallet/operations/transaction/high_level/staking/end.rs @@ -23,7 +23,7 @@ where log::debug!("[TRANSACTION] prepare_end_staking"); let account_output_data = self - .data() + .ledger() .await .unspent_account_output(&account_id) .cloned() @@ -38,8 +38,7 @@ where let protocol_parameters = self.client().get_protocol_parameters().await?; let slot_commitment_id = self.client().get_issuance().await?.latest_commitment.id(); - let future_bounded_epoch = - protocol_parameters.epoch_index_of(protocol_parameters.future_bounded_slot(slot_commitment_id)); + let future_bounded_epoch = protocol_parameters.future_bounded_epoch(slot_commitment_id); if future_bounded_epoch <= staking_feature.end_epoch() { let end_epoch = protocol_parameters.epoch_index_of(slot_commitment_id.slot_index()) diff --git a/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs b/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs index 535a3778c5..3797811d2c 100644 --- a/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs +++ b/sdk/src/wallet/operations/transaction/high_level/staking/extend.rs @@ -31,7 +31,7 @@ where log::debug!("[TRANSACTION] prepare_extend_staking"); let account_output_data = self - .data() + .ledger() .await .unspent_account_output(&account_id) .cloned() @@ -41,8 +41,7 @@ where let slot_commitment_id = self.client().get_issuance().await?.latest_commitment.id(); - let future_bounded_epoch = - protocol_parameters.epoch_index_of(protocol_parameters.future_bounded_slot(slot_commitment_id)); + let future_bounded_epoch = protocol_parameters.future_bounded_epoch(slot_commitment_id); let staking_feature = account_output_data .output @@ -71,8 +70,7 @@ where protocol_parameters.staking_unbonding_period() ))); } - let past_bounded_epoch = - protocol_parameters.epoch_index_of(protocol_parameters.past_bounded_slot(slot_commitment_id)); + let past_bounded_epoch = protocol_parameters.past_bounded_epoch(slot_commitment_id); let end_epoch = past_bounded_epoch.saturating_add(additional_epochs); output_builder = output_builder.replace_feature(StakingFeature::new( staking_feature.staked_amount(), diff --git a/sdk/src/wallet/operations/transaction/input_selection.rs b/sdk/src/wallet/operations/transaction/input_selection.rs index b7cdd895b2..6a42a2cb6f 100644 --- a/sdk/src/wallet/operations/transaction/input_selection.rs +++ b/sdk/src/wallet/operations/transaction/input_selection.rs @@ -4,6 +4,8 @@ use alloc::collections::BTreeSet; use std::collections::HashMap; +use crypto::keys::bip44::Bip44; + #[cfg(feature = "events")] use crate::wallet::events::types::{TransactionProgressEvent, WalletEvent}; use crate::{ @@ -12,12 +14,13 @@ use crate::{ secret::{types::InputSigningData, SecretManage}, }, types::block::{ + address::Bech32Address, output::{Output, OutputId}, protocol::CommittableAgeRange, slot::SlotIndex, }, wallet::{ - core::WalletData, operations::helpers::time::can_output_be_unlocked_forever_from_now_on, types::OutputData, + operations::helpers::time::can_output_be_unlocked_forever_from_now_on, types::OutputData, RemainderValueStrategy, TransactionOptions, Wallet, }, }; @@ -41,7 +44,7 @@ where let creation_slot = self.client().get_slot_index().await?; let slot_commitment_id = self.client().get_issuance().await?.latest_commitment.id(); if options.issuer_id.is_none() { - options.issuer_id = self.data().await.first_account_id(); + options.issuer_id = self.ledger().await.first_account_id(); } let reference_mana_cost = if let Some(issuer_id) = options.issuer_id { Some( @@ -58,7 +61,7 @@ where RemainderValueStrategy::CustomAddress(address) => Some(address), }; // lock so the same inputs can't be selected in multiple transactions - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; #[cfg(feature = "events")] self.emit(WalletEvent::TransactionProgress( @@ -67,7 +70,7 @@ where .await; #[allow(unused_mut)] - let mut forbidden_inputs = wallet_data.locked_outputs.clone(); + let mut forbidden_inputs = wallet_ledger.locked_outputs.clone(); // Prevent consuming the voting output if not actually wanted #[cfg(feature = "participation")] @@ -80,8 +83,9 @@ where // Filter inputs to not include inputs that require additional outputs for storage deposit return or could be // still locked. let available_outputs_signing_data = filter_inputs( - &wallet_data, - wallet_data.unspent_outputs.values(), + &self.address().await, + self.bip_path().await, + wallet_ledger.unspent_outputs.values(), creation_slot, protocol_parameters.committable_age_range(), &options.required_inputs, @@ -91,7 +95,7 @@ where if let Some(burn) = &options.burn { for delegation_id in burn.delegations() { - if let Some(output) = wallet_data.unspent_delegation_output(delegation_id) { + if let Some(output) = wallet_ledger.unspent_delegation_output(delegation_id) { mana_rewards.insert( output.output_id, self.client() @@ -105,12 +109,12 @@ where // Check that no input got already locked for output_id in &options.required_inputs { - if wallet_data.locked_outputs.contains(output_id) { + if wallet_ledger.locked_outputs.contains(output_id) { return Err(crate::wallet::Error::CustomInput(format!( "provided custom input {output_id} is already used in another transaction", ))); } - if let Some(input) = wallet_data.outputs.get(output_id) { + if let Some(input) = wallet_ledger.outputs.get(output_id) { if input.output.can_claim_rewards(outputs.iter().find(|o| { input .output @@ -132,7 +136,7 @@ where let mut input_selection = InputSelection::new( available_outputs_signing_data, outputs, - Some(wallet_data.address.clone().into_inner()), + Some(self.address().await.into_inner()), creation_slot, slot_commitment_id, protocol_parameters.clone(), @@ -165,7 +169,7 @@ where // lock outputs so they don't get used by another transaction for output in &prepared_transaction_data.inputs_data { log::debug!("[TRANSACTION] locking: {}", output.output_id()); - wallet_data.locked_outputs.insert(*output.output_id()); + wallet_ledger.locked_outputs.insert(*output.output_id()); } Ok(prepared_transaction_data) @@ -177,7 +181,8 @@ where /// `claim_outputs` or providing their OutputId's in the custom_inputs #[allow(clippy::too_many_arguments)] fn filter_inputs<'a>( - wallet_data: &WalletData, + wallet_address: &Bech32Address, + wallet_bip_path: Option, available_outputs: impl IntoIterator, slot_index: impl Into + Copy, committable_age_range: CommittableAgeRange, @@ -190,7 +195,7 @@ fn filter_inputs<'a>( let output_can_be_unlocked_now_and_in_future = can_output_be_unlocked_forever_from_now_on( // We use the addresses with unspent outputs, because other addresses of the // account without unspent outputs can't be related to this output - &wallet_data.address.inner, + wallet_address.inner(), &output_data.output, slot_index, committable_age_range, @@ -202,7 +207,9 @@ fn filter_inputs<'a>( } } - if let Some(available_input) = output_data.input_signing_data(wallet_data, slot_index, committable_age_range)? { + if let Some(available_input) = + output_data.input_signing_data(wallet_address, wallet_bip_path, slot_index, committable_age_range)? + { available_outputs_signing_data.push(available_input); } } diff --git a/sdk/src/wallet/operations/transaction/mod.rs b/sdk/src/wallet/operations/transaction/mod.rs index f75de0f89b..57688f25f2 100644 --- a/sdk/src/wallet/operations/transaction/mod.rs +++ b/sdk/src/wallet/operations/transaction/mod.rs @@ -22,6 +22,7 @@ use crate::{ block::{output::Output, payload::signed_transaction::SignedTransactionPayload}, }, wallet::{ + core::WalletLedgerDto, types::{InclusionState, TransactionWithMetadata}, Wallet, }, @@ -172,16 +173,18 @@ where inputs, }; - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; - wallet_data.transactions.insert(transaction_id, transaction.clone()); - wallet_data.pending_transactions.insert(transaction_id); + wallet_ledger.transactions.insert(transaction_id, transaction.clone()); + wallet_ledger.pending_transactions.insert(transaction_id); #[cfg(feature = "storage")] { // TODO: maybe better to use the wallet address as identifier now? - log::debug!("[TRANSACTION] storing wallet"); - self.storage_manager().save_wallet_data(&wallet_data).await?; + log::debug!("[TRANSACTION] storing wallet ledger"); + self.storage_manager() + .save_wallet_ledger(&WalletLedgerDto::from(&*wallet_ledger)) + .await?; } Ok(transaction) @@ -189,10 +192,10 @@ where // unlock outputs async fn unlock_inputs(&self, inputs: &[InputSigningData]) -> crate::wallet::Result<()> { - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; for input_signing_data in inputs { let output_id = input_signing_data.output_id(); - wallet_data.locked_outputs.remove(output_id); + wallet_ledger.locked_outputs.remove(output_id); log::debug!( "[TRANSACTION] Unlocked output {} because of transaction error", output_id diff --git a/sdk/src/wallet/operations/transaction/prepare_output.rs b/sdk/src/wallet/operations/transaction/prepare_output.rs index 75da29a3a7..2d08f60fea 100644 --- a/sdk/src/wallet/operations/transaction/prepare_output.rs +++ b/sdk/src/wallet/operations/transaction/prepare_output.rs @@ -243,7 +243,7 @@ where ) } else { // Transition an existing NFT output - let unspent_nft_output = self.data().await.unspent_nft_output(nft_id).cloned(); + let unspent_nft_output = self.ledger().await.unspent_nft_output(nft_id).cloned(); // Find nft output from the inputs let mut first_output_builder = if let Some(nft_output_data) = &unspent_nft_output { diff --git a/sdk/src/wallet/operations/wait_for_tx_acceptance.rs b/sdk/src/wallet/operations/wait_for_tx_acceptance.rs index 8cd17ac7f8..7f7dc055a8 100644 --- a/sdk/src/wallet/operations/wait_for_tx_acceptance.rs +++ b/sdk/src/wallet/operations/wait_for_tx_acceptance.rs @@ -31,7 +31,7 @@ where log::debug!("[wait_for_transaction_acceptance]"); let transaction = self - .data() + .ledger() .await .transactions .get(transaction_id) diff --git a/sdk/src/wallet/storage/constants.rs b/sdk/src/wallet/storage/constants.rs index 3891fecf14..c30d664764 100644 --- a/sdk/src/wallet/storage/constants.rs +++ b/sdk/src/wallet/storage/constants.rs @@ -15,15 +15,17 @@ pub const fn default_storage_path() -> &'static str { DEFAULT_STORAGE_PATH } +// wallet db schema pub(crate) const DATABASE_SCHEMA_VERSION: u8 = 1; pub(crate) const DATABASE_SCHEMA_VERSION_KEY: &str = "database-schema-version"; -pub(crate) const WALLET_DATA_KEY: &str = "wallet-data"; +// wallet db keys +pub(crate) const WALLET_LEDGER_KEY: &str = "wallet-ledger"; pub(crate) const WALLET_BUILDER_KEY: &str = "wallet-builder"; -pub(crate) const WALLET_SYNC_OPTIONS: &str = "wallet-sync-options"; - pub(crate) const SECRET_MANAGER_KEY: &str = "secret-manager"; +pub(crate) const WALLET_SYNC_OPTIONS: &str = "wallet-sync-options"; + // #[cfg(feature = "participation")] // pub(crate) const PARTICIPATION_EVENTS: &str = "participation-events"; // #[cfg(feature = "participation")] diff --git a/sdk/src/wallet/storage/manager.rs b/sdk/src/wallet/storage/manager.rs index ec217e5365..e733144457 100644 --- a/sdk/src/wallet/storage/manager.rs +++ b/sdk/src/wallet/storage/manager.rs @@ -7,7 +7,7 @@ use crate::{ client::storage::StorageAdapter, types::TryFromDto, wallet::{ - core::{WalletData, WalletDataDto}, + core::{WalletLedger, WalletLedgerDto}, migration::migrate, operations::syncing::SyncOptions, storage::{constants::*, DynStorageAdapter, Storage}, @@ -49,25 +49,28 @@ impl StorageManager { Ok(storage_manager) } - pub(crate) async fn load_wallet_data(&self) -> crate::wallet::Result> { - if let Some(dto) = self.get::(WALLET_DATA_KEY).await? { - Ok(Some(WalletData::try_from_dto(dto)?)) + pub(crate) async fn load_wallet_ledger(&self) -> crate::wallet::Result> { + if let Some(dto) = self.get::(WALLET_LEDGER_KEY).await? { + Ok(Some(WalletLedger::try_from_dto(dto)?)) } else { Ok(None) } } - pub(crate) async fn save_wallet_data(&self, wallet_data: &WalletData) -> crate::wallet::Result<()> { - self.set(WALLET_DATA_KEY, &WalletDataDto::from(wallet_data)).await + pub(crate) async fn save_wallet_ledger(&self, wallet_ledger: &WalletLedgerDto) -> crate::wallet::Result<()> { + self.set(WALLET_LEDGER_KEY, wallet_ledger).await?; + Ok(()) } pub(crate) async fn set_default_sync_options(&self, sync_options: &SyncOptions) -> crate::wallet::Result<()> { - let key = format!("{WALLET_DATA_KEY}-{WALLET_SYNC_OPTIONS}"); + let key = format!("{WALLET_LEDGER_KEY}-{WALLET_SYNC_OPTIONS}"); self.set(&key, &sync_options).await } + // TODO: call this method in wallet builder: https://github.com/iotaledger/iota-sdk/issues/2022 + #[allow(dead_code)] pub(crate) async fn get_default_sync_options(&self) -> crate::wallet::Result> { - let key = format!("{WALLET_DATA_KEY}-{WALLET_SYNC_OPTIONS}"); + let key = format!("{WALLET_LEDGER_KEY}-{WALLET_SYNC_OPTIONS}"); self.get(&key).await } } @@ -91,12 +94,14 @@ impl StorageAdapter for StorageManager { #[cfg(test)] mod tests { + use crypto::keys::bip44::Bip44; use pretty_assertions::assert_eq; use serde::{Deserialize, Serialize}; use super::*; use crate::{ - client::secret::SecretManager, + client::{constants::SHIMMER_COIN_TYPE, secret::SecretManager}, + types::block::address::{Bech32Address, Ed25519Address}, wallet::{core::operations::storage::SaveLoadWallet, storage::adapter::memory::Memory, WalletBuilder}, }; @@ -122,15 +127,18 @@ mod tests { } #[tokio::test] - async fn save_load_wallet_data() { + async fn save_load_wallet_ledger() { let storage_manager = StorageManager::new(Memory::default(), None).await.unwrap(); - assert!(storage_manager.load_wallet_data().await.unwrap().is_none()); + assert!(storage_manager.load_wallet_ledger().await.unwrap().is_none()); - let wallet_data = WalletData::mock(); + let wallet_ledger = WalletLedger::test_instance(); - storage_manager.save_wallet_data(&wallet_data).await.unwrap(); - let wallet = storage_manager.load_wallet_data().await.unwrap(); - assert!(matches!(wallet, Some(data) if data.alias == Some("Alice".to_string()))); + storage_manager + .save_wallet_ledger(&WalletLedgerDto::from(&wallet_ledger)) + .await + .unwrap(); + + assert_eq!(storage_manager.load_wallet_ledger().await.unwrap(), Some(wallet_ledger)); } #[tokio::test] @@ -143,14 +151,24 @@ mod tests { .is_none() ); - let wallet_builder = WalletBuilder::::new(); + let wallet_address = Bech32Address::new("rms".parse().unwrap(), Ed25519Address::null()); + let wallet_bip_path = Bip44::new(SHIMMER_COIN_TYPE); + let wallet_alias = "savings".to_string(); + + let wallet_builder = WalletBuilder::::new() + .with_address(wallet_address.clone()) + .with_bip_path(wallet_bip_path) + .with_alias(wallet_alias.clone()); + wallet_builder.save(&storage_manager).await.unwrap(); - assert!( - WalletBuilder::::load(&storage_manager) - .await - .unwrap() - .is_some() - ); + let restored_wallet_builder = WalletBuilder::::load(&storage_manager) + .await + .unwrap() + .unwrap(); + + assert_eq!(restored_wallet_builder.address, Some(wallet_address)); + assert_eq!(restored_wallet_builder.bip_path, Some(wallet_bip_path)); + assert_eq!(restored_wallet_builder.alias, Some(wallet_alias)); } } diff --git a/sdk/src/wallet/types/mod.rs b/sdk/src/wallet/types/mod.rs index 55a1343151..70e0fcc1fb 100644 --- a/sdk/src/wallet/types/mod.rs +++ b/sdk/src/wallet/types/mod.rs @@ -9,6 +9,7 @@ pub mod participation; use std::str::FromStr; +use crypto::keys::bip44::Bip44; use serde::{Deserialize, Serialize}; pub use self::{ @@ -20,6 +21,7 @@ use crate::{ types::{ api::core::OutputWithMetadataResponse, block::{ + address::Bech32Address, output::{Output, OutputId, OutputIdProof, OutputMetadata}, payload::signed_transaction::{dto::SignedTransactionPayloadDto, SignedTransactionPayload, TransactionId}, protocol::{CommittableAgeRange, ProtocolParameters}, @@ -28,7 +30,6 @@ use crate::{ }, TryFromDto, }, - wallet::core::WalletData, }; /// An output with metadata @@ -55,7 +56,8 @@ impl OutputData { pub fn input_signing_data( &self, - wallet_data: &WalletData, + wallet_address: &Bech32Address, + wallet_bip_path: Option, commitment_slot_index: impl Into, committable_age_range: CommittableAgeRange, ) -> crate::wallet::Result> { @@ -65,9 +67,9 @@ impl OutputData { .ok_or(crate::client::Error::ExpirationDeadzone)?; let chain = if let Some(required_ed25519) = required_address.backing_ed25519() { - if let Some(backing_ed25519) = wallet_data.address.inner().backing_ed25519() { + if let Some(backing_ed25519) = wallet_address.inner().backing_ed25519() { if required_ed25519 == backing_ed25519 { - wallet_data.bip_path + wallet_bip_path } else { // Different ed25519 chain than the wallet one. None diff --git a/sdk/src/wallet/update.rs b/sdk/src/wallet/update.rs index 54a2fe8df1..7ea338e679 100644 --- a/sdk/src/wallet/update.rs +++ b/sdk/src/wallet/update.rs @@ -10,6 +10,7 @@ use crate::{ payload::signed_transaction::TransactionId, }, wallet::{ + core::WalletLedgerDto, types::{InclusionState, OutputData, TransactionWithMetadata}, Wallet, }, @@ -25,12 +26,35 @@ where crate::wallet::Error: From, crate::client::Error: From, { - /// Set the alias for the wallet. - pub async fn set_alias(&self, alias: &str) -> crate::wallet::Result<()> { - let mut wallet_data = self.data_mut().await; - wallet_data.alias = Some(alias.to_string()); + /// Update the wallet address with a possible new Bech32 HRP and clear the inaccessible incoming transactions. + pub(crate) async fn update_address_hrp(&self) -> crate::wallet::Result<()> { + let bech32_hrp = self.client().get_bech32_hrp().await?; + log::debug!("updating wallet with new bech32 hrp: {}", bech32_hrp); + + self.address_mut().await.hrp = bech32_hrp; + + let mut wallet_ledger = self.ledger_mut().await; + wallet_ledger.inaccessible_incoming_transactions.clear(); + #[cfg(feature = "storage")] - self.storage_manager().save_wallet_data(&wallet_data).await?; + { + log::debug!("[save] wallet ledger with updated bech32 hrp",); + self.storage_manager() + .save_wallet_ledger(&WalletLedgerDto::from(&*wallet_ledger)) + .await?; + } + + drop(wallet_ledger); + + Ok(()) + } + + /// Set the wallet alias. + pub async fn set_alias(&self, alias: &str) -> crate::wallet::Result<()> { + log::debug!("setting wallet alias to: {}", alias); + + *self.alias_mut().await = Some(alias.to_string()); + Ok(()) } @@ -40,18 +64,18 @@ where unspent_outputs: Vec, spent_or_unsynced_output_metadata_map: HashMap>, ) -> crate::wallet::Result<()> { - log::debug!("[SYNC] Update wallet with new synced transactions"); + log::debug!("[SYNC] Update wallet ledger with new synced transactions"); let network_id = self.client().get_network_id().await?; - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; // Update spent outputs for (output_id, output_metadata_response_opt) in spent_or_unsynced_output_metadata_map { // If we got the output response and it's still unspent, skip it if let Some(output_metadata_response) = output_metadata_response_opt { if output_metadata_response.is_spent() { - wallet_data.unspent_outputs.remove(&output_id); - if let Some(output_data) = wallet_data.outputs.get_mut(&output_id) { + wallet_ledger.unspent_outputs.remove(&output_id); + if let Some(output_data) = wallet_ledger.outputs.get_mut(&output_id) { output_data.metadata = output_metadata_response; } } else { @@ -60,14 +84,14 @@ where } } - if let Some(output) = wallet_data.outputs.get(&output_id) { + if let Some(output) = wallet_ledger.outputs.get(&output_id) { // Could also be outputs from other networks after we switched the node, so we check that first if output.network_id == network_id { log::debug!("[SYNC] Spent output {}", output_id); - wallet_data.locked_outputs.remove(&output_id); - wallet_data.unspent_outputs.remove(&output_id); + wallet_ledger.locked_outputs.remove(&output_id); + wallet_ledger.unspent_outputs.remove(&output_id); // Update spent data fields - if let Some(output_data) = wallet_data.outputs.get_mut(&output_id) { + if let Some(output_data) = wallet_ledger.outputs.get_mut(&output_id) { if !output_data.is_spent() { log::warn!( "[SYNC] Setting output {} as spent without having the OutputConsumptionMetadata", @@ -97,14 +121,14 @@ where // Add new synced outputs for output_data in unspent_outputs { // Insert output, if it's unknown emit the NewOutputEvent - if wallet_data + if wallet_ledger .outputs .insert(output_data.output_id, output_data.clone()) .is_none() { #[cfg(feature = "events")] { - let transaction = wallet_data + let transaction = wallet_ledger .incoming_transactions .get(output_data.output_id.transaction_id()); self.emit(WalletEvent::NewOutput(Box::new(NewOutputEvent { @@ -118,14 +142,16 @@ where } }; if !output_data.is_spent() { - wallet_data.unspent_outputs.insert(output_data.output_id, output_data); + wallet_ledger.unspent_outputs.insert(output_data.output_id, output_data); } } #[cfg(feature = "storage")] { - log::debug!("[SYNC] storing wallet with new synced data"); - self.storage_manager().save_wallet_data(&wallet_data).await?; + log::debug!("[SYNC] storing wallet ledger with new synced data"); + self.storage_manager() + .save_wallet_ledger(&WalletLedgerDto::from(&*wallet_ledger)) + .await?; } Ok(()) } @@ -139,13 +165,13 @@ where ) -> crate::wallet::Result<()> { log::debug!("[SYNC] Update wallet with new synced transactions"); - let mut wallet_data = self.data_mut().await; + let mut wallet_ledger = self.ledger_mut().await; for transaction in updated_transactions { match transaction.inclusion_state { InclusionState::Confirmed | InclusionState::Conflicting | InclusionState::UnknownPruned => { let transaction_id = transaction.payload.transaction().id(); - wallet_data.pending_transactions.remove(&transaction_id); + wallet_ledger.pending_transactions.remove(&transaction_id); log::debug!( "[SYNC] inclusion_state of {transaction_id} changed to {:?}", transaction.inclusion_state @@ -161,13 +187,13 @@ where } _ => {} } - wallet_data + wallet_ledger .transactions .insert(transaction.payload.transaction().id(), transaction.clone()); } for output_to_unlock in &spent_output_ids { - if let Some(output_data) = wallet_data.outputs.get_mut(output_to_unlock) { + if let Some(output_data) = wallet_ledger.outputs.get_mut(output_to_unlock) { if !output_data.is_spent() { log::warn!( "[SYNC] Setting output {} as spent without having the OutputConsumptionMetadata", @@ -182,13 +208,13 @@ where )); } } - wallet_data.locked_outputs.remove(output_to_unlock); - wallet_data.unspent_outputs.remove(output_to_unlock); + wallet_ledger.locked_outputs.remove(output_to_unlock); + wallet_ledger.unspent_outputs.remove(output_to_unlock); log::debug!("[SYNC] Unlocked spent output {}", output_to_unlock); } for output_to_unlock in &output_ids_to_unlock { - wallet_data.locked_outputs.remove(output_to_unlock); + wallet_ledger.locked_outputs.remove(output_to_unlock); log::debug!( "[SYNC] Unlocked unspent output {} because of a conflicting transaction", output_to_unlock @@ -197,27 +223,11 @@ where #[cfg(feature = "storage")] { - log::debug!("[SYNC] storing wallet with new synced transactions"); - self.storage_manager().save_wallet_data(&wallet_data).await?; - } - Ok(()) - } - - /// Update the wallet address with a possible new Bech32 HRP and clear the inaccessible incoming transactions. - pub(crate) async fn update_bech32_hrp(&self) -> crate::wallet::Result<()> { - let bech32_hrp = self.client().get_bech32_hrp().await?; - log::debug!("updating wallet data with new bech32 hrp: {}", bech32_hrp); - let mut wallet_data = self.data_mut().await; - - wallet_data.address.hrp = bech32_hrp; - wallet_data.inaccessible_incoming_transactions.clear(); - - #[cfg(feature = "storage")] - { - log::debug!("[save] wallet data with updated bech32 hrp",); - self.storage_manager().save_wallet_data(&wallet_data).await?; + log::debug!("[SYNC] storing wallet ledger with new synced transactions"); + self.storage_manager() + .save_wallet_ledger(&WalletLedgerDto::from(&*wallet_ledger)) + .await?; } - Ok(()) } } diff --git a/sdk/tests/client/input_selection/account_outputs.rs b/sdk/tests/client/input_selection/account_outputs.rs index 64a55c4fcb..fbaa444e85 100644 --- a/sdk/tests/client/input_selection/account_outputs.rs +++ b/sdk/tests/client/input_selection/account_outputs.rs @@ -9,7 +9,7 @@ use iota_sdk::{ secret::types::InputSigningData, }, types::block::{ - address::Address, + address::{Address, ImplicitAccountCreationAddress}, mana::ManaAllotment, output::{ feature::SenderFeature, unlock_condition::AddressUnlockCondition, AccountId, AccountOutputBuilder, @@ -2071,3 +2071,62 @@ fn min_allot_account_mana_requirement_covered_2() { ); assert_eq!(selected.transaction.outputs()[1].as_account().mana(), 0); } + +#[test] +fn implicit_account_transition() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let inputs = [BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new(Address::ImplicitAccountCreation( + ImplicitAccountCreationAddress::new( + **Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap().as_ed25519(), + ), + ))) + .with_mana(961) + .finish_output() + .unwrap()]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let input_output_id = *inputs[0].output_id(); + let account_id = AccountId::from(&input_output_id); + let outputs = vec![ + AccountOutputBuilder::new_with_amount(1_000_000, account_id) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .finish_output() + .unwrap(), + ]; + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_required_inputs(vec![input_output_id]) + .with_min_mana_allotment(account_id_1, 2) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 1); + assert!(selected.transaction.outputs()[0].is_account()); + assert_eq!(selected.transaction.allotments().len(), 1); + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, 960).unwrap() + ); + // One remainder Mana + assert_eq!(selected.transaction.outputs()[0].mana(), 1); +} diff --git a/sdk/tests/client/input_selection/basic_outputs.rs b/sdk/tests/client/input_selection/basic_outputs.rs index 6cc3658429..6958ee4b13 100644 --- a/sdk/tests/client/input_selection/basic_outputs.rs +++ b/sdk/tests/client/input_selection/basic_outputs.rs @@ -4,11 +4,16 @@ use std::str::FromStr; use iota_sdk::{ - client::api::input_selection::{Error, InputSelection, Requirement}, + client::{ + api::input_selection::{Error, InputSelection, Requirement}, + secret::types::InputSigningData, + }, types::block::{ address::{Address, AddressCapabilities, MultiAddress, RestrictedAddress, WeightedAddress}, - output::{AccountId, NftId}, + mana::ManaAllotment, + output::{unlock_condition::AddressUnlockCondition, AccountId, BasicOutputBuilder, NftId}, protocol::protocol_parameters, + rand::output::{rand_output_id_with_slot_index, rand_output_metadata_with_id}, }, }; use pretty_assertions::assert_eq; @@ -2432,3 +2437,58 @@ fn ed25519_backed_available_address() { // Provided outputs assert_eq!(selected.transaction.outputs(), outputs); } + +#[test] +fn automatic_allotment_provided_in_and_output() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + + let inputs = [BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(881) + .finish_output() + .unwrap()]; + let inputs = inputs + .into_iter() + .map(|input| InputSigningData { + output: input, + output_metadata: rand_output_metadata_with_id(rand_output_id_with_slot_index(SLOT_INDEX)), + chain: None, + }) + .collect::>(); + + let outputs = vec![ + BasicOutputBuilder::new_with_amount(1_000_000) + .add_unlock_condition(AddressUnlockCondition::new( + Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap(), + )) + .with_mana(1) + .finish_output() + .unwrap(), + ]; + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + [Address::try_from_bech32(BECH32_ADDRESS_ED25519_0).unwrap()], + SLOT_INDEX, + SLOT_COMMITMENT_ID, + protocol_parameters, + ) + .with_min_mana_allotment(account_id_1, 2) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs_data, &inputs)); + assert_eq!(selected.transaction.outputs().len(), 1); + assert!(selected.transaction.outputs().contains(&outputs[0])); + assert_eq!(selected.transaction.allotments().len(), 1); + let mana_cost = 880; + assert_eq!( + selected.transaction.allotments()[0], + ManaAllotment::new(account_id_1, mana_cost).unwrap() + ); + assert_eq!(selected.transaction.outputs()[0].mana(), 1); +} diff --git a/sdk/tests/types/transaction_id.rs b/sdk/tests/types/transaction_id.rs index a6b6190b64..4ac3265b21 100644 --- a/sdk/tests/types/transaction_id.rs +++ b/sdk/tests/types/transaction_id.rs @@ -3,13 +3,7 @@ use core::str::FromStr; -use iota_sdk::types::{ - block::payload::{ - signed_transaction::{dto::SignedTransactionPayloadDto, SignedTransactionPayload, TransactionId}, - Payload, - }, - TryFromDto, -}; +use iota_sdk::types::block::payload::signed_transaction::TransactionId; use packable::PackableExt; use pretty_assertions::assert_eq; diff --git a/sdk/tests/wallet/address_generation.rs b/sdk/tests/wallet/address_generation.rs index eccd3aa430..51fa73587c 100644 --- a/sdk/tests/wallet/address_generation.rs +++ b/sdk/tests/wallet/address_generation.rs @@ -14,10 +14,9 @@ use iota_sdk::{ client::{ constants::IOTA_COIN_TYPE, secret::{mnemonic::MnemonicSecretManager, SecretManager}, - Error as ClientError, }, types::block::address::ToBech32Ext, - wallet::{ClientOptions, Error, Result, Wallet}, + wallet::{ClientOptions, Result, Wallet}, }; use pretty_assertions::assert_eq; diff --git a/sdk/tests/wallet/backup_restore.rs b/sdk/tests/wallet/backup_restore.rs index 7fd0ceec97..45690860d3 100644 --- a/sdk/tests/wallet/backup_restore.rs +++ b/sdk/tests/wallet/backup_restore.rs @@ -98,7 +98,7 @@ // let node_dto = NodeDto::Node(Node::from(Url::parse(NODE_LOCAL).unwrap())); // assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); -// assert_eq!(wallet.address().await, restored_wallet.address().await); +// assert_eq!(wallet.address().clone(), restored_wallet.address().clone()); // // secret manager is the same // assert_eq!( @@ -196,7 +196,7 @@ // // Get wallet // let recovered_wallet = restore_wallet; -// assert_eq!(wallet.address().await, recovered_wallet.address().await); +// assert_eq!(wallet.address().clone(), recovered_wallet.address().clone()); // // secret manager is the same // assert_eq!( @@ -269,14 +269,14 @@ // // Validate restored data // // No wallet restored, because the coin type was different -// assert!(restore_wallet.get_wallet_data().await?.is_empty()); +// assert!(restore_wallet.get_wallet_ledger().await?.is_empty()); // // Restored coin type is not used and it's still the same one // let new_wallet = restore_wallet; // assert_eq!(new_wallet.data().await.coin_type(), &IOTA_COIN_TYPE); // // secret manager is the same // assert_eq!( -// new_wallet.address().await, +// new_wallet.address().clone(), // "smr1qrpwecegav7eh0z363ca69laxej64rrt4e3u0rtycyuh0mam3vq3ulygj9p" // ); @@ -352,13 +352,13 @@ // // Validate restored data // // The wallet is restored, because the coin type is the same -// let restored_wallet = restore_wallet.get_wallet_data().await?; +// let restored_wallet = restore_wallet.get_wallet_ledger().await?; // assert!(restored_wallet.is_some()); // // addresses are still there // assert_eq!( -// restored_wallet.address().await, -// wallet_before_backup.address().await +// restored_wallet.address().clone(), +// wallet_before_backup.address().clone() // ); // // compare client options, they are not restored @@ -431,10 +431,10 @@ // // Validate restored data // // No wallet restored, because the coin type was different -// let restored_wallet = restore_wallet.get_wallet_data().await?; +// let restored_wallet = restore_wallet.get_wallet_ledger().await?; // assert_eq!( -// wallet.address().await, -// restored_wallet.address().await, +// wallet.address().clone(), +// restored_wallet.address().clone(), // ); // // TODO: Restored coin type is used @@ -442,7 +442,7 @@ // assert_eq!(new_wallet.data().await.coin_type(), &SHIMMER_COIN_TYPE); // // secret manager is restored // assert_eq!( -// new_wallet.address().await, +// new_wallet.address().clone(), // "smr1qzvjvjyqxgfx4f0m3xhn2rj24e03dwsmjz082735y3wx88v2gudu2afedhu" // ); @@ -520,7 +520,7 @@ // assert!(client_options.node_manager_builder.nodes.contains(&node_dto)); // // No restored wallet because the bech32 hrp was different -// let restored_wallet = restore_wallet.get_wallet_data().await?; +// let restored_wallet = restore_wallet.get_wallet_ledger().await?; // assert!(restored_wallet.is_empty()); // // Restored coin type is used diff --git a/sdk/tests/wallet/consolidation.rs b/sdk/tests/wallet/consolidation.rs index cb0341dcd6..2ada555aba 100644 --- a/sdk/tests/wallet/consolidation.rs +++ b/sdk/tests/wallet/consolidation.rs @@ -31,7 +31,7 @@ async fn consolidation() -> Result<()> { let balance = wallet_1.sync(None).await.unwrap(); assert_eq!(balance.base_coin().available(), 10 * amount); - assert_eq!(wallet_1.data().await.unspent_outputs().len(), 10); + assert_eq!(wallet_1.ledger().await.unspent_outputs().len(), 10); let tx = wallet_1 .consolidate_outputs(ConsolidationParams::new().with_force(true)) @@ -44,7 +44,7 @@ async fn consolidation() -> Result<()> { // Balance still the same assert_eq!(balance.base_coin().available(), 10 * amount); // Only one unspent output - assert_eq!(wallet_1.data().await.unspent_outputs().len(), 1); + assert_eq!(wallet_1.ledger().await.unspent_outputs().len(), 1); tear_down(storage_path_0)?; tear_down(storage_path_1)?; diff --git a/sdk/tests/wallet/core.rs b/sdk/tests/wallet/core.rs index 8723afe641..be49f758aa 100644 --- a/sdk/tests/wallet/core.rs +++ b/sdk/tests/wallet/core.rs @@ -88,7 +88,7 @@ async fn changed_bip_path() -> Result<()> { drop(wallet); - let err = Wallet::builder() + let result = Wallet::builder() .with_secret_manager(SecretManager::Mnemonic(MnemonicSecretManager::try_from_mnemonic( mnemonic.clone(), )?)) @@ -97,13 +97,14 @@ async fn changed_bip_path() -> Result<()> { .finish() .await; - // Building the wallet with another coin type needs to return an error, because a different coin type was used in - // the existing account - let mismatch_err: Result = Err(Error::BipPathMismatch { + let _mismatch_err: Result = Err(Error::BipPathMismatch { new_bip_path: Some(Bip44::new(IOTA_COIN_TYPE)), old_bip_path: Some(Bip44::new(SHIMMER_COIN_TYPE)), }); - assert!(matches!(err, mismatch_err)); + + // Building the wallet with another coin type needs to return an error, because a different coin type was used in + // the existing account + assert!(matches!(result, _mismatch_err)); // Building the wallet with the same coin type still works assert!( diff --git a/sdk/tests/wallet/output_preparation.rs b/sdk/tests/wallet/output_preparation.rs index d4343fb8c2..a95f4262c2 100644 --- a/sdk/tests/wallet/output_preparation.rs +++ b/sdk/tests/wallet/output_preparation.rs @@ -782,7 +782,7 @@ async fn prepare_output_only_single_nft() -> Result<()> { assert_eq!(balance.nfts().len(), 1); let nft_amount = wallet_1 - .data() + .ledger() .await .unspent_outputs() .values() diff --git a/sdk/tests/wallet/syncing.rs b/sdk/tests/wallet/syncing.rs index cbafe71189..78dc36b486 100644 --- a/sdk/tests/wallet/syncing.rs +++ b/sdk/tests/wallet/syncing.rs @@ -51,7 +51,7 @@ // let wallet_0 = create_wallet_with_funds(storage_path_0, None, None, 1).await?; // let wallet_1 = make_wallet(storage_path_1, None, None).await?; -// let wallet_1_address = wallet_1.address().await; +// let wallet_1_address = wallet_1.address().clone(); // let token_supply = wallet_0.client().get_token_supply().await?; // // Only one basic output without further unlock conditions @@ -154,7 +154,7 @@ // let wallet_0 = create_wallet_with_funds(storage_path_0, None, None, 1).await?; // let wallet_1 = make_wallet(storage_path_1, None, None).await?; -// let wallet_1_address = wallet_1.address().await; +// let wallet_1_address = wallet_1.address().clone(); // let token_supply = wallet_0.client().get_token_supply().await?; @@ -203,7 +203,7 @@ // iota_sdk::client::request_funds_from_faucet( // crate::wallet::common::FAUCET_URL, -// &wallet.address().await, +// &wallet.address().clone(), // ) // .await?; diff --git a/sdk/tests/wallet/transactions.rs b/sdk/tests/wallet/transactions.rs index 30c7943c2b..8091b0e9cc 100644 --- a/sdk/tests/wallet/transactions.rs +++ b/sdk/tests/wallet/transactions.rs @@ -1,10 +1,10 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_sdk::wallet::{MintNftParams, Result, SendNftParams, SendParams, TransactionOptions}; -use pretty_assertions::assert_eq; +// use iota_sdk::wallet::{MintNftParams, Result, SendNftParams, SendParams, TransactionOptions}; +// use pretty_assertions::assert_eq; -use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; +// use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // #[ignore] // #[tokio::test] @@ -21,7 +21,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // let amount = 1_000_000; // let tx = wallet_0 -// .send_with_params([SendParams::new(amount, wallet_1.address().await)?], None) +// .send_with_params([SendParams::new(amount, wallet_1.address().clone())?], None) // .await?; // wallet_0 @@ -53,7 +53,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // vec![ // SendParams::new( // amount, -// wallet_1.address().await, +// wallet_1.address().clone(), // )?; // // Only 127, because we need one remainder // 127 @@ -133,7 +133,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // let wallet_1 = make_wallet(storage_path_1, None, None).await?; // let nft_options = [MintNftParams::new() -// .with_address(wallet_0.address().await) +// .with_address(wallet_0.address().clone()) // .with_metadata(b"some nft metadata".to_vec()) // .with_immutable_metadata(b"some immutable nft metadata".to_vec())]; @@ -146,7 +146,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // // Send to wallet 1 // let transaction = wallet_0 // .send_nft( -// [SendNftParams::new(wallet_1.address().await, nft_id)?], +// [SendNftParams::new(wallet_1.address().clone(), nft_id)?], // None, // ) // .await @@ -178,7 +178,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // let amount = 1_000_000; // let tx = wallet_0 // .send_with_params( -// [SendParams::new(amount, wallet_1.address().await)?], +// [SendParams::new(amount, wallet_1.address().clone())?], // Some(TransactionOptions { // note: Some(String::from("send_with_note")), // ..Default::default() @@ -213,7 +213,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // .send_with_params( // [SendParams::new( // 1_000_000, -// wallet_0.address().await, +// wallet_0.address().clone(), // )?], // None, // ) @@ -228,7 +228,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // // Something in the transaction must be different than in the first one, otherwise it will be the // same // one // 2_000_000, -// wallet_0.address().await, +// wallet_0.address().clone(), // )?], // None, // ) @@ -297,7 +297,7 @@ use crate::wallet::common::{make_wallet, request_funds, setup, tear_down}; // .await; // let tx = wallet_0 -// .send_with_params([SendParams::new(amount, wallet_1.address().await)?], None) +// .send_with_params([SendParams::new(amount, wallet_1.address().clone())?], None) // .await?; // let data = receiver.recv().await.expect("never received event");