diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2785476a8..8cd9a2d0b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,14 +27,14 @@ jobs: - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/rust-version-extractor/rust-version-extractor.py) - name: Build - run: cargo build --release --locked + run: cargo build --release --locked --features trezor - name: Run tests - run: cargo test --release --workspace + run: cargo test --release --workspace --features trezor - name: Run doc tests - run: cargo test --release --doc + run: cargo test --release --doc --features trezor # This test is ignored, so it needs to run separately. - name: Run mixed_sighash_types test - run: cargo test --release mixed_sighash_types + run: cargo test --release mixed_sighash_types --features trezor # This test is ignored, so it needs to run separately. - name: Run test_4opc_sequences test run: cargo test --release test_4opc_sequences -- --ignored @@ -65,14 +65,14 @@ jobs: - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/rust-version-extractor/rust-version-extractor.py) - name: Build - run: cargo build --release --locked + run: cargo build --release --locked --features trezor - name: Run tests - run: cargo test --release --workspace + run: cargo test --release --workspace --features trezor - name: Run doc tests - run: cargo test --release --doc + run: cargo test --release --doc --features trezor # This test is ignored, so it needs to run separately. - name: Run mixed_sighash_types test - run: cargo test --release mixed_sighash_types + run: cargo test --release mixed_sighash_types --features trezor # This test is ignored, so it needs to run separately. - name: Run test_4opc_sequences test run: cargo test --release test_4opc_sequences @@ -97,14 +97,14 @@ jobs: - name: Install rust run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/rust-version-extractor/rust-version-extractor.py) - name: Build - run: cargo build --release --locked + run: cargo build --release --locked --features trezor - name: Run tests - run: cargo test --release --workspace + run: cargo test --release --workspace --features trezor - name: Run doc tests - run: cargo test --release --doc + run: cargo test --release --doc --features trezor # This test is ignored, so it needs to run separately. - name: Run mixed_sighash_types test - run: cargo test --release mixed_sighash_types + run: cargo test --release mixed_sighash_types --features trezor # This test is ignored, so it needs to run separately. - name: Run test_4opc_sequences test run: cargo test --release test_4opc_sequences diff --git a/.github/workflows/release_linux.yml b/.github/workflows/release_linux.yml index 15800f9d56..cb7c9a75c0 100644 --- a/.github/workflows/release_linux.yml +++ b/.github/workflows/release_linux.yml @@ -46,7 +46,7 @@ jobs: - name: Build run: | - cargo build --release --target ${{ matrix.arch }}-unknown-linux-gnu + cargo build --release --target ${{ matrix.arch }}-unknown-linux-gnu --features trezor - name: Create Debian package for GUI run: | diff --git a/.github/workflows/release_macos.yml b/.github/workflows/release_macos.yml index 1a57394ebf..4df2d2b769 100644 --- a/.github/workflows/release_macos.yml +++ b/.github/workflows/release_macos.yml @@ -34,7 +34,7 @@ jobs: - name: Build run: | - cargo build --release --target ${{ matrix.arch }}-apple-darwin + cargo build --release --target ${{ matrix.arch }}-apple-darwin --features trezor - name: Sign and Notarize GUI env: @@ -67,4 +67,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }} - path: Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }}.zip \ No newline at end of file + path: Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }}.zip diff --git a/.github/workflows/release_windows.yml b/.github/workflows/release_windows.yml index b4a34e2ed3..023cb73688 100644 --- a/.github/workflows/release_windows.yml +++ b/.github/workflows/release_windows.yml @@ -29,7 +29,7 @@ jobs: toolchain: stable - name: Build Mintlayer Node and GUI - run: cargo build --release + run: cargo build --release --features trezor - name: Package Mintlayer Node run: | @@ -99,4 +99,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: Mintlayer_Node_GUI_win_${{ steps.get_version.outputs.VERSION }}_Setup - path: Mintlayer_Node_GUI_win_${{ steps.get_version.outputs.VERSION }}_Setup.exe \ No newline at end of file + path: Mintlayer_Node_GUI_win_${{ steps.get_version.outputs.VERSION }}_Setup.exe diff --git a/Cargo.lock b/Cargo.lock index ca961e1edf..f65459d11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -731,6 +731,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + [[package]] name = "bech32" version = "0.11.0" @@ -782,6 +788,20 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcoin" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +dependencies = [ + "bech32 0.10.0-beta", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1 0.28.2", +] + [[package]] name = "bitcoin-bech32" version = "0.13.0" @@ -1914,7 +1934,7 @@ dependencies = [ "rpc-description", "rstest", "schnorrkel", - "secp256k1", + "secp256k1 0.29.1", "serde", "serialization", "sha-1 0.10.1", @@ -3139,6 +3159,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -3147,9 +3173,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hickory-client" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab9683b08d8f8957a857b0236455d80e1886eaa8c6178af556aa7871fb61b55" +checksum = "949d2fef0bbdd31a0f6affc6bf390b4a0017492903eff6f7516cb382d9e85536" dependencies = [ "cfg-if", "data-encoding", @@ -3166,9 +3192,9 @@ dependencies = [ [[package]] name = "hickory-proto" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" dependencies = [ "async-trait", "cfg-if", @@ -3177,7 +3203,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 0.4.0", + "idna", "ipnet", "once_cell", "rand 0.8.5", @@ -3190,9 +3216,9 @@ dependencies = [ [[package]] name = "hickory-server" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be0e43c556b9b3fdb6c7c71a9a32153a2275d02419e3de809e520bfcfe40c37" +checksum = "35e6d1c2df0614595224b32479c72dd6fc82c9bda85962907c45fdb95a691489" dependencies = [ "async-trait", "bytes", @@ -3731,16 +3757,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -4169,6 +4185,18 @@ dependencies = [ "escape8259", ] +[[package]] +name = "libusb1-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -4795,6 +4823,7 @@ dependencies = [ "p2p", "rfd", "serialization", + "storage", "strum", "thiserror", "tokio", @@ -4802,6 +4831,7 @@ dependencies = [ "wallet", "wallet-cli-commands", "wallet-controller", + "wallet-storage", "wallet-types", "winres", ] @@ -5919,6 +5949,26 @@ dependencies = [ "unarray", ] +[[package]] +name = "protobuf" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-support" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +dependencies = [ + "thiserror", +] + [[package]] name = "psl" version = "2.1.67" @@ -6521,6 +6571,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rusb" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -6840,6 +6900,16 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys 0.9.2", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -6847,7 +6917,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "rand 0.8.5", - "secp256k1-sys", + "secp256k1-sys 0.10.1", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", ] [[package]] @@ -7765,6 +7844,7 @@ dependencies = [ "subsystem", "thiserror", "tokio", + "wallet-types", ] [[package]] @@ -8271,6 +8351,21 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "trezor-client" +version = "0.1.4" +source = "git+https://github.com/mintlayer/mintlayer-trezor-firmware?branch=feature/mintlayer-pk#c929c9a8f71ed9edfc679b96e3d4815fd8f3316b" +dependencies = [ + "bitcoin", + "byteorder", + "hex", + "protobuf", + "rusb", + "thiserror", + "tracing", + "unicode-normalization", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -8456,7 +8551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna", "percent-encoding", "serde", ] @@ -8620,11 +8715,13 @@ dependencies = [ name = "wallet" version = "1.0.0" dependencies = [ + "async-trait", "bip39", "chainstate", "common", "consensus", "crypto", + "futures", "hex", "itertools 0.13.0", "logging", @@ -8641,6 +8738,8 @@ dependencies = [ "tempfile", "test-utils", "thiserror", + "tokio", + "trezor-client", "tx-verifier", "utils", "utils-networking", @@ -8808,6 +8907,7 @@ dependencies = [ "rstest", "serde", "serialization", + "storage", "test-utils", "thiserror", "tokio", @@ -8841,6 +8941,7 @@ dependencies = [ "thiserror", "tokio", "tower 0.4.13", + "utils", "utils-networking", "wallet", "wallet-controller", @@ -8899,6 +9000,7 @@ dependencies = [ "utils-networking", "wallet", "wallet-controller", + "wallet-storage", "wallet-test-node", "wallet-types", ] @@ -8961,6 +9063,7 @@ dependencies = [ "storage", "test-utils", "thiserror", + "tx-verifier", "utils", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 30d658dcd6..744407c547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -273,3 +273,6 @@ overflow-checks = true [profile.test.package.script] opt-level = 2 + +[features] +trezor = [] diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index d2348aba0e..a5548e3ed0 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -31,7 +31,7 @@ use common::{ config::ChainConfig, make_order_id, output_value::OutputValue, - tokens::{make_token_id, IsTokenFrozen, TokenId, TokenIssuance}, + tokens::{get_referenced_token_ids, make_token_id, IsTokenFrozen, TokenId, TokenIssuance}, transaction::OutPointSourceId, AccountCommand, AccountNonce, AccountSpending, Block, DelegationId, Destination, GenBlock, Genesis, OrderData, OrderId, PoolId, SignedTransaction, Transaction, TxInput, TxOutput, @@ -594,63 +594,34 @@ async fn calculate_tx_fee_and_collect_token_info( let input_tasks: FuturesOrdered<_> = tx.inputs().iter().map(|input| fetch_utxo(input, db_tx)).collect(); let input_utxos: Vec> = input_tasks.try_collect().await?; - let token_ids = { - let mut token_ids = BTreeSet::new(); - for (inp, utxo) in tx.inputs().iter().zip(input_utxos.iter()) { - match inp { - TxInput::Utxo(_) => match utxo.as_ref().expect("must be present") { - TxOutput::Transfer(v, _) - | TxOutput::LockThenTransfer(v, _, _) - | TxOutput::Htlc(v, _) => match v { - OutputValue::TokenV1(token_id, _) => { - token_ids.insert(*token_id); - } - OutputValue::Coin(_) | OutputValue::TokenV0(_) => {} - }, - TxOutput::IssueNft(token_id, _, _) => { - token_ids.insert(*token_id); - } - TxOutput::CreateStakePool(_, _) - | TxOutput::Burn(_) - | TxOutput::DataDeposit(_) - | TxOutput::DelegateStaking(_, _) - | TxOutput::CreateDelegationId(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::CreateOrder(_) => {} - }, - TxInput::Account(_) => {} - TxInput::AccountCommand(_, cmd) => match cmd { - AccountCommand::MintTokens(token_id, _) - | AccountCommand::FreezeToken(token_id, _) - | AccountCommand::UnmintTokens(token_id) - | AccountCommand::UnfreezeToken(token_id) - | AccountCommand::LockTokenSupply(token_id) - | AccountCommand::ChangeTokenMetadataUri(token_id, _) - | AccountCommand::ChangeTokenAuthority(token_id, _) => { - token_ids.insert(*token_id); - } - AccountCommand::ConcludeOrder(order_id) - | AccountCommand::FillOrder(order_id, _, _) => { - let order = db_tx.get_order(*order_id).await?.expect("must exist"); - match order.ask_currency { - CoinOrTokenId::Coin => {} - CoinOrTokenId::TokenId(id) => { - token_ids.insert(id); - } - }; - match order.give_currency { - CoinOrTokenId::Coin => {} - CoinOrTokenId::TokenId(id) => { - token_ids.insert(id); - } - }; - } - }, - }; - } - token_ids - }; + + let token_ids: BTreeSet<_> = tx + .inputs() + .iter() + .zip(input_utxos.iter()) + .filter_map(|(inp, utxo)| match inp { + TxInput::Utxo(_) => Some(get_referenced_token_ids( + utxo.as_ref().expect("must be present"), + )), + TxInput::Account(_) => None, + TxInput::AccountCommand(_, cmd) => match cmd { + AccountCommand::MintTokens(token_id, _) + | AccountCommand::FreezeToken(token_id, _) + | AccountCommand::UnmintTokens(token_id) + | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::LockTokenSupply(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) + | AccountCommand::ChangeTokenAuthority(token_id, _) => { + Some(BTreeSet::from_iter([*token_id])) + } + AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => None, + }, + }) + .chain(tx.outputs().iter().map(get_referenced_token_ids)) + .fold(BTreeSet::new(), |mut x, mut y| { + x.append(&mut y); + x + }); let token_tasks: FuturesOrdered<_> = token_ids .iter() diff --git a/build-tools/code-docs/build-rpc-docs.sh b/build-tools/code-docs/build-rpc-docs.sh index e0a8e2c2b1..242db4ab86 100755 --- a/build-tools/code-docs/build-rpc-docs.sh +++ b/build-tools/code-docs/build-rpc-docs.sh @@ -5,4 +5,4 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" cd ${SCRIPT_DIR}/../../ UPDATE_EXPECT=1 cargo test -p node-daemon --test rpc_docs -UPDATE_EXPECT=1 cargo test -p wallet-rpc-daemon --test rpc_docs +UPDATE_EXPECT=1 cargo test -p wallet-rpc-daemon --test rpc_docs --features trezor diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index 7d95782a32..91cc1f60db 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -31,7 +31,7 @@ use common::{ address::{dehexify::to_dehexified_json, Address}, chain::{ tokens::{RPCTokenInfo, TokenId}, - ChainConfig, DelegationId, OrderId, PoolId, RpcOrderInfo, TxOutput, + ChainConfig, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, TxOutput, }, primitives::{Amount, BlockHeight, Id}, }; @@ -144,6 +144,15 @@ trait ChainstateRpc { #[method(name = "staker_balance")] async fn staker_balance(&self, pool_address: String) -> RpcResult>; + /// Returns the pool's decommission destination associated with the given pool address. + /// + /// Returns `None` (null) if the pool is not found. + #[method(name = "pool_decommission_destination")] + async fn pool_decommission_destination( + &self, + pool_address: String, + ) -> RpcResult>; + /// Given a pool defined by a pool address, and a delegation address, /// returns the amount of coins owned by that delegation in that pool. #[method(name = "delegation_share")] @@ -338,6 +347,25 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } + async fn pool_decommission_destination( + &self, + pool_address: String, + ) -> RpcResult> { + rpc::handle_result( + self.call(move |this| { + let chain_config = this.get_chain_config(); + let result: Result, _> = + dynamize_err(Address::::from_string(chain_config, pool_address)) + .map(|address| address.into_object()) + .and_then(|pool_id| dynamize_err(this.get_stake_pool_data(pool_id))) + .map(|pool_data| pool_data.map(|d| d.decommission_destination().clone())); + + result + }) + .await, + ) + } + async fn delegation_share( &self, pool_address: String, diff --git a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs index 537c6b6d5a..1f4c96c0fc 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/input_check/signature_only_check.rs @@ -16,7 +16,6 @@ use std::convert::Infallible; use common::chain::{ - partially_signed_transaction::PartiallySignedTransaction, signature::{inputsig::InputWitness, DestinationSigError, Transactable}, tokens::TokenId, ChainConfig, DelegationId, Destination, PoolId, SignedTransaction, TxInput, TxOutput, @@ -102,7 +101,6 @@ impl InputInfoProvider for InputVerifyContextSignature<'_, T> { // Prevent BlockRewardTransactable from being used here pub trait SignatureOnlyVerifiable {} impl SignatureOnlyVerifiable for SignedTransaction {} -impl SignatureOnlyVerifiable for PartiallySignedTransaction {} pub fn verify_tx_signature( chain_config: &ChainConfig, diff --git a/common/src/chain/tokens/rpc.rs b/common/src/chain/tokens/rpc.rs index b704404ee9..1ed5495c99 100644 --- a/common/src/chain/tokens/rpc.rs +++ b/common/src/chain/tokens/rpc.rs @@ -51,6 +51,13 @@ impl RPCTokenInfo { Self::NonFungibleToken(_) => 0, } } + + pub fn token_ticker(&self) -> &[u8] { + match self { + Self::FungibleToken(info) => info.token_ticker.as_bytes(), + Self::NonFungibleToken(info) => info.metadata.ticker.as_bytes(), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, HasValueHint)] diff --git a/common/src/chain/tokens/tokens_utils.rs b/common/src/chain/tokens/tokens_utils.rs index ffbaa60486..04caf8a3df 100644 --- a/common/src/chain/tokens/tokens_utils.rs +++ b/common/src/chain/tokens/tokens_utils.rs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::BTreeSet; + use super::{TokenData, TokenId}; use crate::{ chain::{output_value::OutputValue, AccountCommand, TxInput, TxOutput}, @@ -88,3 +90,33 @@ pub fn is_token_or_nft_issuance(output: &TxOutput) -> bool { TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => true, } } + +/// Get any token referenced by this output +/// ignore tokens V0 +pub fn get_referenced_token_ids(output: &TxOutput) -> BTreeSet { + match output { + TxOutput::Transfer(v, _) + | TxOutput::LockThenTransfer(v, _, _) + | TxOutput::Burn(v) + | TxOutput::Htlc(v, _) => referenced_token_id(v).into_iter().collect(), + | TxOutput::CreateOrder(data) => { + let mut tokens: BTreeSet<_> = referenced_token_id(data.ask()).into_iter().collect(); + tokens.extend(referenced_token_id(data.give())); + tokens + } + TxOutput::CreateStakePool(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::DataDeposit(_) + | TxOutput::IssueFungibleToken(_) => BTreeSet::new(), + TxOutput::IssueNft(token_id, _, _) => BTreeSet::from_iter([*token_id]), + } +} + +fn referenced_token_id(v: &OutputValue) -> Option { + match v { + OutputValue::Coin(_) | OutputValue::TokenV0(_) => None, + OutputValue::TokenV1(token_id, _) => Some(*token_id), + } +} diff --git a/common/src/chain/transaction/mod.rs b/common/src/chain/transaction/mod.rs index 238913cd29..8ca38c9981 100644 --- a/common/src/chain/transaction/mod.rs +++ b/common/src/chain/transaction/mod.rs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use partially_signed_transaction::PartiallySignedTransaction; use thiserror::Error; use serialization::{DirectDecode, DirectEncode}; @@ -34,7 +33,6 @@ pub use account_nonce::*; pub mod utxo_outpoint; pub use utxo_outpoint::*; -pub mod partially_signed_transaction; pub mod signed_transaction; pub mod signed_transaction_intent; @@ -105,14 +103,6 @@ impl Eq for WithId {} pub enum TransactionCreationError { #[error("The number of signatures does not match the number of inputs")] InvalidWitnessCount, - #[error("The number of input utxos does not match the number of inputs")] - InvalidInputUtxosCount, - #[error("The number of destinations does not match the number of inputs")] - InvalidDestinationsCount, - #[error("The number of htlc secrets does not match the number of inputs")] - InvalidHtlcSecretsCount, - #[error("Failed to convert partially signed tx to signed")] - FailedToConvertPartiallySignedTx(PartiallySignedTransaction), } impl Transaction { diff --git a/common/src/chain/transaction/output/output_value.rs b/common/src/chain/transaction/output/output_value.rs index 378d2d2b26..167c9954b4 100644 --- a/common/src/chain/transaction/output/output_value.rs +++ b/common/src/chain/transaction/output/output_value.rs @@ -99,4 +99,11 @@ impl RpcOutputValue { RpcOutputValue::Coin { amount } | RpcOutputValue::Token { id: _, amount } => *amount, } } + + pub fn token_id(&self) -> Option { + match self { + RpcOutputValue::Coin { amount: _ } => None, + RpcOutputValue::Token { id, amount: _ } => Some(*id), + } + } } diff --git a/common/src/chain/transaction/signed_transaction_intent.rs b/common/src/chain/transaction/signed_transaction_intent.rs index 6b68536dc5..9285393d0a 100644 --- a/common/src/chain/transaction/signed_transaction_intent.rs +++ b/common/src/chain/transaction/signed_transaction_intent.rs @@ -66,6 +66,13 @@ pub struct SignedTransactionIntent { } impl SignedTransactionIntent { + pub fn new_unchecked(signed_message: String, signatures: Vec>) -> Self { + Self { + signed_message, + signatures, + } + } + /// Create a signed intent given the id of the transaction and its input destinations. /// /// Only PublicKeyHash and PublicKey destinations are supported by this function. diff --git a/crypto/src/key/extended.rs b/crypto/src/key/extended.rs index 718c08e20b..2caac2bc9b 100644 --- a/crypto/src/key/extended.rs +++ b/crypto/src/key/extended.rs @@ -122,6 +122,12 @@ impl ExtendedPublicKey { } } + pub fn new(public_key: Secp256k1ExtendedPublicKey) -> Self { + Self { + pub_key: ExtendedPublicKeyHolder::Secp256k1Schnorr(public_key), + } + } + pub fn into_public_key(self) -> PublicKey { match self.pub_key { ExtendedPublicKeyHolder::Secp256k1Schnorr(k) => k.into_public_key().into(), diff --git a/crypto/src/key/mod.rs b/crypto/src/key/mod.rs index 83aee5a66b..db9558f801 100644 --- a/crypto/src/key/mod.rs +++ b/crypto/src/key/mod.rs @@ -30,7 +30,10 @@ pub use signature::Signature; use self::key_holder::{PrivateKeyHolder, PublicKeyHolder}; #[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] -pub enum SignatureError {} +pub enum SignatureError { + #[error("Failed to construct a valid signature")] + SignatureConstructionError, +} #[must_use] #[derive(Debug, PartialEq, Eq, Clone, Decode, Encode)] diff --git a/crypto/src/key/secp256k1/extended_keys.rs b/crypto/src/key/secp256k1/extended_keys.rs index bc87202b7a..e41186ebf1 100644 --- a/crypto/src/key/secp256k1/extended_keys.rs +++ b/crypto/src/key/secp256k1/extended_keys.rs @@ -200,6 +200,18 @@ impl Secp256k1ExtendedPublicKey { .into(), } } + + pub fn new( + derivation_path: DerivationPath, + chain_code: ChainCode, + public_key: Secp256k1PublicKey, + ) -> Self { + Self { + derivation_path, + chain_code, + public_key, + } + } } impl Derivable for Secp256k1ExtendedPublicKey { diff --git a/crypto/src/key/signature/mod.rs b/crypto/src/key/signature/mod.rs index 8bfefa9d8e..8f0cc1620a 100644 --- a/crypto/src/key/signature/mod.rs +++ b/crypto/src/key/signature/mod.rs @@ -19,6 +19,8 @@ use std::io::BufWriter; use num_derive::FromPrimitive; use serialization::{hex_encoded::HexEncoded, Decode, DecodeAll, Encode}; +use super::SignatureError; + #[derive(FromPrimitive)] pub enum SignatureKind { Secp256k1Schnorr = 0, @@ -77,6 +79,12 @@ impl Signature { Ok(decoded_sig) } + pub fn from_raw_data>(data: T) -> Result { + let decoded_sig = secp256k1::schnorr::Signature::from_slice(data.as_ref()) + .map_err(|_| SignatureError::SignatureConstructionError)?; + Ok(Self::Secp256k1Schnorr(decoded_sig)) + } + pub fn is_aggregable(&self) -> bool { match self { Self::Secp256k1Schnorr(_) => false, diff --git a/node-daemon/docs/RPC.md b/node-daemon/docs/RPC.md index e180cbe9a5..95ae1711c3 100644 --- a/node-daemon/docs/RPC.md +++ b/node-daemon/docs/RPC.md @@ -347,6 +347,25 @@ EITHER OF 2) null ``` +### Method `chainstate_pool_decommission_destination` + +Returns the pool's decommission destination associated with the given pool address. + +Returns `None` (null) if the pool is not found. + + +Parameters: +``` +{ "pool_address": string } +``` + +Returns: +``` +EITHER OF + 1) bech32 string + 2) null +``` + ### Method `chainstate_delegation_share` Given a pool defined by a pool address, and a delegation address, diff --git a/node-gui/Cargo.toml b/node-gui/Cargo.toml index 714a97cb30..3036e6d200 100644 --- a/node-gui/Cargo.toml +++ b/node-gui/Cargo.toml @@ -17,12 +17,14 @@ node-gui-backend = { path = "./backend" } node-lib = { path = "../node-lib" } node-comm = { path = "../wallet/wallet-node-client" } p2p = { path = "../p2p" } +storage = { path = "../storage" } serialization = { path = "../serialization" } utils = { path = "../utils" } wallet = { path = "../wallet" } wallet-controller = { path = "../wallet/wallet-controller" } wallet-types = { path = "../wallet/types" } wallet-cli-commands = { path = "../wallet/wallet-cli-commands"} +wallet-storage = { path = "../wallet/storage" } anyhow.workspace = true chrono.workspace = true @@ -36,3 +38,6 @@ tokio.workspace = true [target.'cfg(windows)'.build-dependencies] winres = "0.1" + +[features] +trezor = ["wallet-controller/trezor", "wallet-types/trezor", "wallet-cli-commands/trezor", "node-gui-backend/trezor"] diff --git a/node-gui/backend/Cargo.toml b/node-gui/backend/Cargo.toml index efc8cb055e..424ce99f0c 100644 --- a/node-gui/backend/Cargo.toml +++ b/node-gui/backend/Cargo.toml @@ -41,3 +41,6 @@ test-utils = { path = "../../test-utils" } rstest.workspace = true serde_json.workspace = true + +[features] +trezor = ["wallet/trezor", "wallet-controller/trezor", "wallet-types/trezor", "wallet-rpc-lib/trezor", "wallet-rpc-client/trezor", "wallet-cli-commands/trezor"] diff --git a/node-gui/backend/src/backend_impl.rs b/node-gui/backend/src/backend_impl.rs index 99e8010ae9..c5fd2350c4 100644 --- a/node-gui/backend/src/backend_impl.rs +++ b/node-gui/backend/src/backend_impl.rs @@ -36,14 +36,13 @@ use wallet_cli_commands::{ WalletCommand, }; use wallet_controller::{ - make_cold_wallet_rpc_client, types::Balances, ControllerConfig, NodeInterface, UtxoState, - WalletHandlesClient, + make_cold_wallet_rpc_client, + types::{Balances, WalletTypeArgs}, + ControllerConfig, NodeInterface, UtxoState, WalletHandlesClient, }; use wallet_rpc_client::handles_client::WalletRpcHandlesClient; -use wallet_rpc_lib::{EventStream, WalletRpc, WalletService}; -use wallet_types::{ - seed_phrase::StoreSeedPhrase, wallet_type::WalletType, with_locked::WithLocked, -}; +use wallet_rpc_lib::{types::HardwareWalletType, EventStream, WalletRpc, WalletService}; +use wallet_types::{wallet_type::WalletType, with_locked::WithLocked}; use super::{ account_id::AccountId, @@ -205,10 +204,13 @@ impl Backend { } } - async fn get_account_info( + async fn get_account_info( controller: &WalletRpc, account_index: U31, - ) -> Result<(AccountId, AccountInfo), BackendError> { + ) -> Result<(AccountId, AccountInfo), BackendError> + where + T: NodeInterface + Clone + Send + Sync + Debug + 'static, + { let name = controller .wallet_info() .await @@ -258,7 +260,7 @@ impl Backend { async fn add_create_wallet( &mut self, file_path: PathBuf, - mnemonic: wallet_controller::mnemonic::Mnemonic, + wallet_args: WalletTypeArgs, wallet_type: WalletType, import: ImportOrCreate, ) -> Result { @@ -280,7 +282,7 @@ impl Backend { .create_wallet( handles_client, file_path.clone(), - &mnemonic, + wallet_args, import, wallet_events, ) @@ -299,7 +301,13 @@ impl Backend { let client = make_cold_wallet_rpc_client(Arc::clone(&self.chain_config)); let (wallet_rpc, command_handler, best_block, accounts_info, accounts_data) = self - .create_wallet(client, file_path.clone(), &mnemonic, import, wallet_events) + .create_wallet( + client, + file_path.clone(), + wallet_args, + import, + wallet_events, + ) .await?; let wallet_data = WalletData { @@ -311,6 +319,40 @@ impl Backend { (wallet_data, accounts_info, best_block) } + #[cfg(feature = "trezor")] + (WalletType::Trezor, ColdHotNodeController::Hot(controller)) => { + let handles_client = WalletHandlesClient::new( + controller.chainstate.clone(), + controller.mempool.clone(), + controller.block_prod.clone(), + controller.p2p.clone(), + ) + .await + .map_err(|e| BackendError::WalletError(e.to_string()))?; + + let (wallet_rpc, command_handler, best_block, accounts_info, accounts_data) = self + .create_wallet( + handles_client, + file_path.clone(), + wallet_args, + import, + wallet_events, + ) + .await?; + + let wallet_data = WalletData { + controller: GuiHotColdController::Hot(wallet_rpc, command_handler), + accounts: accounts_data, + best_block, + updated: false, + }; + + (wallet_data, accounts_info, best_block) + } + #[cfg(feature = "trezor")] + (WalletType::Trezor, ColdHotNodeController::Cold) => { + return Err(BackendError::ColdTrezorNotSupported) + } (WalletType::Hot, ColdHotNodeController::Cold) => { return Err(BackendError::HotNotSupported) } @@ -332,11 +374,11 @@ impl Backend { Ok(wallet_info) } - async fn create_wallet( + async fn create_wallet( &mut self, handles_client: N, file_path: PathBuf, - mnemonic: &wallet::wallet::Mnemonic, + wallet_args: WalletTypeArgs, import: ImportOrCreate, wallet_events: GuiWalletEvents, ) -> Result< @@ -348,7 +390,10 @@ impl Backend { BTreeMap, ), BackendError, - > { + > + where + N: NodeInterface + Clone + Debug + Send + Sync + 'static, + { let wallet_service = WalletService::start( self.chain_config.clone(), None, @@ -362,14 +407,9 @@ impl Backend { let node_rpc = wallet_service.node_rpc().clone(); let chain_config = wallet_service.chain_config().clone(); let wallet_rpc = WalletRpc::new(wallet_handle, node_rpc.clone(), chain_config.clone()); + wallet_rpc - .create_wallet( - file_path, - StoreSeedPhrase::Store, - Some(mnemonic.to_string()), - None, - import.skip_syncing(), - ) + .create_wallet(file_path, wallet_args, true, !import.skip_syncing()) .await .map_err(|err| BackendError::WalletError(err.to_string()))?; tokio::spawn(forward_events( @@ -446,7 +486,9 @@ impl Backend { best_block, accounts_info, accounts_data, - ) = self.open_wallet(handles_client, file_path.clone(), wallet_events).await?; + ) = self + .open_wallet(handles_client, file_path.clone(), wallet_events, None) + .await?; let wallet_data = WalletData { controller: GuiHotColdController::Hot(wallet_rpc, command_handler), @@ -467,7 +509,7 @@ impl Backend { best_block, accounts_info, accounts_data, - ) = self.open_wallet(client, file_path.clone(), wallet_events).await?; + ) = self.open_wallet(client, file_path.clone(), wallet_events, None).await?; let wallet_data = WalletData { controller: GuiHotColdController::Cold(wallet_rpc, command_handler), @@ -478,6 +520,46 @@ impl Backend { (wallet_data, accounts_info, best_block, encryption_state) } + #[cfg(feature = "trezor")] + (WalletType::Trezor, ColdHotNodeController::Hot(controller)) => { + let handles_client = WalletHandlesClient::new( + controller.chainstate.clone(), + controller.mempool.clone(), + controller.block_prod.clone(), + controller.p2p.clone(), + ) + .await + .map_err(|e| BackendError::WalletError(e.to_string()))?; + + let ( + wallet_rpc, + command_handler, + encryption_state, + best_block, + accounts_info, + accounts_data, + ) = self + .open_wallet( + handles_client, + file_path.clone(), + wallet_events, + Some(HardwareWalletType::Trezor), + ) + .await?; + + let wallet_data = WalletData { + controller: GuiHotColdController::Hot(wallet_rpc, command_handler), + accounts: accounts_data, + best_block, + updated: false, + }; + + (wallet_data, accounts_info, best_block, encryption_state) + } + #[cfg(feature = "trezor")] + (WalletType::Trezor, ColdHotNodeController::Cold) => { + return Err(BackendError::ColdTrezorNotSupported) + } (WalletType::Hot, ColdHotNodeController::Cold) => { return Err(BackendError::HotNotSupported) } @@ -497,11 +579,12 @@ impl Backend { Ok(wallet_info) } - async fn open_wallet( + async fn open_wallet( &mut self, handles_client: N, file_path: PathBuf, wallet_events: GuiWalletEvents, + hardware_wallet: Option, ) -> Result< ( WalletRpc, @@ -512,7 +595,10 @@ impl Backend { BTreeMap, ), BackendError, - > { + > + where + N: NodeInterface + Clone + Debug + Send + Sync + 'static, + { let wallet_service = WalletService::start( self.chain_config.clone(), None, @@ -527,7 +613,7 @@ impl Backend { let chain_config = wallet_service.chain_config().clone(); let wallet_rpc = WalletRpc::new(wallet_handle, node_rpc.clone(), chain_config.clone()); wallet_rpc - .open_wallet(file_path, None, false) + .open_wallet(file_path, None, false, hardware_wallet) .await .map_err(|err| BackendError::WalletError(err.to_string()))?; tokio::spawn(forward_events( @@ -956,13 +1042,13 @@ impl Backend { Self::send_event(&self.event_tx, BackendEvent::OpenWallet(open_res)); } BackendRequest::RecoverWallet { - mnemonic, + wallet_args, file_path, import, wallet_type, } => { let import_res = - self.add_create_wallet(file_path, mnemonic, wallet_type, import).await; + self.add_create_wallet(file_path, wallet_args, wallet_type, import).await; Self::send_event(&self.event_tx, BackendEvent::ImportWallet(import_res)); } BackendRequest::CloseWallet(wallet_id) => { @@ -1250,10 +1336,13 @@ impl Backend { } } -async fn get_account_balance( +async fn get_account_balance( controller: &WalletRpc, account_index: U31, -) -> Result { +) -> Result +where + N: NodeInterface + Clone + Send + Sync + 'static, +{ controller .get_balance( account_index, @@ -1264,11 +1353,14 @@ async fn get_account_balance( .map_err(|e| BackendError::WalletError(e.to_string())) } -async fn encrypt_action( +async fn encrypt_action( action: EncryptionAction, controller: &mut WalletRpc, wallet_id: WalletId, -) -> Result<(WalletId, EncryptionState), BackendError> { +) -> Result<(WalletId, EncryptionState), BackendError> +where + T: NodeInterface + Clone + Send + Sync + 'static, +{ match action { EncryptionAction::SetPassword(password) => controller .encrypt_private_keys(password) @@ -1297,7 +1389,7 @@ async fn select_acc_and_execute_cmd( chain_config: &ChainConfig, ) -> Result where - N: NodeInterface + Clone + Send + Sync + Debug + 'static, + N: NodeInterface + Clone + Send + Sync + 'static + Debug, { c.handle_manageable_wallet_command( chain_config, diff --git a/node-gui/backend/src/error.rs b/node-gui/backend/src/error.rs index f99af2cb0f..b9444799aa 100644 --- a/node-gui/backend/src/error.rs +++ b/node-gui/backend/src/error.rs @@ -41,6 +41,8 @@ pub enum BackendError { ColdWallet, #[error("Cannot interact with a hot wallet when in Cold wallet mode")] HotNotSupported, + #[error("Cannot use a Trezor wallet in a Cold wallet mode")] + ColdTrezorNotSupported, #[error("Invalid console command: {0}")] InvalidConsoleCommand(String), #[error("Empty console command")] diff --git a/node-gui/backend/src/lib.rs b/node-gui/backend/src/lib.rs index 8994369b7c..4327f92ffe 100644 --- a/node-gui/backend/src/lib.rs +++ b/node-gui/backend/src/lib.rs @@ -182,35 +182,14 @@ pub async fn node_initialize( }); (chain_config, chain_info) } - WalletMode::Cold => { - let chain_config = Arc::new(match network { - InitNetwork::Mainnet => common::chain::config::create_mainnet(), - InitNetwork::Testnet => common::chain::config::create_testnet(), - }); - let chain_info = ChainInfo { - best_block_id: chain_config.genesis_block_id(), - best_block_height: BlockHeight::zero(), - median_time: chain_config.genesis_block().timestamp(), - best_block_timestamp: chain_config.genesis_block().timestamp(), - is_initial_block_download: false, - }; - - let manager_join_handle = tokio::spawn(async move {}); - - let backend = backend_impl::Backend::new_cold( - chain_config.clone(), - event_tx, - low_priority_event_tx, - wallet_updated_tx, - manager_join_handle, - ); - - tokio::spawn(async move { - backend_impl::run_cold(backend, request_rx, wallet_updated_rx).await; - }); - - (chain_config, chain_info) - } + WalletMode::Cold => spawn_cold_backend( + network, + event_tx, + request_rx, + low_priority_event_tx, + wallet_updated_tx, + wallet_updated_rx, + ), }; let initialized_node = InitializedNode { @@ -227,3 +206,40 @@ pub async fn node_initialize( Ok(backend_controls) } + +fn spawn_cold_backend( + network: InitNetwork, + event_tx: UnboundedSender, + request_rx: UnboundedReceiver, + low_priority_event_tx: UnboundedSender, + wallet_updated_tx: UnboundedSender, + wallet_updated_rx: UnboundedReceiver, +) -> (Arc, ChainInfo) { + let chain_config = Arc::new(match network { + InitNetwork::Mainnet => common::chain::config::create_mainnet(), + InitNetwork::Testnet => common::chain::config::create_testnet(), + }); + let chain_info = ChainInfo { + best_block_id: chain_config.genesis_block_id(), + best_block_height: BlockHeight::zero(), + median_time: chain_config.genesis_block().timestamp(), + best_block_timestamp: chain_config.genesis_block().timestamp(), + is_initial_block_download: false, + }; + + let manager_join_handle = tokio::spawn(async move {}); + + let backend = backend_impl::Backend::new_cold( + chain_config.clone(), + event_tx, + low_priority_event_tx, + wallet_updated_tx, + manager_join_handle, + ); + + tokio::spawn(async move { + backend_impl::run_cold(backend, request_rx, wallet_updated_rx).await; + }); + + (chain_config, chain_info) +} diff --git a/node-gui/backend/src/messages.rs b/node-gui/backend/src/messages.rs index de0a2a2a54..06c56e3859 100644 --- a/node-gui/backend/src/messages.rs +++ b/node-gui/backend/src/messages.rs @@ -32,7 +32,7 @@ use p2p::P2pEvent; use serialization::hex_encoded::hex_encoded_serialization; use wallet::account::transaction_list::TransactionList; use wallet_cli_commands::ConsoleCommand; -use wallet_controller::types::Balances; +use wallet_controller::types::{Balances, WalletTypeArgs}; use wallet_rpc_lib::types::PoolInfo; use wallet_types::wallet_type::WalletType; @@ -180,7 +180,7 @@ pub enum BackendRequest { // This will remove the old file if it already exists. // The frontend should check if this is what the user really wants. RecoverWallet { - mnemonic: wallet_controller::mnemonic::Mnemonic, + wallet_args: WalletTypeArgs, file_path: PathBuf, import: ImportOrCreate, wallet_type: WalletType, diff --git a/node-gui/src/main_window/main_menu.rs b/node-gui/src/main_window/main_menu.rs index e1810c8847..120d492e51 100644 --- a/node-gui/src/main_window/main_menu.rs +++ b/node-gui/src/main_window/main_menu.rs @@ -108,21 +108,21 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i labeled_button("File", MenuMessage::NoOp), Menu::new(match wallet_mode { WalletMode::Hot => { - vec![ + let menu = vec![ menu_item( - "Create new Hot wallet", + "Create new Software wallet", MenuMessage::CreateNewWallet { wallet_type: WalletType::Hot, }, ), menu_item( - "Recover Hot wallet", + "Recover Software wallet", MenuMessage::RecoverWallet { wallet_type: WalletType::Hot, }, ), menu_item( - "Open Hot wallet", + "Open Software wallet", MenuMessage::OpenWallet { wallet_type: WalletType::Hot, }, @@ -130,7 +130,41 @@ fn make_menu_file<'a>(wallet_mode: WalletMode) -> Item<'a, MenuMessage, Theme, i // TODO: enable setting when needed // menu_item("Settings", MenuMessage::NoOp), menu_item("Exit", MenuMessage::Exit), - ] + ]; + #[cfg(feature = "trezor")] + { + let mut menu = menu; + menu.insert( + 1, + menu_item( + "Create new Trezor wallet", + MenuMessage::CreateNewWallet { + wallet_type: WalletType::Trezor, + }, + ), + ); + menu.insert( + 3, + menu_item( + "Recover from Trezor wallet", + MenuMessage::RecoverWallet { + wallet_type: WalletType::Trezor, + }, + ), + ); + menu.insert( + 5, + menu_item( + "Open Trezor wallet", + MenuMessage::OpenWallet { + wallet_type: WalletType::Trezor, + }, + ), + ); + menu + } + #[cfg(not(feature = "trezor"))] + menu } WalletMode::Cold => { vec![ diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs b/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs index 5694bc9e98..dd56598773 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/left_panel.rs @@ -117,6 +117,10 @@ pub fn view_left_panel( // `next_height` is used to prevent flickering when a new block is found let show_scan_progress = match wallet_info.wallet_type { WalletType::Cold => false, + #[cfg(feature = "trezor")] + WalletType::Trezor => { + wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height + } WalletType::Hot => { wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height } @@ -174,6 +178,41 @@ pub fn view_left_panel( .spacing(10) .padding(10), match wallet_info.wallet_type { + #[cfg(feature = "trezor")] + WalletType::Trezor => { + column![ + panel_button( + "Transactions", + SelectedPanel::Transactions, + selected_panel, + TRANSACTIONS_TOOLTIP_TEXT + ), + panel_button( + "Addresses", + SelectedPanel::Addresses, + selected_panel, + ADDRESSES_TOOLTIP_TEXT + ), + panel_button( + "Send", + SelectedPanel::Send, + selected_panel, + SEND_TOOLTIP_TEXT + ), + panel_button( + "Delegation", + SelectedPanel::Delegation, + selected_panel, + DELEGATION_TOOLTIP_TEXT + ), + panel_button( + "Console", + SelectedPanel::Console, + selected_panel, + CONSOLE_TOOLTIP_TEXT, + ) + ] + } WalletType::Cold => { column![ panel_button( diff --git a/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs b/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs index a5d26f8053..71fcdb5847 100644 --- a/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs +++ b/node-gui/src/main_window/main_widget/tabs/wallet/mod.rs @@ -178,6 +178,8 @@ impl WalletTab { let selected_panel = match wallet_type { WalletType::Hot => SelectedPanel::Transactions, WalletType::Cold => SelectedPanel::Addresses, + #[cfg(feature = "trezor")] + WalletType::Trezor => SelectedPanel::Addresses, }; WalletTab { @@ -482,6 +484,8 @@ impl Tab for WalletTab { let still_syncing = match wallet_info.wallet_type { WalletType::Cold => false, + #[cfg(feature = "trezor")] + WalletType::Trezor => false, WalletType::Hot => { wallet_info.best_block.1.next_height() < node_state.chain_info.best_block_height } diff --git a/node-gui/src/main_window/mod.rs b/node-gui/src/main_window/mod.rs index f929b26cdb..1b10608c25 100644 --- a/node-gui/src/main_window/mod.rs +++ b/node-gui/src/main_window/mod.rs @@ -32,8 +32,11 @@ use node_gui_backend::{ use p2p::{net::types::services::Services, types::peer_id::PeerId, P2pEvent}; use rfd::AsyncFileDialog; use wallet_cli_commands::ConsoleCommand; -use wallet_types::wallet_type::WalletType; +use wallet_controller::types::WalletTypeArgs; +use wallet_types::{seed_phrase::StoreSeedPhrase, wallet_type::WalletType}; +#[cfg(feature = "trezor")] +use crate::widgets::create_hw_wallet::hw_wallet_create_dialog; use crate::{ main_window::{main_menu::MenuMessage, main_widget::MainWidgetMessage}, widgets::{ @@ -56,7 +59,7 @@ mod main_widget; enum ActiveDialog { None, WalletCreate { - generated_mnemonic: wallet_controller::mnemonic::Mnemonic, + wallet_args: WalletArgs, wallet_type: WalletType, }, WalletRecover { @@ -145,6 +148,15 @@ pub struct MainWindow { wallet_msg: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WalletArgs { + Software { + mnemonic: String, + }, + #[cfg(feature = "trezor")] + Trezor, +} + #[derive(Debug, Clone)] pub enum MainWindowMessage { MenuMessage(main_menu::MenuMessage), @@ -158,12 +170,12 @@ pub enum MainWindowMessage { OpenWalletFileCanceled, ImportWalletMnemonic { - mnemonic: String, + args: WalletArgs, import: ImportOrCreate, wallet_type: WalletType, }, ImportWalletFileSelected { - mnemonic: wallet_controller::mnemonic::Mnemonic, + wallet_args: WalletTypeArgs, file_path: PathBuf, import: ImportOrCreate, wallet_type: WalletType, @@ -249,11 +261,20 @@ impl MainWindow { MainWindowMessage::MenuMessage(menu_message) => match menu_message { MenuMessage::NoOp => Command::none(), MenuMessage::CreateNewWallet { wallet_type } => { - let generated_mnemonic = - wallet_controller::mnemonic::generate_new_mnemonic(self.language); + let wallet_args = match wallet_type { + WalletType::Hot | WalletType::Cold => WalletArgs::Software { + mnemonic: wallet_controller::mnemonic::generate_new_mnemonic( + self.language, + ) + .to_string(), + }, + #[cfg(feature = "trezor")] + WalletType::Trezor => WalletArgs::Trezor, + }; + self.active_dialog = ActiveDialog::WalletCreate { - generated_mnemonic, wallet_type, + wallet_args, }; Command::none() } @@ -643,48 +664,59 @@ impl MainWindow { } MainWindowMessage::ImportWalletMnemonic { - mnemonic, + args, import, wallet_type, } => { - let mnemonic_res = - wallet_controller::mnemonic::parse_mnemonic(self.language, &mnemonic); - match mnemonic_res { - Ok(mnemonic) => { - self.file_dialog_active = true; - Command::perform( - async move { - let file_opt = AsyncFileDialog::new().save_file().await; - if let Some(file) = file_opt { - log::info!("Save wallet file: {file:?}"); - MainWindowMessage::ImportWalletFileSelected { - mnemonic, - file_path: file.path().to_owned(), - import, - wallet_type, - } - } else { - MainWindowMessage::ImportWalletFileCanceled - } + let wallet_args = match args { + WalletArgs::Software { mnemonic } => { + let mnemonic_res = + wallet_controller::mnemonic::parse_mnemonic(self.language, &mnemonic); + match mnemonic_res { + Ok(mnemonic) => WalletTypeArgs::Software { + mnemonic: Some(mnemonic.to_string()), + passphrase: None, + store_seed_phrase: StoreSeedPhrase::Store, }, - identity, - ) - } - Err(err) => { - self.show_error(err.to_string()); - Command::none() + Err(err) => { + self.show_error(err.to_string()); + return Command::none(); + } + } } - } + #[cfg(feature = "trezor")] + WalletArgs::Trezor => WalletTypeArgs::Trezor, + }; + + self.file_dialog_active = true; + Command::perform( + async move { + let file_opt = AsyncFileDialog::new().save_file().await; + if let Some(file) = file_opt { + log::info!("Save wallet file: {file:?}"); + MainWindowMessage::ImportWalletFileSelected { + wallet_args, + file_path: file.path().to_owned(), + import, + wallet_type, + } + } else { + MainWindowMessage::ImportWalletFileCanceled + } + }, + identity, + ) } MainWindowMessage::ImportWalletFileSelected { - mnemonic, + wallet_args, file_path, import, wallet_type, } => { self.file_dialog_active = false; + backend_sender.send(BackendRequest::RecoverWallet { - mnemonic, + wallet_args, file_path, import, wallet_type, @@ -763,34 +795,60 @@ impl MainWindow { ActiveDialog::None => Text::new("Nothing to show").into(), ActiveDialog::WalletCreate { - generated_mnemonic, wallet_type, + wallet_args, } => { let wallet_type = *wallet_type; - wallet_mnemonic_dialog( - Some(generated_mnemonic.clone()), - Box::new(move |mnemonic| MainWindowMessage::ImportWalletMnemonic { - mnemonic, - import: ImportOrCreate::Create, - wallet_type, - }), - Box::new(|| MainWindowMessage::CloseDialog), - ) - .into() + match wallet_args { + WalletArgs::Software { mnemonic } => wallet_mnemonic_dialog( + Some(mnemonic.clone()), + Box::new(move |mnemonic| MainWindowMessage::ImportWalletMnemonic { + args: WalletArgs::Software { mnemonic }, + import: ImportOrCreate::Create, + wallet_type, + }), + Box::new(|| MainWindowMessage::CloseDialog), + ) + .into(), + #[cfg(feature = "trezor")] + WalletArgs::Trezor => hw_wallet_create_dialog( + Box::new(move || MainWindowMessage::ImportWalletMnemonic { + args: WalletArgs::Trezor, + import: ImportOrCreate::Create, + wallet_type, + }), + Box::new(|| MainWindowMessage::CloseDialog), + ImportOrCreate::Create, + ) + .into(), + } } ActiveDialog::WalletRecover { wallet_type } => { let wallet_type = *wallet_type; - wallet_mnemonic_dialog( - None, - Box::new(move |mnemonic| MainWindowMessage::ImportWalletMnemonic { - mnemonic, - import: ImportOrCreate::Import, - wallet_type, - }), - Box::new(|| MainWindowMessage::CloseDialog), - ) - .into() + match wallet_type { + WalletType::Hot | WalletType::Cold => wallet_mnemonic_dialog( + None, + Box::new(move |mnemonic| MainWindowMessage::ImportWalletMnemonic { + args: WalletArgs::Software { mnemonic }, + import: ImportOrCreate::Import, + wallet_type, + }), + Box::new(|| MainWindowMessage::CloseDialog), + ) + .into(), + #[cfg(feature = "trezor")] + WalletType::Trezor => hw_wallet_create_dialog( + Box::new(move || MainWindowMessage::ImportWalletMnemonic { + args: WalletArgs::Trezor, + import: ImportOrCreate::Import, + wallet_type, + }), + Box::new(|| MainWindowMessage::CloseDialog), + ImportOrCreate::Import, + ) + .into(), + } } ActiveDialog::WalletSetPassword { wallet_id } => { diff --git a/node-gui/src/widgets/create_hw_wallet.rs b/node-gui/src/widgets/create_hw_wallet.rs new file mode 100644 index 0000000000..fa6d4ab1ae --- /dev/null +++ b/node-gui/src/widgets/create_hw_wallet.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2021-2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use iced::{ + alignment::Horizontal, + widget::{self, container, text, Button, Component, Text}, + Element, Length, Theme, +}; +use iced_aw::Card; +use node_gui_backend::ImportOrCreate; + +pub struct CreateHwWalletDialog { + on_import: Box Message>, + on_close: Box Message>, + mode: ImportOrCreate, +} + +pub fn hw_wallet_create_dialog( + on_import: Box Message>, + on_close: Box Message>, + mode: ImportOrCreate, +) -> CreateHwWalletDialog { + CreateHwWalletDialog { + on_import, + on_close, + mode, + } +} + +#[derive(Default)] +pub struct ImportState { + importing: bool, +} + +#[derive(Clone)] +pub enum ImportEvent { + Ok, + Cancel, +} + +impl Component for CreateHwWalletDialog { + type State = ImportState; + type Event = ImportEvent; + + fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + match event { + ImportEvent::Ok => { + state.importing = true; + Some((self.on_import)()) + } + ImportEvent::Cancel => Some((self.on_close)()), + } + } + + fn view(&self, state: &Self::State) -> Element { + let button = Button::new(Text::new("Select file").horizontal_alignment(Horizontal::Center)) + .width(100.0) + .on_press(ImportEvent::Ok); + + let card = match self.mode { + ImportOrCreate::Create => Card::new( + Text::new("Create new Wallet"), + Text::new("Create a new Trezor wallet using the connected Trezor device"), + ), + ImportOrCreate::Import => Card::new( + Text::new("Recover new Wallet"), + Text::new("Recover a new wallet using the connected Trezor device"), + ), + }; + if state.importing { + card.foot(container(text("Loading...")).width(Length::Fill).center_x()) + } else { + card.foot(container(button).width(Length::Fill).center_x()) + } + .max_width(600.0) + .on_close(ImportEvent::Cancel) + .into() + } +} + +impl<'a, Message> From> + for Element<'a, Message, Theme, iced::Renderer> +where + Message: 'a, +{ + fn from(component: CreateHwWalletDialog) -> Self { + widget::component(component) + } +} diff --git a/node-gui/src/widgets/mod.rs b/node-gui/src/widgets/mod.rs index d5c81a2140..b519b51821 100644 --- a/node-gui/src/widgets/mod.rs +++ b/node-gui/src/widgets/mod.rs @@ -14,6 +14,8 @@ // limitations under the License. pub mod confirm_broadcast; +#[cfg(feature = "trezor")] +pub mod create_hw_wallet; pub mod new_wallet_account; pub mod popup_dialog; pub mod wallet_mnemonic; diff --git a/node-gui/src/widgets/wallet_mnemonic.rs b/node-gui/src/widgets/wallet_mnemonic.rs index 9d52effd86..62d210baf1 100644 --- a/node-gui/src/widgets/wallet_mnemonic.rs +++ b/node-gui/src/widgets/wallet_mnemonic.rs @@ -21,13 +21,13 @@ use iced::{ use iced_aw::Card; pub struct WalletMnemonicDialog { - generated_mnemonic_opt: Option, + generated_mnemonic_opt: Option, on_import: Box Message>, on_close: Box Message>, } pub fn wallet_mnemonic_dialog( - generated_mnemonic_opt: Option, + generated_mnemonic_opt: Option, on_import: Box Message>, on_close: Box Message>, ) -> WalletMnemonicDialog { @@ -67,7 +67,7 @@ impl Component for WalletMnemonicDialog ImportEvent::Ok => { state.importing = true; let mnemonic = match &self.generated_mnemonic_opt { - Some(generated_mnemonic) => generated_mnemonic.to_string(), + Some(generated_mnemonic) => generated_mnemonic.clone(), None => state.entered_mnemonic.clone(), }; Some((self.on_import)(mnemonic)) @@ -78,7 +78,7 @@ impl Component for WalletMnemonicDialog fn view(&self, state: &Self::State) -> Element { let (mnemonic, action_text) = match &self.generated_mnemonic_opt { - Some(generated_mnemonic) => (generated_mnemonic.to_string(), "Create"), + Some(generated_mnemonic) => (generated_mnemonic.clone(), "Create"), None => (state.entered_mnemonic.clone(), "Recover"), }; diff --git a/rpc/types/src/string.rs b/rpc/types/src/string.rs index c93ec4ee40..e609b49977 100644 --- a/rpc/types/src/string.rs +++ b/rpc/types/src/string.rs @@ -112,6 +112,10 @@ impl RpcString { self.0 } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + pub fn try_into_string(self) -> Result { String::from_utf8(self.0).map_err(|e| { let err = e.utf8_error(); diff --git a/test-rpc-functions/Cargo.toml b/test-rpc-functions/Cargo.toml index 608b31801c..731751a3b5 100644 --- a/test-rpc-functions/Cargo.toml +++ b/test-rpc-functions/Cargo.toml @@ -16,6 +16,7 @@ randomness = { path = "../randomness/" } rpc = { path = "../rpc/" } serialization = { path = "../serialization" } subsystem = { path = "../subsystem/" } +wallet-types = { path = "../wallet/types" } async-trait.workspace = true futures.workspace = true diff --git a/test-rpc-functions/src/rpc.rs b/test-rpc-functions/src/rpc.rs index dbd2ea10ec..fa3fb1e7cd 100644 --- a/test-rpc-functions/src/rpc.rs +++ b/test-rpc-functions/src/rpc.rs @@ -28,7 +28,6 @@ use common::{ EpochIndex, }, output_value::OutputValue, - partially_signed_transaction::PartiallySignedTransaction, signature::inputsig::{ arbitrary_message, authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, @@ -46,6 +45,7 @@ use serialization::{ hex_encoded::HexEncoded, Encode as _, }; +use wallet_types::partially_signed_transaction::PartiallySignedTransaction; use crate::{RpcTestFunctionsError, RpcTestFunctionsHandle}; diff --git a/test/functional/test_framework/__init__.py b/test/functional/test_framework/__init__.py index 15a2934604..674e1210b4 100644 --- a/test/functional/test_framework/__init__.py +++ b/test/functional/test_framework/__init__.py @@ -198,6 +198,31 @@ def init_mintlayer_types(): ], }, + "TokenAdditionalInfo": { + "type": "struct", + "type_mapping": [ + ["num_decimals", "u8"], + ["ticker", "Vec"], + ] + }, + + "UtxoAdditionalInfo": { + "type": "enum", + "type_mapping": [ + ["TokenInfo", "TokenAdditionalInfo"], + ["PoolInfo", "(Amount)"], + ["AnyoneCanTake", ""], # TODO + ], + }, + + "UtxoWithAdditionalInfo": { + "type": "struct", + "type_mapping": [ + ["utxo", "TxOutput"], + ["additional_info", "Option"], + ] + }, + "StandardInputSignature": { "type": "struct", "type_mapping": [ @@ -219,9 +244,10 @@ def init_mintlayer_types(): "type_mapping": [ ["tx", "TransactionV1"], ["witnesses", "Vec>"], - ["input_utxos", "Vec>"], + ["input_utxos", "Vec>"], ["destinations", "Vec>"], ["htlc_secrets", "Vec>"], + ["output_additional_infos", "Vec>"], ] }, diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index b4e111f1b7..954fbde84e 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -176,7 +176,7 @@ async def open_wallet(self, name: str, password: Optional[str] = None, force_cha async def recover_wallet(self, mnemonic: str, name: str = "recovered_wallet") -> str: wallet_file = os.path.join(self.node.datadir, name) - return await self._write_command(f"wallet-create \"{wallet_file}\" store-seed-phrase \"{mnemonic}\"\n") + return await self._write_command(f"wallet-recover \"{wallet_file}\" store-seed-phrase \"{mnemonic}\"\n") async def close_wallet(self) -> str: return await self._write_command("wallet-close\n") diff --git a/test/functional/wallet_generate_addresses.py b/test/functional/wallet_generate_addresses.py index e1242b4b28..19a9734c64 100644 --- a/test/functional/wallet_generate_addresses.py +++ b/test/functional/wallet_generate_addresses.py @@ -154,7 +154,7 @@ async def async_test(self): # close this wallet and create a new one with the new seed phrase await wallet.close_wallet() - assert_in("New wallet created successfully", await wallet.recover_wallet(seed_phrase)) + assert_in("Wallet recovered successfully", await wallet.recover_wallet(seed_phrase)) assert_in("Success", await wallet.sync()) assert_in(f"Coins amount: {len(addresses)}", await wallet.get_balance()) diff --git a/test/functional/wallet_get_address_usage.py b/test/functional/wallet_get_address_usage.py index 41ab21386a..3d4fc23ca3 100644 --- a/test/functional/wallet_get_address_usage.py +++ b/test/functional/wallet_get_address_usage.py @@ -75,7 +75,7 @@ async def async_test(self): # new wallet async with WalletCliController(node, self.config, self.log) as wallet: mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - assert_in("New wallet created successfully", await wallet.recover_wallet(mnemonic, "wallet")) + assert_in("Wallet recovered successfully", await wallet.recover_wallet(mnemonic, "wallet")) # check it is on genesis best_block_height = await wallet.get_best_block_height() diff --git a/test/functional/wallet_htlc_refund.py b/test/functional/wallet_htlc_refund.py index 9c8931fb31..8cf4309d71 100644 --- a/test/functional/wallet_htlc_refund.py +++ b/test/functional/wallet_htlc_refund.py @@ -132,7 +132,9 @@ async def async_test(self): assert_not_in("Tokens", balance) # issue a valid token - token_id, _ = (await wallet.issue_new_token("XXXX", 2, "http://uri", alice_address)) + token_ticker = "XXXX" + token_number_of_decimals = 2 + token_id, _ = (await wallet.issue_new_token(token_ticker, token_number_of_decimals, "http://uri", alice_address)) assert token_id is not None self.log.info(f"new token id: {token_id}") token_id_hex = node.test_functions_reveal_token_id(token_id) @@ -182,9 +184,10 @@ async def async_test(self): alice_refund_ptx = { 'tx': tx['transaction'], 'witnesses': [None, None], - 'input_utxos': alice_htlc_outputs, + 'input_utxos': [{'utxo': out, 'additional_info': None} for out in alice_htlc_outputs], 'destinations': [refund_dest_obj, alice_htlc_change_dest], - 'htlc_secrets': [None, None] + 'htlc_secrets': [None, None], + 'output_additional_infos': [None] } alice_refund_tx_hex = scalecodec.base.RuntimeConfiguration().create_scale_object('PartiallySignedTransaction').encode(alice_refund_ptx).to_hex()[2:] @@ -210,9 +213,10 @@ async def async_test(self): bob_refund_ptx = { 'tx': tx['transaction'], 'witnesses': [None, None], - 'input_utxos': bob_htlc_outputs, + 'input_utxos': [{'utxo': out, 'additional_info': None} for out in bob_htlc_outputs], 'destinations': [refund_dest_obj, bob_htlc_change_dest], - 'htlc_secrets': [None, None] + 'htlc_secrets': [None, None], + 'output_additional_infos': [None] } bob_refund_tx_hex = scalecodec.base.RuntimeConfiguration().create_scale_object('PartiallySignedTransaction').encode(bob_refund_ptx).to_hex()[2:] diff --git a/test/functional/wallet_recover_accounts.py b/test/functional/wallet_recover_accounts.py index bfca507e84..41a6544150 100644 --- a/test/functional/wallet_recover_accounts.py +++ b/test/functional/wallet_recover_accounts.py @@ -137,7 +137,7 @@ async def async_test(self): mnemonic = await wallet.show_seed_phrase() assert mnemonic is not None assert_in("Successfully closed the wallet", await wallet.close_wallet()) - assert_in("New wallet created successfully", await wallet.recover_wallet(mnemonic)) + assert_in("Wallet recovered successfully", await wallet.recover_wallet(mnemonic)) # sync and check that accounts are now present and with correct balances assert_in("Success", await wallet.sync()) diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index ba1d97addb..bf71692e41 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -26,8 +26,11 @@ utils-networking = { path = "../utils/networking" } utxo = { path = "../utxo" } wallet-storage = { path = "./storage" } wallet-types = { path = "./types" } +trezor-client = { git = "https://github.com/mintlayer/mintlayer-trezor-firmware", branch = "feature/mintlayer-pk", features = ["bitcoin", "mintlayer"], optional = true } +async-trait.workspace = true bip39 = { workspace = true, default-features = false, features = ["std", "zeroize"] } +futures = { workspace = true, default-features = false } hex.workspace = true itertools.workspace = true parity-scale-codec.workspace = true @@ -37,6 +40,11 @@ zeroize.workspace = true [dev-dependencies] test-utils = { path = "../test-utils" } +tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } rstest.workspace = true tempfile.workspace = true + +[features] +trezor = ["dep:trezor-client", "wallet-types/trezor"] +trezor-emulator = [] diff --git a/wallet/src/account/currency_grouper/mod.rs b/wallet/src/account/currency_grouper/mod.rs index 1139e22f19..c525e0fc44 100644 --- a/wallet/src/account/currency_grouper/mod.rs +++ b/wallet/src/account/currency_grouper/mod.rs @@ -25,7 +25,7 @@ use wallet_types::currency::Currency; use super::UtxoSelectorError; -pub(crate) fn group_outputs( +pub fn group_outputs( outputs: impl Iterator, get_tx_output: impl Fn(&T) -> &TxOutput, mut combiner: impl FnMut(&mut Grouped, &T, Amount) -> WalletResult<()>, diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 08d9781e25..410e8f1554 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -22,7 +22,6 @@ use common::address::pubkeyhash::PublicKeyHash; use common::chain::block::timestamp::BlockTimestamp; use common::chain::classic_multisig::ClassicMultisigChallenge; use common::chain::htlc::HashedTimelockContract; -use common::chain::partially_signed_transaction::PartiallySignedTransaction; use common::chain::{AccountCommand, AccountOutPoint, AccountSpending, OrderId, RpcOrderInfo}; use common::primitives::id::WithId; use common::primitives::{Idable, H256}; @@ -39,15 +38,17 @@ use utils::ensure; pub use utxo_selector::UtxoSelectorError; use wallet_types::account_id::AccountPrefixedId; use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; +use wallet_types::partially_signed_transaction::{PartiallySignedTransaction, UtxoAdditionalInfo}; use wallet_types::with_locked::WithLocked; use crate::account::utxo_selector::{select_coins, OutputGroup}; use crate::destination_getters::{get_tx_output_destination, HtlcSpendingCondition}; -use crate::key_chain::{AccountKeyChainImpl, KeyChainError}; +use crate::key_chain::{AccountKeyChains, KeyChainError, VRFAccountKeyChains}; use crate::send_request::{ make_address_output, make_address_output_from_delegation, make_address_output_token, make_decommission_stake_pool_output, make_mint_token_outputs, make_stake_output, - make_unmint_token_outputs, IssueNftArguments, SelectedInputs, StakePoolDataArguments, + make_unmint_token_outputs, IssueNftArguments, PoolOrTokenId, SelectedInputs, + StakePoolDataArguments, }; use crate::wallet::WalletPoolsFilter; use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; @@ -85,7 +86,7 @@ use wallet_types::{ }; pub use self::output_cache::{ - DelegationData, FungibleTokenInfo, PoolData, TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, + DelegationData, OwnFungibleTokenInfo, PoolData, TxInfo, UnconfirmedTokenInfo, UtxoWithTxOutput, }; use self::output_cache::{OutputCache, TokenIssuanceData}; use self::transaction_list::{get_transaction_list, TransactionList}; @@ -112,48 +113,21 @@ impl TransactionToSign { } } -pub struct Account { +pub struct Account { chain_config: Arc, - key_chain: AccountKeyChainImpl, + key_chain: K, output_cache: OutputCache, account_info: AccountInfo, } -impl Account { - pub fn load_from_database( - chain_config: Arc, - db_tx: &impl WalletStorageReadLocked, - id: &AccountId, - ) -> WalletResult { - let mut account_infos = db_tx.get_accounts_info()?; - let account_info = - account_infos.remove(id).ok_or(KeyChainError::NoAccountFound(id.clone()))?; - - let key_chain = AccountKeyChainImpl::load_from_database( - chain_config.clone(), - db_tx, - id, - &account_info, - )?; - - let txs = db_tx.get_transactions(&key_chain.get_account_id())?; - let output_cache = OutputCache::new(txs)?; - - Ok(Account { - chain_config, - key_chain, - output_cache, - account_info, - }) - } - +impl Account { /// Create a new account by providing a key chain pub fn new( chain_config: Arc, db_tx: &mut impl WalletStorageWriteLocked, - key_chain: AccountKeyChainImpl, + key_chain: K, name: Option, - ) -> WalletResult { + ) -> WalletResult { let account_id = key_chain.get_account_id(); let account_info = AccountInfo::new( @@ -181,7 +155,29 @@ impl Account { Ok(account) } - pub fn key_chain(&self) -> &AccountKeyChainImpl { + pub fn load_from_database( + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + ) -> WalletResult { + let mut account_infos = db_tx.get_accounts_info()?; + let account_info = + account_infos.remove(id).ok_or(KeyChainError::NoAccountFound(id.clone()))?; + + let key_chain = K::load_from_database(chain_config.clone(), db_tx, id, &account_info)?; + + let txs = db_tx.get_transactions(&key_chain.get_account_id())?; + let output_cache = OutputCache::new(txs)?; + + Ok(Account { + chain_config, + key_chain, + output_cache, + account_info, + }) + } + + pub fn key_chain(&self) -> &K { &self.key_chain } @@ -276,10 +272,7 @@ impl Account { ) } SelectedInputs::Inputs(ref inputs) => ( - inputs - .iter() - .map(|(outpoint, utxo)| (outpoint.clone(), (utxo, None))) - .collect(), + inputs.iter().map(|(outpoint, utxo)| (outpoint.clone(), utxo)).collect(), selection_algo, ), } @@ -450,7 +443,7 @@ impl Account { &self, fee_rates: CurrentFeeRate, pay_fee_with_currency: &Currency, - utxos: Vec<(UtxoOutPoint, (&TxOutput, Option))>, + utxos: Vec<(UtxoOutPoint, &TxOutput)>, ) -> Result>, WalletError> { let utxo_to_output_group = |(outpoint, txo): (UtxoOutPoint, TxOutput)| -> WalletResult { @@ -478,9 +471,9 @@ impl Account { currency_grouper::group_utxos_for_input( utxos.into_iter(), - |(_, (tx_output, _))| tx_output, + |(_, tx_output)| tx_output, |grouped: &mut Vec<(UtxoOutPoint, TxOutput)>, element, _| -> WalletResult<()> { - grouped.push((element.0.clone(), element.1 .0.clone())); + grouped.push((element.0.clone(), element.1.clone())); Ok(()) }, vec![], @@ -640,6 +633,7 @@ impl Account { change_addresses: BTreeMap>, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { let mut request = self.select_inputs_for_send_request( request, @@ -653,14 +647,14 @@ impl Account { )?; let fees = request.get_fees(); - let ptx = request.into_partially_signed_tx()?; + let ptx = request.into_partially_signed_tx(additional_utxo_infos)?; Ok((ptx, fees)) } pub fn process_send_request_and_sign( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, request: SendRequest, inputs: SelectedInputs, change_addresses: BTreeMap>, @@ -682,7 +676,7 @@ impl Account { fn decommission_stake_pool_impl( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, pool_id: PoolId, pool_balance: Amount, output_address: Option, @@ -745,7 +739,7 @@ impl Account { pub fn decommission_stake_pool( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, pool_id: PoolId, pool_balance: Amount, output_address: Option, @@ -762,7 +756,7 @@ impl Account { pub fn decommission_stake_pool_request( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, pool_id: PoolId, pool_balance: Amount, output_address: Option, @@ -851,31 +845,18 @@ impl Account { Ok(req) } - fn get_vrf_public_key( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - ) -> WalletResult { - Ok(self.key_chain.issue_vrf_key(db_tx)?.1.into_public_key()) - } - - pub fn get_pool_ids( - &self, - filter: WalletPoolsFilter, - db_tx: &impl WalletStorageReadUnlocked, - ) -> Vec<(PoolId, PoolData)> { + pub fn get_pool_ids(&self, filter: WalletPoolsFilter) -> Vec<(PoolId, PoolData)> { self.output_cache .pool_ids() .into_iter() .filter(|(_, pool_data)| match filter { WalletPoolsFilter::All => true, - WalletPoolsFilter::Decommission => self - .key_chain - .get_private_key_for_destination(&pool_data.decommission_key, db_tx) - .map_or(false, |res| res.is_some()), - WalletPoolsFilter::Stake => self - .key_chain - .get_private_key_for_destination(&pool_data.stake_destination, db_tx) - .map_or(false, |res| res.is_some()), + WalletPoolsFilter::Decommission => { + self.key_chain.is_destination_mine(&pool_data.decommission_key) + } + WalletPoolsFilter::Stake => { + self.key_chain.is_destination_mine(&pool_data.stake_destination) + } }) .collect() } @@ -909,7 +890,7 @@ impl Account { pub fn get_token_unconfirmed_info( &self, - token_info: &RPCFungibleTokenInfo, + token_info: RPCFungibleTokenInfo, ) -> WalletResult { self.output_cache .get_token_unconfirmed_info(token_info, |destination: &Destination| { @@ -917,67 +898,9 @@ impl Account { }) } - pub fn create_stake_pool_tx( - &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, - stake_pool_arguments: StakePoolDataArguments, - median_time: BlockTimestamp, - fee_rate: CurrentFeeRate, - ) -> WalletResult { - // TODO: Use other accounts here - let staker = Destination::PublicKey( - self.key_chain.issue_key(db_tx, KeyPurpose::ReceiveFunds)?.into_public_key(), - ); - let vrf_public_key = self.get_vrf_public_key(db_tx)?; - - // the first UTXO is needed in advance to calculate pool_id, so just make a dummy one - // and then replace it with when we can calculate the pool_id - let dummy_pool_id = PoolId::new(Uint256::from_u64(0).into()); - let dummy_stake_output = - make_stake_output(dummy_pool_id, stake_pool_arguments, staker, vrf_public_key); - let request = SendRequest::new().with_outputs([dummy_stake_output]); - let mut request = self.select_inputs_for_send_request( - request, - SelectedInputs::Utxos(vec![]), - None, - BTreeMap::new(), - db_tx, - median_time, - fee_rate, - None, - )?; - - let input0_outpoint = crate::utils::get_first_utxo_outpoint(request.inputs())?; - let new_pool_id = pos_accounting::make_pool_id(input0_outpoint); - - // update the dummy_pool_id with the new pool_id - let old_pool_id = request - .get_outputs_mut() - .iter_mut() - .find_map(|out| match out { - TxOutput::CreateStakePool(pool_id, _) if *pool_id == dummy_pool_id => Some(pool_id), - TxOutput::CreateStakePool(_, _) - | TxOutput::Burn(_) - | TxOutput::Transfer(_, _) - | TxOutput::DelegateStaking(_, _) - | TxOutput::LockThenTransfer(_, _, _) - | TxOutput::CreateDelegationId(_, _) - | TxOutput::ProduceBlockFromStake(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) - | TxOutput::DataDeposit(_) - | TxOutput::Htlc(_, _) - | TxOutput::CreateOrder(_) => None, - }) - .expect("find output with dummy_pool_id"); - *old_pool_id = new_pool_id; - - Ok(request) - } - pub fn create_htlc_tx( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, output_value: OutputValue, htlc: HashedTimelockContract, median_time: BlockTimestamp, @@ -1000,7 +923,7 @@ impl Account { pub fn create_order_tx( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, ask_value: OutputValue, give_value: OutputValue, conclude_address: Address, @@ -1026,7 +949,7 @@ impl Account { pub fn create_conclude_order_tx( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, order_id: OrderId, order_info: RpcOrderInfo, output_address: Option, @@ -1079,7 +1002,7 @@ impl Account { #[allow(clippy::too_many_arguments)] pub fn create_fill_order_tx( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, order_id: OrderId, order_info: RpcOrderInfo, fill_amount_in_ask_currency: Amount, @@ -1135,7 +1058,7 @@ impl Account { pub fn create_issue_nft_tx( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, nft_issue_arguments: IssueNftArguments, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, @@ -1196,14 +1119,14 @@ impl Account { pub fn mint_tokens( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, address: Address, amount: Amount, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, ) -> WalletResult { - let token_id = *token_info.token_id(); + let token_id = token_info.token_id(); let outputs = make_mint_token_outputs(token_id, amount, address); token_info.check_can_mint(amount)?; @@ -1224,13 +1147,13 @@ impl Account { pub fn unmint_tokens( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, amount: Amount, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, ) -> WalletResult { - let token_id = *token_info.token_id(); + let token_id = token_info.token_id(); let outputs = make_unmint_token_outputs(token_id, amount); token_info.check_can_unmint(amount)?; @@ -1251,12 +1174,12 @@ impl Account { pub fn lock_token_supply( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, ) -> WalletResult { - let token_id = *token_info.token_id(); + let token_id = token_info.token_id(); token_info.check_can_lock()?; let nonce = token_info.get_next_nonce()?; @@ -1275,7 +1198,7 @@ impl Account { pub fn freeze_token( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, is_token_unfreezable: IsTokenUnfreezable, median_time: BlockTimestamp, @@ -1286,7 +1209,7 @@ impl Account { let nonce = token_info.get_next_nonce()?; let tx_input = TxInput::AccountCommand( nonce, - AccountCommand::FreezeToken(*token_info.token_id(), is_token_unfreezable), + AccountCommand::FreezeToken(token_info.token_id(), is_token_unfreezable), ); let authority = token_info.authority()?.clone(); @@ -1302,7 +1225,7 @@ impl Account { pub fn unfreeze_token( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, @@ -1311,7 +1234,7 @@ impl Account { let nonce = token_info.get_next_nonce()?; let tx_input = - TxInput::AccountCommand(nonce, AccountCommand::UnfreezeToken(*token_info.token_id())); + TxInput::AccountCommand(nonce, AccountCommand::UnfreezeToken(token_info.token_id())); let authority = token_info.authority()?.clone(); self.change_token_supply_transaction( @@ -1326,7 +1249,7 @@ impl Account { pub fn change_token_authority( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, address: Address, median_time: BlockTimestamp, @@ -1337,7 +1260,7 @@ impl Account { let nonce = token_info.get_next_nonce()?; let tx_input = TxInput::AccountCommand( nonce, - AccountCommand::ChangeTokenAuthority(*token_info.token_id(), new_authority), + AccountCommand::ChangeTokenAuthority(token_info.token_id(), new_authority), ); let authority = token_info.authority()?.clone(); @@ -1353,7 +1276,7 @@ impl Account { pub fn change_token_metadata_uri( &mut self, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, token_info: &UnconfirmedTokenInfo, metadata_uri: Vec, median_time: BlockTimestamp, @@ -1362,7 +1285,7 @@ impl Account { let nonce = token_info.get_next_nonce()?; let tx_input = TxInput::AccountCommand( nonce, - AccountCommand::ChangeTokenMetadataUri(*token_info.token_id(), metadata_uri), + AccountCommand::ChangeTokenMetadataUri(token_info.token_id(), metadata_uri), ); let authority = token_info.authority()?.clone(); @@ -1381,7 +1304,7 @@ impl Account { authority: Destination, tx_input: TxInput, outputs: Vec, - db_tx: &mut impl WalletStorageWriteUnlocked, + db_tx: &mut impl WalletStorageWriteLocked, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, ) -> Result { @@ -1405,107 +1328,42 @@ impl Account { self.output_cache.pool_data(pool_id).is_ok() } - pub fn get_pos_gen_block_data( + pub fn find_account_destination( &self, - db_tx: &impl WalletStorageReadUnlocked, - pool_id: PoolId, - ) -> WalletResult { - let pool_data = self.output_cache.pool_data(pool_id)?; - let kernel_input: TxInput = pool_data.utxo_outpoint.clone().into(); - let stake_destination = &pool_data.stake_destination; - let kernel_input_utxo = - self.output_cache.get_txo(&pool_data.utxo_outpoint).expect("must exist"); - - let stake_private_key = self - .key_chain - .get_private_key_for_destination(stake_destination, db_tx)? - .ok_or(WalletError::KeyChainError(KeyChainError::NoPrivateKeyFound))?; - - let vrf_private_key = self - .key_chain - .get_vrf_private_key_for_public_key(&pool_data.vrf_public_key, db_tx)? - .ok_or(WalletError::KeyChainError( - KeyChainError::NoVRFPrivateKeyFound, - ))? - .private_key(); - - let data = PoSGenerateBlockInputData::new( - stake_private_key, - vrf_private_key, - pool_id, - vec![kernel_input], - vec![kernel_input_utxo.clone()], - ); - - Ok(data) + acc_outpoint: &AccountOutPoint, + ) -> WalletResult { + match acc_outpoint.account() { + AccountSpending::DelegationBalance(delegation_id, _) => self + .output_cache + .delegation_data(delegation_id) + .map(|data| data.destination.clone()) + .ok_or(WalletError::DelegationNotFound(*delegation_id)), + } } - pub fn tx_to_partially_signed_tx( + pub fn find_account_command_destination( &self, - tx: Transaction, - median_time: BlockTimestamp, - ) -> WalletResult { - let current_block_info = BlockInfo { - height: self.account_info.best_block_height(), - timestamp: median_time, - }; - - let (input_utxos, destinations) = tx - .inputs() - .iter() - .map(|tx_inp| match tx_inp { - TxInput::Utxo(outpoint) => { - // find utxo from cache - self.find_unspent_utxo_with_destination(outpoint, current_block_info) - .map(|(out, dest)| (Some(out), Some(dest))) - } - TxInput::Account(acc_outpoint) => { - // find delegation destination - match acc_outpoint.account() { - AccountSpending::DelegationBalance(delegation_id, _) => self - .output_cache - .delegation_data(delegation_id) - .map(|data| (None, Some(data.destination.clone()))) - .ok_or(WalletError::DelegationNotFound(*delegation_id)), - } - } - TxInput::AccountCommand(_, cmd) => { - match cmd { - // find authority of the token - AccountCommand::MintTokens(token_id, _) - | AccountCommand::UnmintTokens(token_id) - | AccountCommand::LockTokenSupply(token_id) - | AccountCommand::ChangeTokenAuthority(token_id, _) - | AccountCommand::ChangeTokenMetadataUri(token_id, _) - | AccountCommand::FreezeToken(token_id, _) - | AccountCommand::UnfreezeToken(token_id) => self - .output_cache - .token_data(token_id) - .map(|data| (None, Some(data.authority.clone()))) - .ok_or(WalletError::UnknownTokenId(*token_id)), - // find authority of the order - AccountCommand::ConcludeOrder(order_id) => self - .output_cache - .order_data(order_id) - .map(|data| (None, Some(data.conclude_key.clone()))) - .ok_or(WalletError::UnknownOrderId(*order_id)), - AccountCommand::FillOrder(_, _, dest) => Ok((None, Some(dest.clone()))), - } - } - }) - .collect::>>()? - .into_iter() - .unzip(); - - let num_inputs = tx.inputs().len(); - let ptx = PartiallySignedTransaction::new( - tx, - vec![None; num_inputs], - input_utxos, - destinations, - None, - )?; - Ok(ptx) + cmd: &AccountCommand, + ) -> WalletResult { + match cmd { + AccountCommand::MintTokens(token_id, _) + | AccountCommand::UnmintTokens(token_id) + | AccountCommand::LockTokenSupply(token_id) + | AccountCommand::ChangeTokenAuthority(token_id, _) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) + | AccountCommand::FreezeToken(token_id, _) + | AccountCommand::UnfreezeToken(token_id) => self + .output_cache + .token_data(token_id) + .map(|data| data.authority.clone()) + .ok_or(WalletError::UnknownTokenId(*token_id)), + AccountCommand::ConcludeOrder(order_id) => self + .output_cache + .order_data(order_id) + .map(|data| data.conclude_key.clone()) + .ok_or(WalletError::UnknownOrderId(*order_id)), + AccountCommand::FillOrder(_, _, dest) => Ok(dest.clone()), + } } pub fn find_unspent_utxo_with_destination( @@ -1513,8 +1371,7 @@ impl Account { outpoint: &UtxoOutPoint, current_block_info: BlockInfo, ) -> WalletResult<(TxOutput, Destination)> { - let (txo, _) = - self.output_cache.find_unspent_unlocked_utxo(outpoint, current_block_info)?; + let txo = self.output_cache.find_unspent_unlocked_utxo(outpoint, current_block_info)?; Ok(( txo.clone(), @@ -1592,22 +1449,6 @@ impl Account { Ok(self.key_chain.issue_address(db_tx, purpose)?) } - /// Get a new vrf key that hasn't been used before - pub fn get_new_vrf_key( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - ) -> WalletResult<(ChildNumber, Address)> { - Ok( - self.key_chain.issue_vrf_key(db_tx).map(|(child_number, vrf_key)| { - ( - child_number, - Address::new(&self.chain_config, vrf_key.public_key().clone()) - .expect("addressable"), - ) - })?, - ) - } - /// Get the corresponding public key for a given public key hash pub fn find_corresponding_pub_key( &self, @@ -1649,14 +1490,14 @@ impl Account { }; let amounts_by_currency = currency_grouper::group_utxos_for_input( self.output_cache - .utxos_with_token_ids( + .utxos( current_block_info, UtxoState::Confirmed.into(), WithLocked::Unlocked, |txo| get_utxo_type(txo).is_some() && self.is_watched_by(txo, &address), ) .into_iter(), - |(_, (tx_output, _))| tx_output, + |(_, tx_output)| tx_output, |total: &mut Amount, _, amount| -> WalletResult<()> { *total = (*total + amount).ok_or(WalletError::OutputAmountOverflow)?; Ok(()) @@ -1667,16 +1508,6 @@ impl Account { Ok((address, amounts_by_currency, standalone_key)) } - pub fn get_all_issued_vrf_public_keys( - &self, - ) -> BTreeMap, bool)> { - self.key_chain.get_all_issued_vrf_public_keys() - } - - pub fn get_legacy_vrf_public_key(&self) -> Address { - self.key_chain.get_legacy_vrf_public_key() - } - pub fn get_addresses_usage(&self) -> &KeychainUsageState { self.key_chain.get_addresses_usage_state() } @@ -1857,7 +1688,7 @@ impl Account { with_locked, ) .into_iter(), - |(_, (tx_output, _))| tx_output, + |(_, tx_output)| tx_output, |total: &mut Amount, _, amount| -> WalletResult<()> { *total = (*total + amount).ok_or(WalletError::OutputAmountOverflow)?; Ok(()) @@ -1873,20 +1704,15 @@ impl Account { median_time: BlockTimestamp, utxo_states: UtxoStates, with_locked: WithLocked, - ) -> Vec<(UtxoOutPoint, (&TxOutput, Option))> { + ) -> Vec<(UtxoOutPoint, &TxOutput)> { let current_block_info = BlockInfo { height: self.account_info.best_block_height(), timestamp: median_time, }; - self.output_cache.utxos_with_token_ids( - current_block_info, - utxo_states, - with_locked, - |txo| { - self.is_watched_multisig_output(txo) - && get_utxo_type(txo).is_some_and(|v| utxo_types.contains(v)) - }, - ) + self.output_cache.utxos(current_block_info, utxo_states, with_locked, |txo| { + self.is_watched_multisig_output(txo) + && get_utxo_type(txo).is_some_and(|v| utxo_types.contains(v)) + }) } pub fn get_utxos( @@ -1895,17 +1721,14 @@ impl Account { median_time: BlockTimestamp, utxo_states: UtxoStates, with_locked: WithLocked, - ) -> Vec<(UtxoOutPoint, (&TxOutput, Option))> { + ) -> Vec<(UtxoOutPoint, &TxOutput)> { let current_block_info = BlockInfo { height: self.account_info.best_block_height(), timestamp: median_time, }; - self.output_cache.utxos_with_token_ids( - current_block_info, - utxo_states, - with_locked, - |txo| self.is_mine(txo) && get_utxo_type(txo).is_some_and(|v| utxo_types.contains(v)), - ) + self.output_cache.utxos(current_block_info, utxo_states, with_locked, |txo| { + self.is_mine(txo) && get_utxo_type(txo).is_some_and(|v| utxo_types.contains(v)) + }) } pub fn get_transaction_list(&self, skip: usize, count: usize) -> WalletResult { @@ -2284,7 +2107,7 @@ impl Account { } } -impl common::size_estimation::DestinationInfoProvider for Account { +impl common::size_estimation::DestinationInfoProvider for Account { fn get_multisig_info( &self, destination: &Destination, @@ -2305,6 +2128,134 @@ struct PreselectedInputAmounts { pub burn: Amount, } +impl Account { + fn get_vrf_public_key( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + ) -> WalletResult { + Ok(self.key_chain.issue_vrf_key(db_tx)?.1.into_public_key()) + } + + pub fn get_all_issued_vrf_public_keys( + &self, + ) -> BTreeMap, bool)> { + self.key_chain.get_all_issued_vrf_public_keys() + } + + pub fn get_legacy_vrf_public_key(&self) -> Address { + self.key_chain.get_legacy_vrf_public_key() + } + + /// Get a new vrf key that hasn't been used before + pub fn get_new_vrf_key( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + ) -> WalletResult<(ChildNumber, Address)> { + Ok( + self.key_chain.issue_vrf_key(db_tx).map(|(child_number, vrf_key)| { + ( + child_number, + Address::new(&self.chain_config, vrf_key.public_key().clone()) + .expect("addressable"), + ) + })?, + ) + } + + pub fn get_pos_gen_block_data( + &self, + db_tx: &impl WalletStorageReadUnlocked, + pool_id: PoolId, + ) -> WalletResult { + let pool_data = self.output_cache.pool_data(pool_id)?; + let kernel_input: TxInput = pool_data.utxo_outpoint.clone().into(); + let stake_destination = &pool_data.stake_destination; + let kernel_input_utxo = + self.output_cache.get_txo(&pool_data.utxo_outpoint).expect("must exist"); + + let stake_private_key = self + .key_chain + .get_private_key_for_destination(stake_destination, db_tx)? + .ok_or(WalletError::KeyChainError(KeyChainError::NoPrivateKeyFound))?; + + let vrf_private_key = self + .key_chain + .get_vrf_private_key_for_public_key(&pool_data.vrf_public_key, db_tx)? + .ok_or(WalletError::KeyChainError( + KeyChainError::NoVRFPrivateKeyFound, + ))? + .private_key(); + + let data = PoSGenerateBlockInputData::new( + stake_private_key, + vrf_private_key, + pool_id, + vec![kernel_input], + vec![kernel_input_utxo.clone()], + ); + + Ok(data) + } + + pub fn create_stake_pool_tx( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + stake_pool_arguments: StakePoolDataArguments, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> WalletResult { + // TODO: Use other accounts here + let staker = Destination::PublicKey( + self.key_chain.issue_key(db_tx, KeyPurpose::ReceiveFunds)?.into_public_key(), + ); + let vrf_public_key = self.get_vrf_public_key(db_tx)?; + + // the first UTXO is needed in advance to calculate pool_id, so just make a dummy one + // and then replace it with when we can calculate the pool_id + let dummy_pool_id = PoolId::new(Uint256::from_u64(0).into()); + let dummy_stake_output = + make_stake_output(dummy_pool_id, stake_pool_arguments, staker, vrf_public_key); + let request = SendRequest::new().with_outputs([dummy_stake_output]); + let mut request = self.select_inputs_for_send_request( + request, + SelectedInputs::Utxos(vec![]), + None, + BTreeMap::new(), + db_tx, + median_time, + fee_rate, + None, + )?; + + let input0_outpoint = crate::utils::get_first_utxo_outpoint(request.inputs())?; + let new_pool_id = pos_accounting::make_pool_id(input0_outpoint); + + // update the dummy_pool_id with the new pool_id + let old_pool_id = request + .get_outputs_mut() + .iter_mut() + .find_map(|out| match out { + TxOutput::CreateStakePool(pool_id, _) if *pool_id == dummy_pool_id => Some(pool_id), + TxOutput::CreateStakePool(_, _) + | TxOutput::Burn(_) + | TxOutput::Transfer(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::ProduceBlockFromStake(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::DataDeposit(_) + | TxOutput::Htlc(_, _) + | TxOutput::CreateOrder(_) => None, + }) + .expect("find output with dummy_pool_id"); + *old_pool_id = new_pool_id; + + Ok(request) + } +} + /// There are some preselected inputs like the Token account inputs with a nonce /// that need to be included in the request /// Here we group them up by currency and sum the total amount and fee they bring to the diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index b6f09c6892..6c81c008f8 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -26,9 +26,9 @@ use common::{ output_value::OutputValue, stakelock::StakePoolData, tokens::{ - is_token_or_nft_issuance, make_token_id, IsTokenFreezable, IsTokenUnfreezable, - RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCTokenTotalSupply, TokenId, TokenIssuance, - TokenTotalSupply, + get_referenced_token_ids, make_token_id, IsTokenFreezable, IsTokenUnfreezable, + RPCFungibleTokenInfo, RPCIsTokenFrozen, RPCNonFungibleTokenInfo, RPCTokenTotalSupply, + TokenId, TokenIssuance, TokenTotalSupply, }, AccountCommand, AccountNonce, AccountSpending, DelegationId, Destination, GenBlock, OrderId, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, @@ -51,7 +51,7 @@ use wallet_types::{ use crate::{destination_getters::get_all_tx_output_destinations, WalletError, WalletResult}; -pub type UtxoWithTxOutput<'a> = (UtxoOutPoint, (&'a TxOutput, Option)); +pub type UtxoWithTxOutput<'a> = (UtxoOutPoint, &'a TxOutput); #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct TxInfo { @@ -261,33 +261,83 @@ impl TokenCurrentSupplyState { } } -pub struct FungibleTokenInfo { +pub struct OwnFungibleTokenInfo { frozen: TokenFreezableState, last_nonce: Option, total_supply: TokenCurrentSupplyState, authority: Destination, + num_decimals: u8, + ticker: Vec, +} + +pub struct FungibleTokenInfo { + frozen: TokenFreezableState, + num_decimals: u8, + ticker: Vec, +} + +impl From for FungibleTokenInfo { + fn from(value: RPCFungibleTokenInfo) -> Self { + Self { + frozen: value.frozen.into(), + num_decimals: value.number_of_decimals, + ticker: value.token_ticker.into_bytes(), + } + } +} + +pub struct NonFungibleTokenInfo { + ticker: Vec, +} + +impl From<&RPCNonFungibleTokenInfo> for NonFungibleTokenInfo { + fn from(value: &RPCNonFungibleTokenInfo) -> Self { + Self { + ticker: value.metadata.ticker.as_bytes().to_vec(), + } + } } +/// Token info from the Node + any unconfirmed Txs from this wallet pub enum UnconfirmedTokenInfo { - OwnFungibleToken(TokenId, FungibleTokenInfo), - FungibleToken(TokenId, TokenFreezableState), - NonFungibleToken(TokenId), + /// Token info owned by this wallet + OwnFungibleToken(TokenId, OwnFungibleTokenInfo), + /// Token info not owned by this wallet + FungibleToken(TokenId, FungibleTokenInfo), + /// NFT info + NonFungibleToken(TokenId, NonFungibleTokenInfo), } impl UnconfirmedTokenInfo { - pub fn token_id(&self) -> &TokenId { + pub fn token_id(&self) -> TokenId { match self { Self::OwnFungibleToken(token_id, _) | Self::FungibleToken(token_id, _) - | Self::NonFungibleToken(token_id) => token_id, + | Self::NonFungibleToken(token_id, _) => *token_id, + } + } + + pub fn num_decimals(&self) -> u8 { + match self { + Self::OwnFungibleToken(_, info) => info.num_decimals, + Self::FungibleToken(_, info) => info.num_decimals, + Self::NonFungibleToken(_, _) => 0, + } + } + + pub fn token_ticker(&self) -> &[u8] { + match self { + Self::OwnFungibleToken(_, info) => &info.ticker, + Self::FungibleToken(_, info) => &info.ticker, + Self::NonFungibleToken(_, info) => &info.ticker, } } pub fn check_can_be_used(&self) -> WalletResult<()> { match self { Self::OwnFungibleToken(_, state) => state.frozen.check_can_be_used(), - Self::FungibleToken(_, state) => state.check_can_be_used(), - Self::NonFungibleToken(_) => Ok(()), + Self::FungibleToken(_, state) => state.frozen.check_can_be_used(), + Self::NonFungibleToken(_, _) => Ok(()), } } @@ -297,7 +347,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -309,7 +359,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -324,7 +374,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -336,7 +386,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -348,7 +398,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -360,7 +410,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -372,7 +422,7 @@ impl UnconfirmedTokenInfo { Self::FungibleToken(token_id, _) => { Err(WalletError::CannotChangeNotOwnedToken(*token_id)) } - Self::NonFungibleToken(token_id) => { + Self::NonFungibleToken(token_id, _) => { Err(WalletError::CannotChangeNonFungibleToken(*token_id)) } } @@ -383,7 +433,7 @@ impl UnconfirmedTokenInfo { match self { Self::OwnFungibleToken(_, state) => Some(state.total_supply.current_supply()), Self::FungibleToken(_, _) => None, - Self::NonFungibleToken(_) => None, + Self::NonFungibleToken(_, _) => None, } } } @@ -627,7 +677,7 @@ impl OutputCache { pub fn get_token_unconfirmed_info bool>( &self, - token_info: &RPCFungibleTokenInfo, + token_info: RPCFungibleTokenInfo, is_mine: F, ) -> WalletResult { let token_data = match self.token_issuance.get(&token_info.token_id) { @@ -635,7 +685,7 @@ impl OutputCache { if !is_mine(&token_data.authority) { return Ok(UnconfirmedTokenInfo::FungibleToken( token_info.token_id, - token_info.frozen.into(), + token_info.into(), )); } token_data @@ -644,7 +694,7 @@ impl OutputCache { None => { return Ok(UnconfirmedTokenInfo::FungibleToken( token_info.token_id, - token_info.frozen.into(), + token_info.into(), )); } }; @@ -671,11 +721,13 @@ impl OutputCache { Ok(UnconfirmedTokenInfo::OwnFungibleToken( token_info.token_id, - FungibleTokenInfo { + OwnFungibleTokenInfo { frozen: frozen_state, last_nonce: token_data.last_nonce, total_supply, authority: token_data.authority.clone(), + num_decimals: token_info.number_of_decimals, + ticker: token_info.token_ticker.into_bytes(), }, )) } @@ -1175,7 +1227,7 @@ impl OutputCache { &self, utxo: &UtxoOutPoint, current_block_info: BlockInfo, - ) -> WalletResult<(&TxOutput, Option)> { + ) -> WalletResult<&TxOutput> { let tx = self .txs .get(&utxo.source_id()) @@ -1210,14 +1262,7 @@ impl OutputCache { WalletError::TokenV0Utxo(utxo.clone()) ); - let token_id = match tx { - WalletTx::Tx(tx_data) => is_token_or_nft_issuance(output) - .then_some(make_token_id(tx_data.get_transaction().inputs())) - .flatten(), - WalletTx::Block(_) => None, - }; - - Ok((output, token_id)) + Ok(output) } pub fn find_used_tokens( @@ -1225,14 +1270,14 @@ impl OutputCache { current_block_info: BlockInfo, inputs: &[UtxoOutPoint], ) -> WalletResult> { - inputs - .iter() - .filter_map(|utxo| { - self.find_unspent_unlocked_utxo(utxo, current_block_info) - .map(|(_, token_id)| token_id) - .transpose() - }) - .collect() + inputs.iter().try_fold(BTreeSet::new(), |mut token_ids, utxo| { + let new_ids = self + .find_unspent_unlocked_utxo(utxo, current_block_info) + .map(get_referenced_token_ids)?; + token_ids.extend(new_ids); + + Ok(token_ids) + }) } pub fn find_utxos( @@ -1249,13 +1294,13 @@ impl OutputCache { .collect() } - pub fn utxos_with_token_ids bool>( + pub fn utxos bool>( &self, current_block_info: BlockInfo, utxo_states: UtxoStates, locked_state: WithLocked, output_filter: F, - ) -> Vec<(UtxoOutPoint, (&TxOutput, Option))> { + ) -> Vec<(UtxoOutPoint, &TxOutput)> { let output_filter = &output_filter; self.txs .values() @@ -1279,15 +1324,7 @@ impl OutputCache { && !is_v0_token_output(output) && output_filter(output) }) - .map(move |(output, outpoint)| { - let token_id = match tx { - WalletTx::Tx(tx_data) => is_token_or_nft_issuance(output) - .then_some(make_token_id(tx_data.get_transaction().inputs())) - .flatten(), - WalletTx::Block(_) => None, - }; - (outpoint, (output, token_id)) - }) + .map(|(output, outpoint)| (outpoint, output)) }) .collect() } diff --git a/wallet/src/account/transaction_list/mod.rs b/wallet/src/account/transaction_list/mod.rs index a4bb4705af..0d4089f2fc 100644 --- a/wallet/src/account/transaction_list/mod.rs +++ b/wallet/src/account/transaction_list/mod.rs @@ -23,10 +23,10 @@ use serde::Serialize; use wallet_types::{ currency::Currency, wallet_tx::{TxData, TxState}, - KeyPurpose, WalletTx, + WalletTx, }; -use crate::{key_chain::AccountKeyChainImpl, WalletError, WalletResult}; +use crate::{key_chain::AccountKeyChains, WalletError, WalletResult}; use super::{currency_grouper::group_outputs, output_cache::OutputCache}; @@ -110,11 +110,11 @@ fn compare_tx_ref(a: &TxRef, b: &TxRef) -> Ordering { } } -fn own_output(key_chain: &AccountKeyChainImpl, output: &TxOutput) -> bool { +fn own_output(key_chain: &impl AccountKeyChains, output: &TxOutput) -> bool { match output { - TxOutput::Transfer(_, dest) | TxOutput::LockThenTransfer(_, dest, _) => KeyPurpose::ALL - .iter() - .any(|purpose| key_chain.get_leaf_key_chain(*purpose).is_destination_mine(dest)), + TxOutput::Transfer(_, dest) | TxOutput::LockThenTransfer(_, dest, _) => { + key_chain.is_destination_mine(dest) + } TxOutput::Burn(_) | TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) @@ -129,7 +129,7 @@ fn own_output(key_chain: &AccountKeyChainImpl, output: &TxOutput) -> bool { } fn own_input<'a>( - key_chain: &AccountKeyChainImpl, + key_chain: &impl AccountKeyChains, output_cache: &'a OutputCache, input: &TxInput, ) -> Option<&'a TxOutput> { @@ -146,7 +146,7 @@ fn own_input<'a>( } fn get_transaction( - key_chain: &AccountKeyChainImpl, + key_chain: &impl AccountKeyChains, output_cache: &OutputCache, tx_data: &TxData, ) -> WalletResult { @@ -213,7 +213,7 @@ fn get_transaction( } pub fn get_transaction_list( - key_chain: &AccountKeyChainImpl, + key_chain: &impl AccountKeyChains, output_cache: &OutputCache, skip: usize, count: usize, diff --git a/wallet/src/key_chain/account_key_chain/mod.rs b/wallet/src/key_chain/account_key_chain/mod.rs index 1876df76cf..3316de7a76 100644 --- a/wallet/src/key_chain/account_key_chain/mod.rs +++ b/wallet/src/key_chain/account_key_chain/mod.rs @@ -23,7 +23,6 @@ use common::chain::{ChainConfig, Destination}; use crypto::key::extended::{ExtendedPrivateKey, ExtendedPublicKey}; use crypto::key::hdkd::child_number::ChildNumber; use crypto::key::hdkd::derivable::Derivable; -use crypto::key::hdkd::derivation_path::DerivationPath; use crypto::key::hdkd::u31::U31; use crypto::key::{PrivateKey, PublicKey}; use crypto::vrf::{ExtendedVRFPrivateKey, ExtendedVRFPublicKey, VRFPublicKey}; @@ -41,11 +40,13 @@ use wallet_types::account_info::{ use wallet_types::keys::KeyPurpose; use wallet_types::{AccountId, AccountInfo, KeychainUsageState}; -use super::vrf_key_chain::VrfKeySoftChain; -use super::{make_path_to_vrf_key, AccountKeyChains, MasterKeyChain, VRF_INDEX}; +use super::vrf_key_chain::{EmptyVrfKeyChain, VrfKeyChain, VrfKeySoftChain}; +use super::{ + make_path_to_vrf_key, AccountKeyChains, MasterKeyChain, VRFAccountKeyChains, VRF_INDEX, +}; /// This key chain contains a pool of pre-generated keys and addresses for the usage in a wallet -pub struct AccountKeyChainImpl { +pub struct AccountKeyChainImpl { chain_config: Arc, account_index: U31, @@ -53,14 +54,11 @@ pub struct AccountKeyChainImpl { /// The account public key from which all the addresses are derived account_public_key: ConstValue, - /// The account vrf public key from which all the pool addresses are derived - account_vrf_public_key: ConstValue, - /// Key chains for receiving and change funds sub_chains: WithPurpose, /// VRF key chain - vrf_chain: VrfKeySoftChain, + vrf_chain: V, /// Standalone watch only keys added by the user not derived from this account's chain standalone_watch_only_keys: BTreeMap, @@ -75,7 +73,7 @@ pub struct AccountKeyChainImpl { lookahead_size: ConstValue, } -impl AccountKeyChainImpl { +impl AccountKeyChainImpl { pub fn new_from_root_key( chain_config: Arc, db_tx: &mut impl WalletStorageWriteLocked, @@ -83,7 +81,7 @@ impl AccountKeyChainImpl { root_vrf_key: ExtendedVRFPrivateKey, account_index: U31, lookahead_size: u32, - ) -> KeyChainResult { + ) -> KeyChainResult { let account_path = make_account_path(&chain_config, account_index); let account_privkey = root_key.derive_absolute_path(&account_path)?; @@ -138,11 +136,10 @@ impl AccountKeyChainImpl { ); vrf_chain.save_usage_state(db_tx)?; - let mut new_account = AccountKeyChainImpl { + let mut new_account = Self { chain_config, account_index, account_public_key: account_pubkey.into(), - account_vrf_public_key: account_vrf_pub_key.into(), sub_chains, vrf_chain, standalone_watch_only_keys: BTreeMap::new(), @@ -155,7 +152,61 @@ impl AccountKeyChainImpl { Ok(new_account) } +} + +impl AccountKeyChainImpl { + pub fn new_from_hardware_key( + chain_config: Arc, + db_tx: &mut impl WalletStorageWriteLocked, + account_pubkey: ExtendedPublicKey, + account_index: U31, + lookahead_size: u32, + ) -> KeyChainResult { + let account_id = AccountId::new_from_xpub(&account_pubkey); + let receiving_key_chain = LeafKeySoftChain::new_empty( + chain_config.clone(), + account_id.clone(), + KeyPurpose::ReceiveFunds, + account_pubkey + .clone() + .derive_child(KeyPurpose::ReceiveFunds.get_deterministic_index())?, + ); + receiving_key_chain.save_usage_state(db_tx)?; + + let change_key_chain = LeafKeySoftChain::new_empty( + chain_config.clone(), + account_id.clone(), + KeyPurpose::Change, + account_pubkey + .clone() + .derive_child(KeyPurpose::Change.get_deterministic_index())?, + ); + change_key_chain.save_usage_state(db_tx)?; + + let sub_chains = WithPurpose::new(receiving_key_chain, change_key_chain); + + let vrf_chain = EmptyVrfKeyChain {}; + + let mut new_account = Self { + chain_config, + account_index, + account_public_key: account_pubkey.into(), + sub_chains, + vrf_chain, + standalone_watch_only_keys: BTreeMap::new(), + standalone_multisig_keys: BTreeMap::new(), + standalone_private_keys: BTreeMap::new(), + lookahead_size: lookahead_size.into(), + }; + + new_account.top_up_all(db_tx)?; + + Ok(new_account) + } +} + +impl AccountKeyChainImpl { fn derive_account_private_key( &self, db_tx: &impl WalletStorageReadUnlocked, @@ -177,8 +228,58 @@ impl AccountKeyChainImpl { Ok(root_key) } + /// Get the private key that corresponds to the provided public key + fn get_private_key( + parent_key: &ExtendedPrivateKey, + requested_key: &ExtendedPublicKey, + ) -> KeyChainResult { + let derived_key = + parent_key.clone().derive_absolute_path(requested_key.get_derivation_path())?; + if &derived_key.to_public_key() == requested_key { + Ok(derived_key) + } else { + Err(KeyChainError::KeysNotInSameHierarchy) + } + } + + /// Get the private key that corresponds to the provided public key + fn get_vrf_private_key( + parent_key: &ExtendedVRFPrivateKey, + requested_key: &ExtendedVRFPublicKey, + ) -> KeyChainResult { + let derived_key = + parent_key.clone().derive_absolute_path(requested_key.get_derivation_path())?; + if &derived_key.to_public_key() == requested_key { + Ok(derived_key) + } else { + Err(KeyChainError::KeysNotInSameHierarchy) + } + } + + pub fn derive_private_key( + &self, + requested_key: &ExtendedPublicKey, + db_tx: &impl WalletStorageReadUnlocked, + ) -> KeyChainResult { + let xpriv = self.derive_account_private_key(db_tx)?; + Self::get_private_key(&xpriv, requested_key) + } + + /// Get the leaf key chain for a particular key purpose + pub fn get_leaf_key_chain(&self, purpose: KeyPurpose) -> &LeafKeySoftChain { + self.sub_chains.get_for(purpose) + } + + /// Get the mutable leaf key chain for a particular key purpose. This is used internally with + /// database persistence done externally. + fn get_leaf_key_chain_mut(&mut self, purpose: KeyPurpose) -> &mut LeafKeySoftChain { + self.sub_chains.mut_for(purpose) + } +} + +impl AccountKeyChains for AccountKeyChainImpl { /// Load the key chain from the database - pub fn load_from_database( + fn load_from_database( chain_config: Arc, db_tx: &impl WalletStorageReadLocked, id: &AccountId, @@ -193,7 +294,7 @@ impl AccountKeyChainImpl { id, )?; - let vrf_chain = VrfKeySoftChain::load_keys( + let vrf_chain = V::load_from_database( chain_config.clone(), db_tx, id, @@ -230,11 +331,10 @@ impl AccountKeyChainImpl { }) .collect(); - Ok(AccountKeyChainImpl { + Ok(Self { chain_config, account_index: account_info.account_index(), account_public_key: pubkey_id, - account_vrf_public_key: vrf_chain.get_account_vrf_public_key().clone().into(), sub_chains, vrf_chain, standalone_watch_only_keys, @@ -243,25 +343,43 @@ impl AccountKeyChainImpl { lookahead_size: account_info.lookahead_size().into(), }) } + fn find_public_key(&self, destination: &Destination) -> Option { + for purpose in KeyPurpose::ALL { + let leaf_key = self.get_leaf_key_chain(purpose); + if let Some(xpub) = leaf_key + .get_child_num_from_destination(destination) + .and_then(|child_num| leaf_key.get_derived_xpub(child_num)) + { + return Some(super::FoundPubKey::Hierarchy(xpub.clone())); + } + } + + self.standalone_private_keys + .get(destination) + .map(|(_, acc_public_key)| super::FoundPubKey::Standalone(acc_public_key.clone())) + } - pub fn account_index(&self) -> U31 { + fn find_multisig_challenge( + &self, + destination: &Destination, + ) -> Option<&ClassicMultisigChallenge> { + self.get_multisig_challenge(destination) + } + + fn account_index(&self) -> U31 { self.account_index } - pub fn get_account_id(&self) -> AccountId { + fn get_account_id(&self) -> AccountId { AccountId::new_from_xpub(&self.account_public_key) } - pub fn account_public_key(&self) -> &ExtendedPublicKey { + fn account_public_key(&self) -> &ExtendedPublicKey { self.account_public_key.as_ref() } - pub fn account_vrf_public_key(&self) -> &ExtendedVRFPublicKey { - self.account_vrf_public_key.as_ref() - } - /// Return the next unused address and don't mark it as issued - pub fn next_unused_address( + fn next_unused_address( &mut self, db_tx: &mut impl WalletStorageWriteLocked, purpose: KeyPurpose, @@ -271,7 +389,7 @@ impl AccountKeyChainImpl { } /// Issue a new address that hasn't been used before - pub fn issue_address( + fn issue_address( &mut self, db_tx: &mut impl WalletStorageWriteLocked, purpose: KeyPurpose, @@ -283,7 +401,7 @@ impl AccountKeyChainImpl { } /// Issue a new derived key that hasn't been used before - pub fn issue_key( + fn issue_key( &mut self, db_tx: &mut impl WalletStorageWriteLocked, purpose: KeyPurpose, @@ -294,18 +412,9 @@ impl AccountKeyChainImpl { Ok(key) } - /// Issue a new derived vrf key that hasn't been used before - pub fn issue_vrf_key( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - ) -> KeyChainResult<(ChildNumber, ExtendedVRFPublicKey)> { - let lookahead_size = self.lookahead_size(); - self.vrf_chain.issue_new(db_tx, lookahead_size) - } - /// Reload the sub chain keys from DB to restore the cache /// Should be called after issuing a new key but not using committing it to the DB - pub fn reload_keys(&mut self, db_tx: &impl WalletStorageReadLocked) -> KeyChainResult<()> { + fn reload_keys(&mut self, db_tx: &impl WalletStorageReadLocked) -> KeyChainResult<()> { self.sub_chains = LeafKeySoftChain::load_leaf_keys( self.chain_config.clone(), &self.account_public_key, @@ -313,7 +422,7 @@ impl AccountKeyChainImpl { &self.get_account_id(), )?; - self.vrf_chain = VrfKeySoftChain::load_keys( + self.vrf_chain = V::load_from_database( self.chain_config.clone(), db_tx, &self.get_account_id(), @@ -323,148 +432,29 @@ impl AccountKeyChainImpl { Ok(()) } - /// Get the private key that corresponds to the provided public key - fn get_private_key( - parent_key: &ExtendedPrivateKey, - requested_key: &ExtendedPublicKey, - ) -> KeyChainResult { - let derived_key = - parent_key.clone().derive_absolute_path(requested_key.get_derivation_path())?; - if &derived_key.to_public_key() == requested_key { - Ok(derived_key) - } else { - Err(KeyChainError::KeysNotInSameHierarchy) - } - } - - /// Get the private key that corresponds to the provided public key - fn get_vrf_private_key( - parent_key: &ExtendedVRFPrivateKey, - requested_key: &ExtendedVRFPublicKey, - ) -> KeyChainResult { - let derived_key = - parent_key.clone().derive_absolute_path(requested_key.get_derivation_path())?; - if &derived_key.to_public_key() == requested_key { - Ok(derived_key) - } else { - Err(KeyChainError::KeysNotInSameHierarchy) - } - } - - pub fn derive_private_key( - &self, - requested_key: &ExtendedPublicKey, - db_tx: &impl WalletStorageReadUnlocked, - ) -> KeyChainResult { - let xpriv = self.derive_account_private_key(db_tx)?; - Self::get_private_key(&xpriv, requested_key) - } - - pub fn get_private_key_for_destination( - &self, - destination: &Destination, - db_tx: &impl WalletStorageReadUnlocked, - ) -> KeyChainResult> { - let xpriv = self.derive_account_private_key(db_tx)?; - for purpose in KeyPurpose::ALL { - let leaf_key = self.get_leaf_key_chain(purpose); - if let Some(xpub) = leaf_key - .get_child_num_from_destination(destination) - .and_then(|child_num| leaf_key.get_derived_xpub(child_num)) - { - return Self::get_private_key(&xpriv, xpub).map(|pk| Some(pk.private_key())); - } - } - - let standalone_pk = self - .standalone_private_keys - .get(destination) - .map(|(_, acc_public_key)| db_tx.get_account_standalone_private_key(acc_public_key)) - .transpose()? - .flatten(); - - Ok(standalone_pk) - } - - pub fn get_multisig_challenge( - &self, - destination: &Destination, - ) -> Option<&ClassicMultisigChallenge> { - self.standalone_multisig_keys - .get(destination) - .map(|multisig| &multisig.challenge) - } - - pub fn get_private_key_for_path( - &self, - path: &DerivationPath, - db_tx: &impl WalletStorageReadUnlocked, - ) -> KeyChainResult { - let xpriv = self.derive_account_private_key(db_tx)?; - xpriv.derive_absolute_path(path).map_err(KeyChainError::Derivation) - } - - pub fn get_vrf_private_key_for_public_key( - &self, - public_key: &VRFPublicKey, - db_tx: &impl WalletStorageReadUnlocked, - ) -> KeyChainResult> { - let xpriv = self.derive_account_private_vrf_key(db_tx)?; - - if let Some(xpub) = self.vrf_chain.get_derived_xpub_from_public_key(public_key) { - return Self::get_vrf_private_key(&xpriv, xpub).map(Option::Some); - } - Ok(None) - } - - pub fn get_private_vrf_key_for_path( - &self, - path: &DerivationPath, - db_tx: &impl WalletStorageReadUnlocked, - ) -> KeyChainResult { - let xpriv = self.derive_account_private_vrf_key(db_tx)?; - xpriv.derive_absolute_path(path).map_err(KeyChainError::Derivation) - } - - /// Get the leaf key chain for a particular key purpose - pub fn get_leaf_key_chain(&self, purpose: KeyPurpose) -> &LeafKeySoftChain { - self.sub_chains.get_for(purpose) - } - - /// Get the mutable leaf key chain for a particular key purpose. This is used internally with - /// database persistence done externally. - fn get_leaf_key_chain_mut(&mut self, purpose: KeyPurpose) -> &mut LeafKeySoftChain { - self.sub_chains.mut_for(purpose) - } - - /// Get the vrf key chain for - pub fn get_vrf_key_chain(&self) -> &VrfKeySoftChain { - &self.vrf_chain - } - // Return true if the provided destination belongs to this key chain - pub fn is_destination_mine(&self, destination: &Destination) -> bool { + fn is_destination_mine(&self, destination: &Destination) -> bool { KeyPurpose::ALL .iter() .any(|p| self.get_leaf_key_chain(*p).is_destination_mine(destination)) } // Return true if the provided public key belongs to this key chain - pub fn is_public_key_mine(&self, public_key: &PublicKey) -> bool { + fn is_public_key_mine(&self, public_key: &PublicKey) -> bool { KeyPurpose::ALL .iter() .any(|purpose| self.get_leaf_key_chain(*purpose).is_public_key_mine(public_key)) } // Return true if the provided public key hash belongs to this key chain - pub fn is_public_key_hash_mine(&self, pubkey_hash: &PublicKeyHash) -> bool { + fn is_public_key_hash_mine(&self, pubkey_hash: &PublicKeyHash) -> bool { KeyPurpose::ALL .iter() .any(|purpose| self.get_leaf_key_chain(*purpose).is_public_key_hash_mine(pubkey_hash)) } // Return true if the provided public key hash is one the standalone added keys - pub fn is_public_key_hash_watched(&self, pubkey_hash: PublicKeyHash) -> bool { + fn is_public_key_hash_watched(&self, pubkey_hash: PublicKeyHash) -> bool { let dest = Destination::PublicKeyHash(pubkey_hash); self.standalone_watch_only_keys.contains_key(&dest) || self.standalone_private_keys.contains_key(&dest) @@ -472,12 +462,89 @@ impl AccountKeyChainImpl { // Return true if the provided public key hash belongs to this key chain // or is one the standalone added keys - pub fn is_public_key_hash_mine_or_watched(&self, pubkey_hash: PublicKeyHash) -> bool { + fn is_public_key_hash_mine_or_watched(&self, pubkey_hash: PublicKeyHash) -> bool { self.is_public_key_hash_mine(&pubkey_hash) || self.is_public_key_hash_watched(pubkey_hash) } + /// Find the corresponding public key for a given public key hash + fn get_public_key_from_public_key_hash( + &self, + pubkey_hash: &PublicKeyHash, + ) -> Option { + KeyPurpose::ALL.iter().find_map(|purpose| { + self.get_leaf_key_chain(*purpose) + .get_public_key_from_public_key_hash(pubkey_hash) + }) + } + + /// Derive addresses until there are lookahead unused ones + fn top_up_all(&mut self, db_tx: &mut impl WalletStorageWriteLocked) -> KeyChainResult<()> { + let lookahead_size = self.lookahead_size(); + KeyPurpose::ALL.iter().try_for_each(|purpose| { + self.get_leaf_key_chain_mut(*purpose).top_up(db_tx, lookahead_size) + })?; + self.vrf_chain.top_up(lookahead_size) + } + + fn lookahead_size(&self) -> u32 { + *self.lookahead_size + } + + /// Marks a public key as being used. Returns true if a key was found and set to used. + fn mark_public_key_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + public_key: &PublicKey, + ) -> KeyChainResult { + let lookahead_size = self.lookahead_size(); + for purpose in KeyPurpose::ALL { + let leaf_keys = self.get_leaf_key_chain_mut(purpose); + if leaf_keys.mark_pubkey_as_used(db_tx, public_key, lookahead_size)? { + return Ok(true); + } + } + Ok(false) + } + + fn mark_public_key_hash_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + pub_key_hash: &PublicKeyHash, + ) -> KeyChainResult { + let lookahead_size = self.lookahead_size(); + for purpose in KeyPurpose::ALL { + let leaf_keys = self.get_leaf_key_chain_mut(purpose); + if leaf_keys.mark_pub_key_hash_as_used(db_tx, pub_key_hash, lookahead_size)? { + return Ok(true); + } + } + Ok(false) + } + + /// Marks a vrf public key as being used. Returns true if a key was found and set to used. + fn mark_vrf_public_key_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + public_key: &VRFPublicKey, + ) -> KeyChainResult { + let lookahead_size = self.lookahead_size(); + if self.vrf_chain.mark_pubkey_as_used(db_tx, public_key, lookahead_size)? { + return Ok(true); + } + Ok(false) + } + + fn get_multisig_challenge( + &self, + destination: &Destination, + ) -> Option<&ClassicMultisigChallenge> { + self.standalone_multisig_keys + .get(destination) + .map(|multisig| &multisig.challenge) + } + /// Add, rename or delete a label for a standalone address not from the keys derived from this account - pub fn standalone_address_label_rename( + fn standalone_address_label_rename( &mut self, db_tx: &mut impl WalletStorageWriteLocked, new_address: Destination, @@ -504,7 +571,7 @@ impl AccountKeyChainImpl { } /// Adds a new public key hash to be watched, standalone from the keys derived from this account - pub fn add_standalone_watch_only_address( + fn add_standalone_watch_only_address( &mut self, db_tx: &mut impl WalletStorageWriteLocked, new_address: PublicKeyHash, @@ -526,7 +593,7 @@ impl AccountKeyChainImpl { } /// Adds a new private key to be watched, standalone from the keys derived from this account - pub fn add_standalone_private_key( + fn add_standalone_private_key( &mut self, db_tx: &mut impl WalletStorageWriteUnlocked, new_private_key: PrivateKey, @@ -551,7 +618,7 @@ impl AccountKeyChainImpl { } /// Adds a multisig to be watched - pub fn add_standalone_multisig( + fn add_standalone_multisig( &mut self, db_tx: &mut impl WalletStorageWriteLocked, challenge: ClassicMultisigChallenge, @@ -573,79 +640,11 @@ impl AccountKeyChainImpl { Ok(multisig_pkh) } - /// Find the corresponding public key for a given public key hash - pub fn get_public_key_from_public_key_hash( - &self, - pubkey_hash: &PublicKeyHash, - ) -> Option { - KeyPurpose::ALL.iter().find_map(|purpose| { - self.get_leaf_key_chain(*purpose) - .get_public_key_from_public_key_hash(pubkey_hash) - }) - } - - /// Derive addresses until there are lookahead unused ones - pub fn top_up_all(&mut self, db_tx: &mut impl WalletStorageWriteLocked) -> KeyChainResult<()> { - let lookahead_size = self.lookahead_size(); - KeyPurpose::ALL.iter().try_for_each(|purpose| { - self.get_leaf_key_chain_mut(*purpose).top_up(db_tx, lookahead_size) - })?; - self.vrf_chain.top_up(lookahead_size) - } - - pub fn lookahead_size(&self) -> u32 { - *self.lookahead_size - } - - /// Marks a public key as being used. Returns true if a key was found and set to used. - pub fn mark_public_key_as_used( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - public_key: &PublicKey, - ) -> KeyChainResult { - let lookahead_size = self.lookahead_size(); - for purpose in KeyPurpose::ALL { - let leaf_keys = self.get_leaf_key_chain_mut(purpose); - if leaf_keys.mark_pubkey_as_used(db_tx, public_key, lookahead_size)? { - return Ok(true); - } - } - Ok(false) - } - - pub fn mark_public_key_hash_as_used( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - pub_key_hash: &PublicKeyHash, - ) -> KeyChainResult { - let lookahead_size = self.lookahead_size(); - for purpose in KeyPurpose::ALL { - let leaf_keys = self.get_leaf_key_chain_mut(purpose); - if leaf_keys.mark_pub_key_hash_as_used(db_tx, pub_key_hash, lookahead_size)? { - return Ok(true); - } - } - Ok(false) - } - - /// Marks a vrf public key as being used. Returns true if a key was found and set to used. - pub fn mark_vrf_public_key_as_used( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - public_key: &VRFPublicKey, - ) -> KeyChainResult { - let lookahead_size = self.lookahead_size(); - if self.vrf_chain.mark_pubkey_as_used(db_tx, public_key, lookahead_size)? { - return Ok(true); - } - Ok(false) - } - - pub fn get_all_issued_addresses(&self) -> BTreeMap> { + fn get_all_issued_addresses(&self) -> BTreeMap> { self.get_leaf_key_chain(KeyPurpose::ReceiveFunds).get_all_issued_addresses() } - pub fn get_all_standalone_addresses(&self) -> StandaloneAddresses { + fn get_all_standalone_addresses(&self) -> StandaloneAddresses { StandaloneAddresses { watch_only_addresses: self.standalone_watch_only_keys.clone().into_iter().collect(), multisig_addresses: self.standalone_multisig_keys.clone().into_iter().collect(), @@ -663,7 +662,7 @@ impl AccountKeyChainImpl { } } - pub fn get_all_standalone_address_details( + fn get_all_standalone_address_details( &self, address: Destination, ) -> Option<(Destination, StandaloneAddressDetails)> { @@ -679,43 +678,68 @@ impl AccountKeyChainImpl { } } - pub fn get_all_issued_vrf_public_keys( + fn get_addresses_usage_state(&self) -> &KeychainUsageState { + self.get_leaf_key_chain(KeyPurpose::ReceiveFunds).usage_state() + } +} + +impl VRFAccountKeyChains for AccountKeyChainImpl { + /// Issue a new derived vrf key that hasn't been used before + fn issue_vrf_key( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + ) -> KeyChainResult<(ChildNumber, ExtendedVRFPublicKey)> { + let lookahead_size = self.lookahead_size(); + self.vrf_chain.issue_new(db_tx, lookahead_size) + } + + fn get_vrf_private_key_for_public_key( + &self, + public_key: &VRFPublicKey, + db_tx: &impl WalletStorageReadUnlocked, + ) -> KeyChainResult> { + let xpriv = self.derive_account_private_vrf_key(db_tx)?; + + if let Some(xpub) = self.vrf_chain.get_derived_xpub_from_public_key(public_key) { + return Self::get_vrf_private_key(&xpriv, xpub).map(Option::Some); + } + Ok(None) + } + + fn get_all_issued_vrf_public_keys( &self, ) -> BTreeMap, bool)> { self.vrf_chain.get_all_issued_keys() } - pub fn get_legacy_vrf_public_key(&self) -> Address { + fn get_legacy_vrf_public_key(&self) -> Address { self.vrf_chain.get_legacy_vrf_public_key() } - pub fn get_addresses_usage_state(&self) -> &KeychainUsageState { - self.get_leaf_key_chain(KeyPurpose::ReceiveFunds).usage_state() - } -} - -impl AccountKeyChains for AccountKeyChainImpl { - fn find_public_key(&self, destination: &Destination) -> Option { + fn get_private_key_for_destination( + &self, + destination: &Destination, + db_tx: &impl WalletStorageReadUnlocked, + ) -> KeyChainResult> { + let xpriv = self.derive_account_private_key(db_tx)?; for purpose in KeyPurpose::ALL { let leaf_key = self.get_leaf_key_chain(purpose); if let Some(xpub) = leaf_key .get_child_num_from_destination(destination) .and_then(|child_num| leaf_key.get_derived_xpub(child_num)) { - return Some(super::FoundPubKey::Hierarchy(xpub.clone())); + return Self::get_private_key(&xpriv, xpub).map(|pk| Some(pk.private_key())); } } - self.standalone_private_keys + let standalone_pk = self + .standalone_private_keys .get(destination) - .map(|(_, acc_public_key)| super::FoundPubKey::Standalone(acc_public_key.clone())) - } + .map(|(_, acc_public_key)| db_tx.get_account_standalone_private_key(acc_public_key)) + .transpose()? + .flatten(); - fn find_multisig_challenge( - &self, - destination: &Destination, - ) -> Option<&ClassicMultisigChallenge> { - self.get_multisig_challenge(destination) + Ok(standalone_pk) } } diff --git a/wallet/src/key_chain/master_key_chain/mod.rs b/wallet/src/key_chain/master_key_chain/mod.rs index 06c4af0888..a93eb60b92 100644 --- a/wallet/src/key_chain/master_key_chain/mod.rs +++ b/wallet/src/key_chain/master_key_chain/mod.rs @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::key_chain::account_key_chain::AccountKeyChainImpl; use crate::key_chain::{KeyChainError, KeyChainResult, DEFAULT_KEY_KIND}; use common::chain::ChainConfig; use crypto::key::extended::ExtendedPrivateKey; @@ -27,8 +26,9 @@ use wallet_storage::{ }; use wallet_types::seed_phrase::{SerializableSeedPhrase, StoreSeedPhrase}; -use super::DEFAULT_VRF_KEY_KIND; +use super::{AccountKeyChainImplSoftware, DEFAULT_VRF_KEY_KIND}; +#[derive(Clone, Debug)] pub struct MasterKeyChain { /// The specific chain this KeyChain is based on, this will affect the address format chain_config: Arc, @@ -134,10 +134,10 @@ impl MasterKeyChain { db_tx: &mut impl WalletStorageWriteUnlocked, account_index: U31, lookahead_size: u32, - ) -> KeyChainResult { + ) -> KeyChainResult { let root_key = Self::load_root_key(db_tx)?; let root_vrf_key = Self::load_root_vrf_key(db_tx)?; - AccountKeyChainImpl::new_from_root_key( + AccountKeyChainImplSoftware::new_from_root_key( self.chain_config.clone(), db_tx, root_key, diff --git a/wallet/src/key_chain/mod.rs b/wallet/src/key_chain/mod.rs index 777c9b5faa..a18faa1f29 100644 --- a/wallet/src/key_chain/mod.rs +++ b/wallet/src/key_chain/mod.rs @@ -32,23 +32,34 @@ mod master_key_chain; mod vrf_key_chain; mod with_purpose; +use std::collections::BTreeMap; +use std::sync::Arc; + pub use account_key_chain::AccountKeyChainImpl; use common::chain::classic_multisig::ClassicMultisigChallenge; use crypto::key::hdkd::u31::U31; -use crypto::vrf::VRFKeyKind; +use crypto::key::{PrivateKey, PublicKey}; +use crypto::vrf::{ExtendedVRFPrivateKey, ExtendedVRFPublicKey, VRFKeyKind, VRFPublicKey}; pub use master_key_chain::MasterKeyChain; -use common::address::pubkeyhash::PublicKeyHashError; -use common::address::{AddressError, RpcAddress}; +use common::address::pubkeyhash::{PublicKeyHash, PublicKeyHashError}; +use common::address::{Address, AddressError, RpcAddress}; use common::chain::config::BIP44_PATH; use common::chain::{ChainConfig, Destination}; use crypto::key::extended::{ExtendedKeyKind, ExtendedPublicKey}; use crypto::key::hdkd::child_number::ChildNumber; use crypto::key::hdkd::derivable::DerivationError; use crypto::key::hdkd::derivation_path::DerivationPath; +use wallet_storage::{ + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, + WalletStorageWriteUnlocked, +}; use wallet_types::account_id::AccountPublicKey; +use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; use wallet_types::keys::{KeyPurpose, KeyPurposeError}; -use wallet_types::AccountId; +use wallet_types::{AccountId, AccountInfo, KeychainUsageState}; + +use self::vrf_key_chain::{EmptyVrfKeyChain, VrfKeySoftChain}; /// The number of nodes in a BIP44 path pub const BIP44_PATH_LENGTH: usize = 5; @@ -112,13 +123,184 @@ pub enum FoundPubKey { Standalone(AccountPublicKey), } -pub trait AccountKeyChains { +impl FoundPubKey { + pub fn into_public_key(self) -> PublicKey { + match self { + Self::Hierarchy(xpub) => xpub.into_public_key(), + Self::Standalone(acc_pk) => acc_pk.into_item_id(), + } + } +} + +pub type AccountKeyChainImplSoftware = AccountKeyChainImpl; +pub type AccountKeyChainImplHardware = AccountKeyChainImpl; + +pub trait AccountKeyChains +where + Self: Sized, +{ + fn load_from_database( + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + account_info: &AccountInfo, + ) -> KeyChainResult; + fn find_public_key(&self, destination: &Destination) -> Option; fn find_multisig_challenge( &self, destination: &Destination, ) -> Option<&ClassicMultisigChallenge>; + + fn account_index(&self) -> U31; + + fn get_account_id(&self) -> AccountId; + + fn account_public_key(&self) -> &ExtendedPublicKey; + + /// Return the next unused address and don't mark it as issued + fn next_unused_address( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + purpose: KeyPurpose, + ) -> KeyChainResult<(ChildNumber, Address)>; + + /// Issue a new address that hasn't been used before + fn issue_address( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + purpose: KeyPurpose, + ) -> KeyChainResult<(ChildNumber, Address)>; + + /// Issue a new derived key that hasn't been used before + fn issue_key( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + purpose: KeyPurpose, + ) -> KeyChainResult; + + /// Reload the sub chain keys from DB to restore the cache + /// Should be called after issuing a new key but not using committing it to the DB + fn reload_keys(&mut self, db_tx: &impl WalletStorageReadLocked) -> KeyChainResult<()>; + + // Return true if the provided destination belongs to this key chain + fn is_destination_mine(&self, destination: &Destination) -> bool; + + // Return true if the provided public key belongs to this key chain + fn is_public_key_mine(&self, public_key: &PublicKey) -> bool; + + // Return true if the provided public key hash belongs to this key chain + fn is_public_key_hash_mine(&self, pubkey_hash: &PublicKeyHash) -> bool; + + // Return true if the provided public key hash is one the standalone added keys + fn is_public_key_hash_watched(&self, pubkey_hash: PublicKeyHash) -> bool; + + // Return true if the provided public key hash belongs to this key chain + // or is one the standalone added keys + fn is_public_key_hash_mine_or_watched(&self, pubkey_hash: PublicKeyHash) -> bool; + + /// Find the corresponding public key for a given public key hash + fn get_public_key_from_public_key_hash(&self, pubkey_hash: &PublicKeyHash) + -> Option; + + /// Derive addresses until there are lookahead unused ones + fn top_up_all(&mut self, db_tx: &mut impl WalletStorageWriteLocked) -> KeyChainResult<()>; + + fn lookahead_size(&self) -> u32; + + /// Marks a public key as being used. Returns true if a key was found and set to used. + fn mark_public_key_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + public_key: &PublicKey, + ) -> KeyChainResult; + + fn mark_public_key_hash_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + pub_key_hash: &PublicKeyHash, + ) -> KeyChainResult; + + /// Marks a vrf public key as being used. Returns true if a key was found and set to used. + fn mark_vrf_public_key_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + public_key: &VRFPublicKey, + ) -> KeyChainResult; + + fn get_multisig_challenge( + &self, + destination: &Destination, + ) -> Option<&ClassicMultisigChallenge>; + + /// Add, rename or delete a label for a standalone address not from the keys derived from this account + fn standalone_address_label_rename( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + new_address: Destination, + new_label: Option, + ) -> KeyChainResult<()>; + + /// Adds a new public key hash to be watched, standalone from the keys derived from this account + fn add_standalone_watch_only_address( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + new_address: PublicKeyHash, + label: Option, + ) -> KeyChainResult<()>; + + /// Adds a new private key to be watched, standalone from the keys derived from this account + fn add_standalone_private_key( + &mut self, + db_tx: &mut impl WalletStorageWriteUnlocked, + new_private_key: PrivateKey, + label: Option, + ) -> KeyChainResult<()>; + + /// Adds a multisig to be watched + fn add_standalone_multisig( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + challenge: ClassicMultisigChallenge, + label: Option, + ) -> KeyChainResult; + + fn get_all_issued_addresses(&self) -> BTreeMap>; + + fn get_all_standalone_addresses(&self) -> StandaloneAddresses; + + fn get_all_standalone_address_details( + &self, + address: Destination, + ) -> Option<(Destination, StandaloneAddressDetails)>; + + fn get_addresses_usage_state(&self) -> &KeychainUsageState; +} + +pub trait VRFAccountKeyChains { + fn issue_vrf_key( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + ) -> KeyChainResult<(ChildNumber, ExtendedVRFPublicKey)>; + + fn get_vrf_private_key_for_public_key( + &self, + public_key: &VRFPublicKey, + db_tx: &impl WalletStorageReadUnlocked, + ) -> KeyChainResult>; + + fn get_all_issued_vrf_public_keys( + &self, + ) -> BTreeMap, bool)>; + + fn get_legacy_vrf_public_key(&self) -> Address; + + fn get_private_key_for_destination( + &self, + destination: &Destination, + db_tx: &impl WalletStorageReadUnlocked, + ) -> KeyChainResult>; } /// Result type used for the key chain diff --git a/wallet/src/key_chain/tests.rs b/wallet/src/key_chain/tests.rs index f129e74bbd..821cc5b60f 100644 --- a/wallet/src/key_chain/tests.rs +++ b/wallet/src/key_chain/tests.rs @@ -172,7 +172,7 @@ fn key_lookahead(#[case] purpose: KeyPurpose) { drop(key_chain); - let mut key_chain = AccountKeyChainImpl::load_from_database( + let mut key_chain = AccountKeyChainImplSoftware::load_from_database( Arc::clone(&chain_config), &db.transaction_ro().unwrap(), &id, @@ -240,7 +240,7 @@ fn top_up_and_lookahead(#[case] purpose: KeyPurpose) { drop(key_chain); - let mut key_chain = AccountKeyChainImpl::load_from_database( + let mut key_chain = AccountKeyChainImplSoftware::load_from_database( chain_config, &db.transaction_ro().unwrap(), &id, diff --git a/wallet/src/key_chain/vrf_key_chain/mod.rs b/wallet/src/key_chain/vrf_key_chain/mod.rs index 26b7f63928..b11da1f6bd 100644 --- a/wallet/src/key_chain/vrf_key_chain/mod.rs +++ b/wallet/src/key_chain/vrf_key_chain/mod.rs @@ -99,72 +99,6 @@ impl VrfKeySoftChain { Ok(vrf_chain) } - pub fn load_keys( - chain_config: Arc, - db_tx: &impl WalletStorageReadLocked, - id: &AccountId, - lookahead_size: u32, - ) -> KeyChainResult { - let AccountVrfKeys { - account_vrf_key, - legacy_vrf_key, - } = db_tx - .get_account_vrf_public_keys(id)? - .ok_or(KeyChainError::CouldNotLoadKeyChain)?; - - let usage = db_tx - .get_vrf_keychain_usage_state(id)? - .ok_or(KeyChainError::CouldNotLoadKeyChain)?; - - let public_keys = (0..=usage.last_issued().map_or(0, |issued| issued.into_u32())) - .map(|index| { - let child_number = ChildNumber::from_index_with_hardened_bit(index); - Ok(( - child_number, - account_vrf_key.clone().derive_child(child_number)?, - )) - }) - .collect::>()?; - - VrfKeySoftChain::new_from_parts( - chain_config.clone(), - id.clone(), - account_vrf_key, - public_keys, - usage, - legacy_vrf_key, - lookahead_size, - ) - } - - pub fn get_account_vrf_public_key(&self) -> &ExtendedVRFPublicKey { - &self.parent_pubkey - } - - /// Issue a new key - pub fn issue_new( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - lookahead_size: u32, - ) -> KeyChainResult<(ChildNumber, ExtendedVRFPublicKey)> { - let new_issued_index = self.get_new_issued_index(lookahead_size)?; - - let key = self.derive_and_add_key(new_issued_index)?; - - let index = ChildNumber::from_normal(new_issued_index); - - logging::log::debug!( - "new vrf address: {}, index: {}", - Address::new(&self.chain_config, key.clone().into_public_key()).expect("addressable"), - new_issued_index, - ); - - self.usage_state.increment_up_to_last_issued(new_issued_index); - self.save_usage_state(db_tx)?; - - Ok((index, key)) - } - /// Persist the usage state to the database pub fn save_usage_state( &self, @@ -248,35 +182,6 @@ impl VrfKeySoftChain { self.derived_public_keys.keys().last().copied() } - /// Derive up `lookahead_size` keys starting from the last used index. If the gap from the last - /// used key to the last derived key is already `lookahead_size`, this method has no effect - pub fn top_up(&mut self, lookahead_size: u32) -> KeyChainResult<()> { - // Find how many keys to derive - let starting_index = match self.get_last_derived_index() { - None => 0, - Some(last_derived_index) => last_derived_index.get_index().into_u32() + 1, - }; - - let up_to_index = match self.last_used() { - None => lookahead_size, - Some(last_used) => last_used.into_u32() + lookahead_size + 1, - }; - - // Derive the needed keys (the loop can be ) - for i in starting_index..up_to_index { - if let Some(index) = U31::from_u32(i) { - self.derive_and_add_key(index)?; - } - } - - Ok(()) - } - - pub fn is_public_key_mine(&self, public_key: &VRFPublicKey) -> bool { - self.public_key_to_index.contains_key(public_key) - || public_key == self.legacy_pubkey.public_key() - } - pub fn get_derived_xpub_from_public_key( &self, pub_key: &VRFPublicKey, @@ -308,20 +213,6 @@ impl VrfKeySoftChain { self.top_up(lookahead_size) } - pub fn mark_pubkey_as_used( - &mut self, - db_tx: &mut impl WalletStorageWriteLocked, - public_key: &VRFPublicKey, - lookahead_size: u32, - ) -> KeyChainResult { - if let Some(child_num) = self.get_child_num_from_public_key(public_key) { - self.mark_child_key_as_used(db_tx, child_num, lookahead_size)?; - Ok(true) - } else { - Ok(false) - } - } - /// Get the index of the last used key or None if no key is used pub fn last_used(&self) -> Option { self.usage_state.last_used() @@ -337,6 +228,30 @@ impl VrfKeySoftChain { .expect("addressable") } + /// Issue a new key + pub fn issue_new( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + lookahead_size: u32, + ) -> KeyChainResult<(ChildNumber, ExtendedVRFPublicKey)> { + let new_issued_index = self.get_new_issued_index(lookahead_size)?; + + let key = self.derive_and_add_key(new_issued_index)?; + + let index = ChildNumber::from_normal(new_issued_index); + + logging::log::debug!( + "new vrf address: {}, index: {}", + Address::new(&self.chain_config, key.clone().into_public_key()).expect("addressable"), + new_issued_index, + ); + + self.usage_state.increment_up_to_last_issued(new_issued_index); + self.save_usage_state(db_tx)?; + + Ok((index, key)) + } + pub fn get_all_issued_keys(&self) -> BTreeMap, bool)> { let last_issued = match self.usage_state.last_issued() { Some(index) => index, @@ -361,8 +276,131 @@ impl VrfKeySoftChain { }) .collect() } +} + +pub trait VrfKeyChain +where + Self: Sized, +{ + fn load_from_database( + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + lookahead_size: u32, + ) -> KeyChainResult; + + /// Derive up `lookahead_size` keys starting from the last used index. If the gap from the last + /// used key to the last derived key is already `lookahead_size`, this method has no effect + fn top_up(&mut self, lookahead_size: u32) -> KeyChainResult<()>; + + fn mark_pubkey_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + public_key: &VRFPublicKey, + lookahead_size: u32, + ) -> KeyChainResult; +} - pub fn usage_state(&self) -> &KeychainUsageState { - &self.usage_state +impl VrfKeyChain for VrfKeySoftChain { + fn load_from_database( + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + lookahead_size: u32, + ) -> KeyChainResult { + let AccountVrfKeys { + account_vrf_key, + legacy_vrf_key, + } = db_tx + .get_account_vrf_public_keys(id)? + .ok_or(KeyChainError::CouldNotLoadKeyChain)?; + + let usage = db_tx + .get_vrf_keychain_usage_state(id)? + .ok_or(KeyChainError::CouldNotLoadKeyChain)?; + + let public_keys = (0..=usage.last_issued().map_or(0, |issued| issued.into_u32())) + .map(|index| { + let child_number = ChildNumber::from_index_with_hardened_bit(index); + Ok(( + child_number, + account_vrf_key.clone().derive_child(child_number)?, + )) + }) + .collect::>()?; + + VrfKeySoftChain::new_from_parts( + chain_config.clone(), + id.clone(), + account_vrf_key, + public_keys, + usage, + legacy_vrf_key, + lookahead_size, + ) + } + + /// Derive up `lookahead_size` keys starting from the last used index. If the gap from the last + /// used key to the last derived key is already `lookahead_size`, this method has no effect + fn top_up(&mut self, lookahead_size: u32) -> KeyChainResult<()> { + // Find how many keys to derive + let starting_index = match self.get_last_derived_index() { + None => 0, + Some(last_derived_index) => last_derived_index.get_index().into_u32() + 1, + }; + + let up_to_index = match self.last_used() { + None => lookahead_size, + Some(last_used) => last_used.into_u32() + lookahead_size + 1, + }; + + // Derive the needed keys (the loop can be ) + for i in starting_index..up_to_index { + if let Some(index) = U31::from_u32(i) { + self.derive_and_add_key(index)?; + } + } + + Ok(()) + } + + fn mark_pubkey_as_used( + &mut self, + db_tx: &mut impl WalletStorageWriteLocked, + public_key: &VRFPublicKey, + lookahead_size: u32, + ) -> KeyChainResult { + if let Some(child_num) = self.get_child_num_from_public_key(public_key) { + self.mark_child_key_as_used(db_tx, child_num, lookahead_size)?; + Ok(true) + } else { + Ok(false) + } + } +} + +pub struct EmptyVrfKeyChain; + +impl VrfKeyChain for EmptyVrfKeyChain { + fn load_from_database( + _chain_config: Arc, + _db_tx: &impl WalletStorageReadLocked, + _id: &AccountId, + _lookahead_size: u32, + ) -> KeyChainResult { + Ok(Self {}) + } + + fn top_up(&mut self, _lookahead_size: u32) -> KeyChainResult<()> { + Ok(()) + } + + fn mark_pubkey_as_used( + &mut self, + _db_tx: &mut impl WalletStorageWriteLocked, + _public_key: &VRFPublicKey, + _lookahead_size: u32, + ) -> KeyChainResult { + Ok(false) } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 45274623be..7a12058a66 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -23,9 +23,10 @@ pub mod wallet; pub mod wallet_events; mod utils; +use signer::software_signer::SoftwareSignerProvider; pub use crate::account::Account; pub use crate::send_request::SendRequest; pub use crate::wallet::{Wallet, WalletError, WalletResult}; -pub type DefaultWallet = Wallet; +pub type DefaultWallet = Wallet; diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 1601522af5..1c21f2410e 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -18,10 +18,9 @@ use std::mem::take; use common::address::Address; use common::chain::output_value::OutputValue; -use common::chain::partially_signed_transaction::PartiallySignedTransaction; use common::chain::stakelock::StakePoolData; use common::chain::timelock::OutputTimeLock::ForBlockCount; -use common::chain::tokens::{Metadata, TokenId, TokenIssuance}; +use common::chain::tokens::{Metadata, NftIssuance, TokenId, TokenIssuance}; use common::chain::{ ChainConfig, Destination, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, }; @@ -30,11 +29,20 @@ use common::primitives::{Amount, BlockHeight}; use crypto::vrf::VRFPublicKey; use utils::ensure; use wallet_types::currency::Currency; +use wallet_types::partially_signed_transaction::{ + PartiallySignedTransaction, TokenAdditionalInfo, UtxoAdditionalInfo, UtxoWithAdditionalInfo, +}; use crate::account::PoolData; use crate::destination_getters::{get_tx_output_destination, HtlcSpendingCondition}; use crate::{WalletError, WalletResult}; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PoolOrTokenId { + PoolId(PoolId), + TokenId(TokenId), +} + /// The `SendRequest` struct provides the necessary information to the wallet /// on the precise method of sending funds to a designated destination. #[derive(Debug, Clone)] @@ -288,22 +296,104 @@ impl SendRequest { take(&mut self.fees) } - pub fn into_partially_signed_tx(self) -> WalletResult { + pub fn into_partially_signed_tx( + self, + additional_info: &BTreeMap, + ) -> WalletResult { let num_inputs = self.inputs.len(); - let tx = Transaction::new(self.flags, self.inputs, self.outputs)?; let destinations = self.destinations.into_iter().map(Some).collect(); + let utxos = self + .utxos + .into_iter() + .map(|utxo| { + utxo.map(|utxo| { + let additional_info = find_additional_info(&utxo, additional_info)?; + Ok(UtxoWithAdditionalInfo::new(utxo, additional_info)) + }) + .transpose() + }) + .collect::>>()?; + let output_additional_infos = self + .outputs + .iter() + .map(|utxo| find_additional_info(utxo, additional_info)) + .collect::>>()?; + let tx = Transaction::new(self.flags, self.inputs, self.outputs)?; let ptx = PartiallySignedTransaction::new( tx, vec![None; num_inputs], - self.utxos, + utxos, destinations, None, + output_additional_infos, )?; Ok(ptx) } } +/// Find additional data for TxOutput, mainly for UI purposes +fn find_additional_info( + utxo: &TxOutput, + additional_info: &BTreeMap, +) -> Result, WalletError> { + let additional_info = match utxo { + TxOutput::Burn(value) + | TxOutput::Htlc(value, _) + | TxOutput::Transfer(value, _) + | TxOutput::LockThenTransfer(value, _, _) => { + find_token_additional_info(value, additional_info)?.map(UtxoAdditionalInfo::TokenInfo) + } + TxOutput::CreateOrder(data) => { + let ask = find_token_additional_info(data.ask(), additional_info)?; + let give = find_token_additional_info(data.give(), additional_info)?; + + Some(UtxoAdditionalInfo::CreateOrder { ask, give }) + } + TxOutput::IssueNft(_, data, _) => { + Some(UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: 0, + ticker: match data.as_ref() { + NftIssuance::V0(data) => data.metadata.ticker().clone(), + }, + })) + } + TxOutput::CreateStakePool(_, data) => Some(UtxoAdditionalInfo::PoolInfo { + staker_balance: data.pledge(), + }), + TxOutput::ProduceBlockFromStake(_, pool_id) => additional_info + .get(&PoolOrTokenId::PoolId(*pool_id)) + .ok_or(WalletError::MissingPoolAdditionalData(*pool_id)) + .map(Some)? + .cloned(), + TxOutput::DataDeposit(_) + | TxOutput::IssueFungibleToken(_) + | TxOutput::DelegateStaking(_, _) + | TxOutput::CreateDelegationId(_, _) => None, + }; + Ok(additional_info) +} + +fn find_token_additional_info( + value: &OutputValue, + additional_info: &BTreeMap, +) -> WalletResult> { + match value { + OutputValue::Coin(_) | OutputValue::TokenV0(_) => Ok(None), + OutputValue::TokenV1(token_id, _) => additional_info + .get(&PoolOrTokenId::TokenId(*token_id)) + .ok_or(WalletError::MissingTokenAdditionalData(*token_id)) + .cloned() + .map(|data| match data { + UtxoAdditionalInfo::TokenInfo(data) => Ok(Some(data.clone())), + UtxoAdditionalInfo::PoolInfo { staker_balance: _ } + | UtxoAdditionalInfo::CreateOrder { ask: _, give: _ } => { + Err(WalletError::MismatchedTokenAdditionalData(*token_id)) + } + })?, + } +} + pub enum SelectedInputs { Utxos(Vec), Inputs(Vec<(UtxoOutPoint, TxOutput)>), diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 4cb41035c7..eecfb8b3e3 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -13,22 +13,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::chain::{ - partially_signed_transaction::PartiallySignedTransaction, - signature::{ - inputsig::arbitrary_message::{ArbitraryMessageSignature, SignArbitraryMessageError}, - DestinationSigError, +use std::sync::Arc; + +use async_trait::async_trait; +use common::{ + address::AddressError, + chain::{ + signature::{ + inputsig::{ + arbitrary_message::{ArbitraryMessageSignature, SignArbitraryMessageError}, + classical_multisig::multisig_partial_signature::PartiallySignedMultisigStructureError, + }, + DestinationSigError, + }, + ChainConfig, Destination, SignedTransactionIntent, SignedTransactionIntentError, + Transaction, }, - Destination, SignedTransactionIntent, SignedTransactionIntentError, Transaction, }; -use crypto::key::hdkd::derivable::DerivationError; -use wallet_types::signature_status::SignatureStatus; +use crypto::key::{ + hdkd::{derivable::DerivationError, u31::U31}, + SignatureError, +}; +use wallet_storage::{ + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, +}; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, signature_status::SignatureStatus, + AccountId, +}; -use crate::key_chain::{AccountKeyChains, KeyChainError}; +use crate::{ + key_chain::{AccountKeyChains, KeyChainError}, + Account, WalletResult, +}; pub mod software_signer; +#[cfg(feature = "trezor")] +pub mod trezor_signer; + +#[cfg(feature = "trezor")] +use self::trezor_signer::TrezorError; -/// KeyChain errors +/// Signer errors #[derive(thiserror::Error, Debug, Eq, PartialEq)] pub enum SignerError { #[error("The provided keys do not belong to the same hierarchy")] @@ -47,18 +73,41 @@ pub enum SignerError { SignArbitraryMessageError(#[from] SignArbitraryMessageError), #[error("Signed transaction intent error: {0}")] SignedTransactionIntentError(#[from] SignedTransactionIntentError), + #[error("{0}")] + MultisigError(#[from] PartiallySignedMultisigStructureError), + #[error("{0}")] + SerializationError(#[from] serialization::Error), + #[cfg(feature = "trezor")] + #[error("Trezor error: {0}")] + TrezorError(#[from] TrezorError), + #[error("Partially signed tx is missing input's destination")] + MissingDestinationInTransaction, + #[error("Partially signed tx is missing UTXO type input's UTXO")] + MissingUtxo, + #[error("Partially signed tx is missing extra info for UTXO")] + MissingUtxoExtraInfo, + #[error("Tokens V0 are not supported")] + UnsupportedTokensV0, + #[error("Invalid TxOutput type as UTXO, cannot be spent")] + InvalidUtxo, + #[error("Address error: {0}")] + AddressError(#[from] AddressError), + #[error("Signature error: {0}")] + SignatureError(#[from] SignatureError), } type SignerResult = Result; /// Signer trait responsible for signing transactions or challenges using a software or hardware /// wallet +#[async_trait] pub trait Signer { /// Sign a partially signed transaction and return the before and after signature statuses. - fn sign_tx( - &self, + async fn sign_tx( + &mut self, tx: PartiallySignedTransaction, - key_chain: &impl AccountKeyChains, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), ) -> SignerResult<( PartiallySignedTransaction, Vec, @@ -66,21 +115,45 @@ pub trait Signer { )>; /// Sign an arbitrary message for a destination known to this key chain. - fn sign_challenge( - &self, + async fn sign_challenge( + &mut self, message: &[u8], destination: &Destination, - key_chain: &impl AccountKeyChains, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), ) -> SignerResult; /// Sign a transaction intent. The number of `input_destinations` must be the same as /// the number of inputs in the transaction; all of the destinations must be known /// to this key chain. - fn sign_transaction_intent( - &self, + async fn sign_transaction_intent( + &mut self, transaction: &Transaction, input_destinations: &[Destination], intent: &str, - key_chain: &impl AccountKeyChains, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), ) -> SignerResult; } + +pub trait SignerProvider { + type S: Signer; + type K: AccountKeyChains + Sync; + + fn provide(&mut self, chain_config: Arc, account_index: U31) -> Self::S; + + fn make_new_account( + &mut self, + chain_config: Arc, + account_index: U31, + name: Option, + db_tx: &mut impl WalletStorageWriteUnlocked, + ) -> WalletResult>; + + fn load_account_from_database( + &self, + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + ) -> WalletResult>; +} diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index 06f8bab5e6..07dc1a7a9d 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -15,9 +15,9 @@ use std::sync::Arc; +use async_trait::async_trait; use common::chain::{ htlc::HtlcSecret, - partially_signed_transaction::PartiallySignedTransaction, signature::{ inputsig::{ arbitrary_message::ArbitraryMessageSignature, @@ -44,33 +44,45 @@ use crypto::key::{ }; use itertools::Itertools; use randomness::make_true_rng; -use wallet_storage::WalletStorageReadUnlocked; -use wallet_types::signature_status::SignatureStatus; +use wallet_storage::{ + StoreTxRwUnlocked, WalletStorageReadLocked, WalletStorageReadUnlocked, + WalletStorageWriteUnlocked, +}; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, seed_phrase::StoreSeedPhrase, + signature_status::SignatureStatus, AccountId, +}; -use crate::key_chain::{make_account_path, AccountKeyChains, FoundPubKey, MasterKeyChain}; +use crate::{ + key_chain::{ + make_account_path, AccountKeyChainImplSoftware, AccountKeyChains, FoundPubKey, + MasterKeyChain, + }, + Account, WalletResult, +}; -use super::{Signer, SignerError, SignerResult}; +use super::{Signer, SignerError, SignerProvider, SignerResult}; -pub struct SoftwareSigner<'a, T> { - db_tx: &'a T, +pub struct SoftwareSigner { chain_config: Arc, account_index: U31, } -impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { - pub fn new(db_tx: &'a T, chain_config: Arc, account_index: U31) -> Self { +impl SoftwareSigner { + pub fn new(chain_config: Arc, account_index: U31) -> Self { Self { - db_tx, chain_config, account_index, } } - fn derive_account_private_key(&self) -> SignerResult { + fn derive_account_private_key( + &self, + db_tx: &impl WalletStorageReadUnlocked, + ) -> SignerResult { let account_path = make_account_path(&self.chain_config, self.account_index); - let root_key = - MasterKeyChain::load_root_key(self.db_tx)?.derive_absolute_path(&account_path)?; + let root_key = MasterKeyChain::load_root_key(db_tx)?.derive_absolute_path(&account_path)?; Ok(root_key) } @@ -78,21 +90,22 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { &self, destination: &Destination, key_chain: &impl AccountKeyChains, + db_tx: &impl WalletStorageReadUnlocked, ) -> SignerResult> { - let xpriv = self.derive_account_private_key()?; + let xpriv = self.derive_account_private_key(db_tx)?; match key_chain.find_public_key(destination) { Some(FoundPubKey::Hierarchy(xpub)) => { get_private_key(&xpriv, &xpub).map(|pk| Some(pk.private_key())) } Some(FoundPubKey::Standalone(acc_public_key)) => { - let standalone_pk = - self.db_tx.get_account_standalone_private_key(&acc_public_key)?; + let standalone_pk = db_tx.get_account_standalone_private_key(&acc_public_key)?; Ok(standalone_pk) } None => Ok(None), } } + #[allow(clippy::too_many_arguments)] fn sign_input( &self, tx: &Transaction, @@ -101,6 +114,7 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { inputs_utxo_refs: &[Option<&TxOutput>], key_chain: &impl AccountKeyChains, htlc_secret: &Option, + db_tx: &impl WalletStorageReadUnlocked, ) -> SignerResult<(Option, SignatureStatus)> { match destination { Destination::AnyoneCanSpend => Ok(( @@ -109,7 +123,7 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { )), Destination::PublicKey(_) | Destination::PublicKeyHash(_) => { let sig = self - .get_private_key_for_destination(destination, key_chain)? + .get_private_key_for_destination(destination, key_chain, db_tx)? .map(|private_key| { let sighash_type = SigHashType::try_from(SigHashType::ALL).expect("Should not fail"); @@ -158,6 +172,7 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { inputs_utxo_refs, current_signatures, key_chain, + db_tx, )?; let signature = encode_multisig_spend(&sig, inputs_utxo_refs[input_index]); @@ -178,6 +193,7 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { input_utxos: &[Option<&TxOutput>], mut current_signatures: AuthorizedClassicalMultisigSpend, key_chain: &impl AccountKeyChains, + db_tx: &impl WalletStorageReadUnlocked, ) -> SignerResult<( AuthorizedClassicalMultisigSpend, SignatureStatus, @@ -204,6 +220,7 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { if let Some(private_key) = self.get_private_key_for_destination( &Destination::PublicKey(public_key.clone()), key_chain, + db_tx, )? { let res = sign_classical_multisig_spending( &self.chain_config, @@ -237,17 +254,20 @@ impl<'a, T: WalletStorageReadUnlocked> SoftwareSigner<'a, T> { } } -impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { - fn sign_tx( - &self, +#[async_trait] +impl Signer for SoftwareSigner { + async fn sign_tx( + &mut self, ptx: PartiallySignedTransaction, - key_chain: &impl AccountKeyChains, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), ) -> SignerResult<( PartiallySignedTransaction, Vec, Vec, )> { - let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); + let inputs_utxo_refs: Vec<_> = + ptx.input_utxos().iter().map(|u| u.as_ref().map(|x| &x.utxo)).collect(); let (witnesses, prev_statuses, new_statuses) = ptx .witnesses() @@ -292,6 +312,7 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { &inputs_utxo_refs, sig_components, key_chain, + db_tx, )?; let signature = @@ -326,6 +347,7 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { &inputs_utxo_refs, key_chain, htlc_secret, + db_tx, )?; Ok((sig, SignatureStatus::NotSigned, status)) } @@ -339,14 +361,15 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { Ok((ptx.with_witnesses(witnesses), prev_statuses, new_statuses)) } - fn sign_challenge( - &self, + async fn sign_challenge( + &mut self, message: &[u8], destination: &Destination, - key_chain: &impl AccountKeyChains, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), ) -> SignerResult { let private_key = self - .get_private_key_for_destination(destination, key_chain)? + .get_private_key_for_destination(destination, key_chain, db_tx)? .ok_or(SignerError::DestinationNotFromThisWallet)?; let sig = ArbitraryMessageSignature::produce_uniparty_signature( @@ -359,19 +382,20 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { Ok(sig) } - fn sign_transaction_intent( - &self, + async fn sign_transaction_intent( + &mut self, transaction: &Transaction, input_destinations: &[Destination], intent: &str, - key_chain: &impl AccountKeyChains, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), ) -> SignerResult { SignedTransactionIntent::produce_from_transaction( transaction, input_destinations, intent, |dest| { - self.get_private_key_for_destination(dest, key_chain)? + self.get_private_key_for_destination(dest, key_chain, db_tx)? .ok_or(SignerError::DestinationNotFromThisWallet) }, make_true_rng(), @@ -393,5 +417,73 @@ fn get_private_key( } } +#[derive(Clone, Debug)] +pub struct SoftwareSignerProvider { + master_key_chain: MasterKeyChain, +} + +impl SoftwareSignerProvider { + pub fn new_from_mnemonic( + chain_config: Arc, + db_tx: &mut StoreTxRwUnlocked, + mnemonic_str: &str, + passphrase: Option<&str>, + save_seed_phrase: StoreSeedPhrase, + ) -> SignerResult { + let master_key_chain = MasterKeyChain::new_from_mnemonic( + chain_config, + db_tx, + mnemonic_str, + passphrase, + save_seed_phrase, + )?; + + Ok(Self { master_key_chain }) + } + + pub fn load_from_database( + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + ) -> WalletResult { + let master_key_chain = MasterKeyChain::new_from_existing_database(chain_config, db_tx)?; + Ok(Self { master_key_chain }) + } +} + +impl SignerProvider for SoftwareSignerProvider { + type S = SoftwareSigner; + type K = AccountKeyChainImplSoftware; + + fn provide(&mut self, chain_config: Arc, account_index: U31) -> Self::S { + SoftwareSigner::new(chain_config, account_index) + } + + fn make_new_account( + &mut self, + chain_config: Arc, + next_account_index: U31, + name: Option, + db_tx: &mut impl WalletStorageWriteUnlocked, + ) -> WalletResult> { + let lookahead_size = db_tx.get_lookahead_size()?; + let account_key_chain = self.master_key_chain.create_account_key_chain( + db_tx, + next_account_index, + lookahead_size, + )?; + + Account::new(chain_config, db_tx, account_key_chain, name) + } + + fn load_account_from_database( + &self, + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + ) -> WalletResult> { + Account::load_from_database(chain_config, db_tx, id) + } +} + #[cfg(test)] mod tests; diff --git a/wallet/src/signer/software_signer/tests.rs b/wallet/src/signer/software_signer/tests.rs index 780c8947a1..e31b765681 100644 --- a/wallet/src/signer/software_signer/tests.rs +++ b/wallet/src/signer/software_signer/tests.rs @@ -13,21 +13,32 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::BTreeMap; use std::ops::{Add, Div, Mul, Sub}; use super::*; -use crate::destination_getters::{get_tx_output_destination, HtlcSpendingCondition}; use crate::key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}; use crate::{Account, SendRequest}; +use common::chain::block::timestamp::BlockTimestamp; use common::chain::config::create_regtest; +use common::chain::htlc::HashedTimelockContract; use common::chain::output_value::OutputValue; +use common::chain::signature::inputsig::arbitrary_message::produce_message_challenge; +use common::chain::signature::inputsig::authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend; +use common::chain::stakelock::StakePoolData; use common::chain::timelock::OutputTimeLock; -use common::chain::{GenBlock, TxInput}; -use common::primitives::amount::UnsignedIntType; -use common::primitives::{Amount, Id, H256}; -use crypto::key::KeyKind; +use common::chain::tokens::{NftIssuance, NftIssuanceV0, TokenId, TokenIssuance}; +use common::chain::{ + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, GenBlock, + OrderData, OutPointSourceId, PoolId, TxInput, +}; +use common::primitives::per_thousand::PerThousand; +use common::primitives::{Amount, BlockHeight, Id, H256}; +use crypto::key::secp256k1::Secp256k1PublicKey; +use crypto::key::{KeyKind, PublicKey, Signature}; use randomness::{Rng, RngCore}; use rstest::rstest; +use serialization::Encode; use test_utils::random::{make_seedable_rng, Seed}; use wallet_storage::{DefaultBackend, Store, Transactional}; use wallet_types::account_info::DEFAULT_ACCOUNT_INDEX; @@ -40,7 +51,10 @@ const MNEMONIC: &str = #[rstest] #[trace] #[case(Seed::from_entropy())] -fn sign_transaction(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_message(#[case] seed: Seed) { + use wallet_storage::TransactionRwUnlocked; + let mut rng = make_seedable_rng(seed); let config = Arc::new(create_regtest()); @@ -61,8 +75,140 @@ fn sign_transaction(#[case] seed: Seed) { .unwrap(); let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); + let destination = account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object(); + let mut signer = SoftwareSigner::new(config.clone(), DEFAULT_ACCOUNT_INDEX); + let message = vec![rng.gen::(), rng.gen::(), rng.gen::()]; + db_tx.commit().unwrap(); + let db_tx = db.local_rw_unlocked().read_only_store(); + let res = signer + .sign_challenge(&message, &destination, account.key_chain(), &db_tx) + .await + .unwrap(); + + let message_challenge = produce_message_challenge(&message); + res.verify_signature(&config, &destination, &message_challenge).unwrap(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_transaction_intent(#[case] seed: Seed) { + use common::primitives::Idable; + use wallet_storage::TransactionRwUnlocked; + + let mut rng = make_seedable_rng(seed); + + let config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let mut signer = SoftwareSigner::new(config.clone(), DEFAULT_ACCOUNT_INDEX); + + let inputs: Vec = (0..rng.gen_range(1..5)) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + let input_destinations: Vec<_> = (0..inputs.len()) + .map(|_| account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object()) + .collect(); + + let tx = Transaction::new( + 0, + inputs, + vec![TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen())), + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), + )], + ) + .unwrap(); + db_tx.commit().unwrap(); + + let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); + let db_tx = db.local_rw_unlocked().read_only_store(); + let res = signer + .sign_transaction_intent( + &tx, + &input_destinations, + &intent, + account.key_chain(), + &db_tx, + ) + .await + .unwrap(); + + let expected_signed_message = + SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); + res.verify(&config, &input_destinations, &expected_signed_message).unwrap(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_transaction(#[case] seed: Seed) { + use std::num::NonZeroU8; + + use common::{ + chain::{ + classic_multisig::ClassicMultisigChallenge, + htlc::HashedTimelockContract, + stakelock::StakePoolData, + tokens::{ + IsTokenUnfreezable, Metadata, NftIssuance, NftIssuanceV0, TokenId, TokenIssuance, + TokenIssuanceV1, + }, + AccountCommand, AccountNonce, AccountOutPoint, AccountSpending, DelegationId, + OrderData, OrderId, PoolId, + }, + primitives::amount::UnsignedIntType, + }; + use crypto::vrf::VRFPrivateKey; + use serialization::extras::non_empty_vec::DataOrNoVec; + use wallet_storage::TransactionRwUnlocked; + + let mut rng = make_seedable_rng(seed); + + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + chain_config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); + let amounts: Vec = (0..(2 + rng.next_u32() % 5)) - .map(|_| Amount::from_atoms(rng.next_u32() as UnsignedIntType)) + .map(|_| Amount::from_atoms(rng.gen_range(1..10) as UnsignedIntType)) .collect(); let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); @@ -83,9 +229,8 @@ fn sign_transaction(#[case] seed: Seed) { }) .collect(); - let inputs: Vec = utxos - .iter() - .map(|_txo| { + let inputs: Vec = (0..utxos.len()) + .map(|_| { let source_id = if rng.gen_bool(0.5) { Id::::new(H256::random_using(&mut rng)).into() } else { @@ -95,6 +240,114 @@ fn sign_transaction(#[case] seed: Seed) { }) .collect(); + let (_dest_prv, pub_key1) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let pub_key2 = if let Destination::PublicKeyHash(pkh) = + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() + { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + let pub_key3 = if let Destination::PublicKeyHash(pkh) = + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() + { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + let min_required_signatures = 2; + let challenge = ClassicMultisigChallenge::new( + &chain_config, + NonZeroU8::new(min_required_signatures).unwrap(), + vec![pub_key1, pub_key2, pub_key3], + ) + .unwrap(); + let multisig_hash = account.add_standalone_multisig(&mut db_tx, challenge, None).unwrap(); + + let multisig_dest = Destination::ClassicMultisig(multisig_hash); + + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + let multisig_input = TxInput::from_utxo(source_id, rng.next_u32()); + let multisig_utxo = TxOutput::Transfer(OutputValue::Coin(Amount::from_atoms(1)), multisig_dest); + + let acc_inputs = vec![ + TxInput::Account(AccountOutPoint::new( + AccountNonce::new(0), + AccountSpending::DelegationBalance( + DelegationId::new(H256::random_using(&mut rng)), + Amount::from_atoms(rng.next_u32() as u128), + ), + )), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::MintTokens( + TokenId::new(H256::random_using(&mut rng)), + Amount::from_atoms(100), + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::UnmintTokens(TokenId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::LockTokenSupply(TokenId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FreezeToken( + TokenId::new(H256::random_using(&mut rng)), + IsTokenUnfreezable::Yes, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::UnfreezeToken(TokenId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ChangeTokenAuthority( + TokenId::new(H256::random_using(&mut rng)), + Destination::AnyoneCanSpend, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ConcludeOrder(OrderId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FillOrder( + OrderId::new(H256::random_using(&mut rng)), + Amount::from_atoms(123), + Destination::AnyoneCanSpend, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ChangeTokenMetadataUri( + TokenId::new(H256::random_using(&mut rng)), + "http://uri".as_bytes().to_vec(), + ), + ), + ]; + let acc_dests: Vec = acc_inputs + .iter() + .map(|_| { + let purpose = if rng.gen_bool(0.5) { + ReceiveFunds + } else { + Change + }; + + account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object() + }) + .collect(); + let dest_amount = total_amount.div(10).unwrap().mul(5).unwrap(); let lock_amount = total_amount.div(10).unwrap().mul(1).unwrap(); let burn_amount = total_amount.div(10).unwrap().mul(1).unwrap(); @@ -105,6 +358,53 @@ fn sign_transaction(#[case] seed: Seed) { let _fee_amount = total_amount.sub(outputs_amounts_sum).unwrap(); let (_dest_prv, dest_pub) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let (_, vrf_public_key) = VRFPrivateKey::new_from_entropy(crypto::vrf::VRFKeyKind::Schnorrkel); + + let pool_id = PoolId::new(H256::random()); + let delegation_id = DelegationId::new(H256::random()); + let pool_data = StakePoolData::new( + Amount::from_atoms(5000000), + Destination::PublicKey(dest_pub.clone()), + vrf_public_key, + Destination::PublicKey(dest_pub.clone()), + PerThousand::new_from_rng(&mut rng), + Amount::from_atoms(100), + ); + let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, + authority: Destination::PublicKey(dest_pub.clone()), + is_freezable: common::chain::tokens::IsTokenFreezable::No, + }); + + let nft_issuance = NftIssuance::V0(NftIssuanceV0 { + metadata: Metadata { + creator: None, + name: "Name".as_bytes().to_vec(), + description: "SomeNFT".as_bytes().to_vec(), + ticker: "NFTX".as_bytes().to_vec(), + icon_uri: DataOrNoVec::from(None), + additional_metadata_uri: DataOrNoVec::from(None), + media_uri: DataOrNoVec::from(None), + media_hash: "123456".as_bytes().to_vec(), + }, + }); + let nft_id = TokenId::new(H256::random()); + + let hash_lock = HashedTimelockContract { + secret_hash: common::chain::htlc::HtlcSecretHash([1; 20]), + spend_key: Destination::PublicKey(dest_pub.clone()), + refund_timelock: OutputTimeLock::UntilHeight(BlockHeight::new(123)), + refund_key: Destination::AnyoneCanSpend, + }; + + let order_data = OrderData::new( + Destination::PublicKey(dest_pub.clone()), + OutputValue::Coin(Amount::from_atoms(100)), + OutputValue::Coin(total_amount), + ); let outputs = vec![ TxOutput::Transfer( @@ -117,38 +417,337 @@ fn sign_transaction(#[case] seed: Seed) { OutputTimeLock::ForSeconds(rng.next_u64()), ), TxOutput::Burn(OutputValue::Coin(burn_amount)), + TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), + TxOutput::CreateDelegationId( + Destination::AnyoneCanSpend, + PoolId::new(H256::random_using(&mut rng)), + ), + TxOutput::DelegateStaking(burn_amount, delegation_id), + TxOutput::IssueFungibleToken(Box::new(token_issuance)), + TxOutput::IssueNft( + nft_id, + Box::new(nft_issuance.clone()), + Destination::AnyoneCanSpend, + ), + TxOutput::DataDeposit(vec![1, 2, 3]), + TxOutput::Htlc(OutputValue::Coin(burn_amount), Box::new(hash_lock)), + TxOutput::CreateOrder(Box::new(order_data)), TxOutput::Transfer( - OutputValue::Coin(Amount::from_atoms(100)), + OutputValue::Coin(Amount::from_atoms(100_000_000_000)), account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), ), ]; - let tx = Transaction::new(0, inputs, outputs).unwrap(); - - let req = SendRequest::from_transaction(tx, utxos.clone(), &|_| None).unwrap(); - let ptx = req.into_partially_signed_tx().unwrap(); + let req = SendRequest::new() + .with_inputs(inputs.clone().into_iter().zip(utxos.clone()), &|_| None) + .unwrap() + .with_inputs( + [multisig_input.clone()].into_iter().zip([multisig_utxo.clone()]), + &|_| None, + ) + .unwrap() + .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) + .with_outputs(outputs); + let destinations = req.destinations().to_vec(); + let additional_info = BTreeMap::new(); + let ptx = req.into_partially_signed_tx(&additional_info).unwrap(); - let signer = SoftwareSigner::new(&db_tx, config.clone(), DEFAULT_ACCOUNT_INDEX); - let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain()).unwrap(); + db_tx.commit().unwrap(); + let db_tx = db.local_rw_unlocked().read_only_store(); + let mut signer = SoftwareSigner::new(chain_config.clone(), DEFAULT_ACCOUNT_INDEX); + let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).await.unwrap(); + eprintln!("num inputs in tx: {} {:?}", inputs.len(), ptx.witnesses()); assert!(ptx.all_signatures_available()); - let sig_tx = ptx.into_signed_tx().unwrap(); + let utxos_ref = utxos + .iter() + .map(Some) + .chain([Some(&multisig_utxo)]) + .chain(acc_dests.iter().map(|_| None)) + .collect::>(); + + for (i, dest) in destinations.iter().enumerate() { + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + dest, + &ptx, + &utxos_ref, + i, + ) + .unwrap(); + } +} + +#[rstest] +#[trace] +fn fixed_signatures() { + use std::num::NonZeroU8; + + use common::chain::{ + classic_multisig::ClassicMultisigChallenge, + tokens::{IsTokenUnfreezable, Metadata, TokenIssuanceV1}, + OrderId, + }; + use crypto::vrf::VRFPrivateKey; + use serialization::extras::non_empty_vec::DataOrNoVec; + + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + chain_config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let amount_1 = Amount::from_atoms(1); + let random_pk = PublicKey::from( + Secp256k1PublicKey::from_bytes(&[ + 2, 2, 11, 98, 44, 24, 95, 8, 36, 172, 134, 193, 94, 60, 66, 115, 65, 190, 73, 45, 12, + 228, 65, 185, 175, 158, 4, 233, 125, 192, 84, 52, 242, + ]) + .unwrap(), + ); + + eprintln!("getting first address"); + let (num, addr) = account.get_new_address(&mut db_tx, ReceiveFunds).unwrap(); + eprintln!("num: {num:?}"); + + let wallet_pk0 = if let Destination::PublicKeyHash(pkh) = addr.into_object() { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + + let wallet_pk1 = if let Destination::PublicKeyHash(pkh) = + account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object() + { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + let min_required_signatures = 2; + let challenge = ClassicMultisigChallenge::new( + &chain_config, + NonZeroU8::new(min_required_signatures).unwrap(), + vec![wallet_pk0.clone(), random_pk, wallet_pk1], + ) + .unwrap(); + let multisig_hash = + account.add_standalone_multisig(&mut db_tx, challenge.clone(), None).unwrap(); + + let multisig_dest = Destination::ClassicMultisig(multisig_hash); + + let multisig_input = TxInput::from_utxo(Id::::new(H256::zero()).into(), 2); + let multisig_utxo = TxOutput::Transfer(OutputValue::Coin(amount_1), multisig_dest); + + let delegation_id = DelegationId::new(H256::zero()); + let token_id = TokenId::new(H256::zero()); + let order_id = OrderId::new(H256::zero()); + let pool_id = PoolId::new(H256::zero()); - let utxos_ref = utxos.iter().map(Some).collect::>(); + let uri = "http://uri.com".as_bytes().to_vec(); - for i in 0..sig_tx.inputs().len() { - let destination = get_tx_output_destination( - utxos_ref[i].unwrap(), + let wallet_dest0 = Destination::PublicKeyHash((&wallet_pk0).into()); + + let utxos = [TxOutput::Transfer(OutputValue::Coin(amount_1), wallet_dest0.clone())]; + + let inputs = [TxInput::from_utxo( + OutPointSourceId::Transaction(Id::::new(H256::zero())), + 1, + )]; + + let acc_inputs = vec![ + TxInput::Account(AccountOutPoint::new( + AccountNonce::new(0), + AccountSpending::DelegationBalance(delegation_id, amount_1), + )), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::MintTokens(token_id, amount_1), + ), + TxInput::AccountCommand(AccountNonce::new(0), AccountCommand::UnmintTokens(token_id)), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::LockTokenSupply(token_id), + ), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::Yes), + ), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::UnfreezeToken(token_id), + ), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ChangeTokenAuthority(token_id, Destination::AnyoneCanSpend), + ), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ConcludeOrder(order_id), + ), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::FillOrder(order_id, amount_1, Destination::AnyoneCanSpend), + ), + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri(token_id, uri.clone()), + ), + ]; + let acc_dests: Vec = vec![wallet_dest0.clone(); acc_inputs.len()]; + + let (_, vrf_public_key) = + VRFPrivateKey::new_using_random_bytes(&[0; 32], crypto::vrf::VRFKeyKind::Schnorrkel) + .unwrap(); + + let pool_data = StakePoolData::new( + amount_1, + Destination::AnyoneCanSpend, + vrf_public_key, + Destination::AnyoneCanSpend, + PerThousand::new(1).unwrap(), + amount_1, + ); + let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: 2, + metadata_uri: uri.clone(), + total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, + authority: Destination::AnyoneCanSpend, + is_freezable: common::chain::tokens::IsTokenFreezable::Yes, + }); + + let nft_issuance = NftIssuance::V0(NftIssuanceV0 { + metadata: Metadata { + creator: None, + name: "Name".as_bytes().to_vec(), + description: "SomeNFT".as_bytes().to_vec(), + ticker: "NFTX".as_bytes().to_vec(), + icon_uri: DataOrNoVec::from(None), + additional_metadata_uri: DataOrNoVec::from(None), + media_uri: Some(uri).into(), + media_hash: "123456".as_bytes().to_vec(), + }, + }); + let hash_lock = HashedTimelockContract { + secret_hash: common::chain::htlc::HtlcSecretHash([1; 20]), + spend_key: Destination::AnyoneCanSpend, + refund_timelock: OutputTimeLock::UntilHeight(BlockHeight::new(10)), + refund_key: Destination::AnyoneCanSpend, + }; + + let order_data = OrderData::new( + Destination::AnyoneCanSpend, + OutputValue::Coin(amount_1), + OutputValue::Coin(amount_1), + ); + + let outputs = vec![ + TxOutput::Transfer(OutputValue::Coin(amount_1), Destination::AnyoneCanSpend), + TxOutput::LockThenTransfer( + OutputValue::Coin(amount_1), + Destination::AnyoneCanSpend, + OutputTimeLock::UntilHeight(BlockHeight::new(10)), + ), + TxOutput::LockThenTransfer( + OutputValue::Coin(amount_1), + Destination::AnyoneCanSpend, + OutputTimeLock::UntilTime(BlockTimestamp::from_int_seconds(10)), + ), + TxOutput::LockThenTransfer( + OutputValue::Coin(amount_1), + Destination::AnyoneCanSpend, + OutputTimeLock::ForBlockCount(10), + ), + TxOutput::LockThenTransfer( + OutputValue::Coin(amount_1), + Destination::AnyoneCanSpend, + OutputTimeLock::ForSeconds(10), + ), + TxOutput::Burn(OutputValue::Coin(amount_1)), + TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), + TxOutput::CreateDelegationId(Destination::AnyoneCanSpend, pool_id), + TxOutput::DelegateStaking(amount_1, delegation_id), + TxOutput::IssueFungibleToken(Box::new(token_issuance)), + TxOutput::IssueNft( + token_id, + Box::new(nft_issuance.clone()), + Destination::AnyoneCanSpend, + ), + TxOutput::DataDeposit(vec![1, 2, 3]), + TxOutput::Htlc(OutputValue::Coin(amount_1), Box::new(hash_lock)), + TxOutput::CreateOrder(Box::new(order_data)), + ]; + + let req = SendRequest::new() + .with_inputs(inputs.clone().into_iter().zip(utxos.clone()), &|_| None) + .unwrap() + .with_inputs( + [multisig_input.clone()].into_iter().zip([multisig_utxo.clone()]), &|_| None, - HtlcSpendingCondition::Skip, ) - .unwrap(); + .unwrap() + .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) + .with_outputs(outputs); + let destinations = req.destinations().to_vec(); + let additional_info = BTreeMap::new(); + let ptx = req.into_partially_signed_tx(&additional_info).unwrap(); + + let utxos_ref = utxos + .iter() + .map(Some) + .chain([Some(&multisig_utxo)]) + .chain(acc_dests.iter().map(|_| None)) + .collect::>(); + + let multisig_signatures = [ + (0, "7a99714dc6cc917faa2afded8028159a5048caf6f8382f67e6b61623fbe62c60423f8f7983f88f40c6f42924594f3de492a232e9e703b241c3b17b130f8daa59"), + (2, "0a0a17d71bc98fa5c24ea611c856d0d08c6a765ea3c4c8f068668e0bdff710b82b0d2eead83d059386cd3cbdf4308418d4b81e6f52c001a9141ec477a8d24845"), + ]; + + let mut current_signatures = AuthorizedClassicalMultisigSpend::new_empty(challenge.clone()); + + for (idx, sig) in multisig_signatures { + let mut signature = hex::decode(sig).unwrap(); + signature.insert(0, 0); + let sig = Signature::from_data(signature).unwrap(); + current_signatures.add_signature(idx as u8, sig); + } + + let sighash_type: SigHashType = SigHashType::ALL.try_into().unwrap(); + let multisig_sig = StandardInputSignature::new(sighash_type, current_signatures.encode()); + + let sig = "7a99714dc6cc917faa2afded8028159a5048caf6f8382f67e6b61623fbe62c60423f8f7983f88f40c6f42924594f3de492a232e9e703b241c3b17b130f8daa59"; + let mut bytes = hex::decode(sig).unwrap(); + bytes.insert(0, 0); + + let sig = Signature::from_data(bytes).unwrap(); + let sig = AuthorizedPublicKeyHashSpend::new(wallet_pk0.clone(), sig); + let sig = StandardInputSignature::new(SigHashType::ALL.try_into().unwrap(), sig.encode()); + + let mut sigs = vec![Some(InputWitness::Standard(sig)); ptx.count_inputs()]; + sigs[1] = Some(InputWitness::Standard(multisig_sig)); + let ptx = ptx.with_witnesses(sigs); + + assert!(ptx.all_signatures_available()); + for (i, dest) in destinations.iter().enumerate() { tx_verifier::input_check::signature_only_check::verify_tx_signature( - &config, - &destination, - &sig_tx, + &chain_config, + dest, + &ptx, &utxos_ref, i, ) diff --git a/wallet/src/signer/trezor_signer/mod.rs b/wallet/src/signer/trezor_signer/mod.rs new file mode 100644 index 0000000000..f916c1c1ef --- /dev/null +++ b/wallet/src/signer/trezor_signer/mod.rs @@ -0,0 +1,1183 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; + +use async_trait::async_trait; +use common::{ + address::Address, + chain::{ + htlc::HtlcSecret, + output_value::OutputValue, + signature::{ + inputsig::{ + arbitrary_message::ArbitraryMessageSignature, + authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, + authorize_pubkey_spend::AuthorizedPublicKeySpend, + authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend, + classical_multisig::{ + authorize_classical_multisig::AuthorizedClassicalMultisigSpend, + multisig_partial_signature::{self, PartiallySignedMultisigChallenge}, + }, + standard_signature::StandardInputSignature, + InputWitness, + }, + sighash::{sighashtype::SigHashType, signature_hash}, + DestinationSigError, + }, + timelock::OutputTimeLock, + tokens::{NftIssuance, TokenIssuance, TokenTotalSupply}, + AccountCommand, AccountSpending, ChainConfig, Destination, OutPointSourceId, + SignedTransactionIntent, Transaction, TxInput, TxOutput, + }, + primitives::{Idable, H256}, +}; +use crypto::key::{ + extended::ExtendedPublicKey, + hdkd::{chain_code::ChainCode, derivable::Derivable, u31::U31}, + secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey}, + Signature, SignatureError, +}; +use itertools::Itertools; +use serialization::Encode; +use trezor_client::{ + client::mintlayer::MintlayerSignature, + find_devices, + protos::{ + MintlayerAccountCommandTxInput, MintlayerAccountTxInput, MintlayerAddressPath, + MintlayerBurnTxOutput, MintlayerChangeTokenAuhtority, MintlayerChangeTokenMetadataUri, + MintlayerConcludeOrder, MintlayerCreateDelegationIdTxOutput, MintlayerCreateOrderTxOutput, + MintlayerCreateStakePoolTxOutput, MintlayerDataDepositTxOutput, + MintlayerDelegateStakingTxOutput, MintlayerFillOrder, MintlayerFreezeToken, + MintlayerHtlcTxOutput, MintlayerIssueFungibleTokenTxOutput, MintlayerIssueNftTxOutput, + MintlayerLockThenTransferTxOutput, MintlayerLockTokenSupply, MintlayerMintTokens, + MintlayerOutputValue, MintlayerProduceBlockFromStakeTxOutput, MintlayerTokenOutputValue, + MintlayerTokenTotalSupply, MintlayerTokenTotalSupplyType, MintlayerTxInput, + MintlayerTxOutput, MintlayerUnfreezeToken, MintlayerUnmintTokens, MintlayerUtxoType, + }, + Model, +}; +use trezor_client::{ + protos::{MintlayerTransferTxOutput, MintlayerUtxoTxInput}, + Trezor, +}; +use utils::ensure; +use wallet_storage::{ + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteUnlocked, +}; +use wallet_types::{ + account_info::DEFAULT_ACCOUNT_INDEX, + partially_signed_transaction::{ + PartiallySignedTransaction, TokenAdditionalInfo, UtxoAdditionalInfo, + }, + signature_status::SignatureStatus, + AccountId, +}; + +use crate::{ + key_chain::{make_account_path, AccountKeyChainImplHardware, AccountKeyChains, FoundPubKey}, + Account, WalletError, WalletResult, +}; + +use super::{Signer, SignerError, SignerProvider, SignerResult}; + +/// Signer errors +#[derive(thiserror::Error, Debug, Eq, PartialEq)] +pub enum TrezorError { + #[error("No connected Trezor device found")] + NoDeviceFound, + #[error("Trezor device error: {0}")] + DeviceError(String), + #[error("Invalid public key returned from trezor")] + InvalidKey, + #[error("Invalid Signature error: {0}")] + SignatureError(#[from] SignatureError), +} + +pub struct TrezorSigner { + chain_config: Arc, + client: Arc>, +} + +impl TrezorSigner { + pub fn new(chain_config: Arc, client: Arc>) -> Self { + Self { + chain_config, + client, + } + } + + fn make_signature( + &self, + signature: &[MintlayerSignature], + destination: &Destination, + sighash_type: SigHashType, + sighash: H256, + secret: Option, + key_chain: &impl AccountKeyChains, + ) -> SignerResult<(Option, SignatureStatus)> { + let add_secret_if_needed = |sig: StandardInputSignature| { + let sig = if let Some(htlc_secret) = secret { + let sig_with_secret = AuthorizedHashedTimelockContractSpend::Secret( + htlc_secret, + sig.raw_signature().to_owned(), + ); + let serialized_sig = sig_with_secret.encode(); + + StandardInputSignature::new(sig.sighash_type(), serialized_sig) + } else { + sig + }; + + InputWitness::Standard(sig) + }; + + match destination { + Destination::AnyoneCanSpend => Ok(( + Some(InputWitness::NoSignature(None)), + SignatureStatus::FullySigned, + )), + Destination::PublicKeyHash(_) => { + if let Some(signature) = signature.first() { + let pk = key_chain + .find_public_key(destination) + .ok_or(SignerError::DestinationNotFromThisWallet)? + .into_public_key(); + let sig = Signature::from_raw_data(&signature.signature) + .map_err(TrezorError::SignatureError)?; + let sig = AuthorizedPublicKeyHashSpend::new(pk, sig); + let sig = add_secret_if_needed(StandardInputSignature::new( + sighash_type, + sig.encode(), + )); + + Ok((Some(sig), SignatureStatus::FullySigned)) + } else { + Ok((None, SignatureStatus::NotSigned)) + } + } + Destination::PublicKey(_) => { + if let Some(signature) = signature.first() { + let sig = Signature::from_raw_data(&signature.signature) + .map_err(TrezorError::SignatureError)?; + let sig = AuthorizedPublicKeySpend::new(sig); + let sig = add_secret_if_needed(StandardInputSignature::new( + sighash_type, + sig.encode(), + )); + + Ok((Some(sig), SignatureStatus::FullySigned)) + } else { + Ok((None, SignatureStatus::NotSigned)) + } + } + Destination::ClassicMultisig(_) => { + if let Some(challenge) = key_chain.find_multisig_challenge(destination) { + let mut current_signatures = + AuthorizedClassicalMultisigSpend::new_empty(challenge.clone()); + + for sig in signature { + if let Some(idx) = sig.multisig_idx { + let sig = Signature::from_raw_data(&sig.signature) + .map_err(TrezorError::SignatureError)?; + current_signatures.add_signature(idx as u8, sig); + } + } + + let msg = sighash.encode(); + // Check the signatures status again after adding that last signature + let verifier = PartiallySignedMultisigChallenge::from_partial( + &self.chain_config, + &msg, + ¤t_signatures, + )?; + + let status = match verifier.verify_signatures(&self.chain_config)? { + multisig_partial_signature::SigsVerifyResult::CompleteAndValid => { + SignatureStatus::FullySigned + } + multisig_partial_signature::SigsVerifyResult::Incomplete => { + SignatureStatus::PartialMultisig { + required_signatures: challenge.min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + } + } + multisig_partial_signature::SigsVerifyResult::Invalid => { + unreachable!( + "We checked the signatures then added a signature, so this should be unreachable" + ) + } + }; + + let sig = add_secret_if_needed(StandardInputSignature::new( + sighash_type, + current_signatures.encode(), + )); + return Ok((Some(sig), status)); + } + + Ok((None, SignatureStatus::NotSigned)) + } + Destination::ScriptHash(_) => Ok((None, SignatureStatus::NotSigned)), + } + } + + fn to_trezor_output_msgs( + &self, + ptx: &PartiallySignedTransaction, + ) -> SignerResult> { + let outputs = ptx + .tx() + .outputs() + .iter() + .zip(ptx.output_additional_infos()) + .map(|(out, additional_info)| { + to_trezor_output_msg(&self.chain_config, out, additional_info) + }) + .collect(); + outputs + } +} + +#[async_trait] +impl Signer for TrezorSigner { + async fn sign_tx( + &mut self, + ptx: PartiallySignedTransaction, + key_chain: &(impl AccountKeyChains + Sync), + _db_tx: &(impl WalletStorageReadUnlocked + Sync), + ) -> SignerResult<( + PartiallySignedTransaction, + Vec, + Vec, + )> { + let inputs = to_trezor_input_msgs(&ptx, key_chain, &self.chain_config)?; + let outputs = self.to_trezor_output_msgs(&ptx)?; + let utxos = to_trezor_utxo_msgs(&ptx, &self.chain_config)?; + + let new_signatures = self + .client + .lock() + .expect("poisoned lock") + .mintlayer_sign_tx(inputs, outputs, utxos) + .map_err(|err| TrezorError::DeviceError(err.to_string()))?; + + let inputs_utxo_refs: Vec<_> = + ptx.input_utxos().iter().map(|u| u.as_ref().map(|x| &x.utxo)).collect(); + + let (witnesses, prev_statuses, new_statuses) = ptx + .witnesses() + .iter() + .enumerate() + .zip(ptx.destinations()) + .zip(ptx.htlc_secrets()) + .map(|(((i, witness), destination), secret)| match witness { + Some(w) => match w { + InputWitness::NoSignature(_) => Ok(( + Some(w.clone()), + SignatureStatus::FullySigned, + SignatureStatus::FullySigned, + )), + InputWitness::Standard(sig) => match destination { + Some(destination) => { + if tx_verifier::input_check::signature_only_check::verify_tx_signature( + &self.chain_config, + destination, + &ptx, + &inputs_utxo_refs, + i, + ).is_ok() + { + Ok(( + Some(w.clone()), + SignatureStatus::FullySigned, + SignatureStatus::FullySigned, + )) + } else if let Destination::ClassicMultisig(_) = destination { + let sighash = + signature_hash(sig.sighash_type(), ptx.tx(), &inputs_utxo_refs, i)?; + + let mut current_signatures = AuthorizedClassicalMultisigSpend::from_data( + sig.raw_signature(), + )?; + + let previous_status = SignatureStatus::PartialMultisig { + required_signatures: current_signatures.challenge().min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + }; + + if let Some(signature) = new_signatures.get(i) { + for sig in signature { + if let Some(idx) = sig.multisig_idx { + let sig = Signature::from_raw_data(&sig.signature) + .map_err(TrezorError::SignatureError)?; + current_signatures.add_signature(idx as u8, sig); + } + } + + let msg = sighash.encode(); + // Check the signatures status again after adding that last signature + let verifier = PartiallySignedMultisigChallenge::from_partial( + &self.chain_config, + &msg, + ¤t_signatures, + )?; + + let status = match verifier.verify_signatures(&self.chain_config)? { + multisig_partial_signature::SigsVerifyResult::CompleteAndValid => { + SignatureStatus::FullySigned + } + multisig_partial_signature::SigsVerifyResult::Incomplete => { + let challenge = current_signatures.challenge(); + SignatureStatus::PartialMultisig { + required_signatures: challenge.min_required_signatures(), + num_signatures: current_signatures.signatures().len() as u8, + } + } + multisig_partial_signature::SigsVerifyResult::Invalid => { + unreachable!( + "We checked the signatures then added a signature, so this should be unreachable" + ) + } + }; + + let sighash_type = + SigHashType::try_from(SigHashType::ALL).expect("Should not fail"); + let sig = InputWitness::Standard(StandardInputSignature::new( + sighash_type, + current_signatures.encode(), + )); + return Ok((Some(sig), + previous_status, + status)); + } + else { + Ok(( + None, + SignatureStatus::InvalidSignature, + SignatureStatus::NotSigned, + )) + + } + + + } else { + Ok(( + None, + SignatureStatus::InvalidSignature, + SignatureStatus::NotSigned, + )) + } + } + None => Ok(( + Some(w.clone()), + SignatureStatus::UnknownSignature, + SignatureStatus::UnknownSignature, + )), + }, + }, + None => match (destination, new_signatures.get(i)) { + (Some(destination), Some(sig)) => { + let sighash_type = + SigHashType::try_from(SigHashType::ALL).expect("Should not fail"); + let sighash = signature_hash(sighash_type, ptx.tx(), &inputs_utxo_refs, i)?; + let (sig, status) = self.make_signature( + sig, + destination, + sighash_type, + sighash, + secret.clone(), + key_chain, + )?; + Ok((sig, SignatureStatus::NotSigned, status)) + } + (Some(_) | None, None) | (None, Some(_)) => { + Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)) + } + }, + }) + .collect::, SignerError>>()? + .into_iter() + .multiunzip(); + + Ok((ptx.with_witnesses(witnesses), prev_statuses, new_statuses)) + } + + async fn sign_challenge( + &mut self, + message: &[u8], + destination: &Destination, + key_chain: &(impl AccountKeyChains + Sync), + _db_tx: &(impl WalletStorageReadUnlocked + Sync), + ) -> SignerResult { + let data = match key_chain.find_public_key(destination) { + Some(FoundPubKey::Hierarchy(xpub)) => { + let address_n = xpub + .get_derivation_path() + .as_slice() + .iter() + .map(|c| c.into_encoded_index()) + .collect(); + + let addr = Address::new(&self.chain_config, destination.clone())?.into_string(); + + let sig = self + .client + .lock() + .expect("poisoned lock") + .mintlayer_sign_message(address_n, addr, message.to_vec()) + .map_err(|err| TrezorError::DeviceError(err.to_string()))?; + let signature = + Signature::from_raw_data(&sig).map_err(TrezorError::SignatureError)?; + + match &destination { + Destination::PublicKey(_) => AuthorizedPublicKeySpend::new(signature).encode(), + Destination::PublicKeyHash(_) => { + AuthorizedPublicKeyHashSpend::new(xpub.into_public_key(), signature) + .encode() + } + Destination::AnyoneCanSpend => { + return Err(SignerError::SigningError( + DestinationSigError::AttemptedToProduceSignatureForAnyoneCanSpend, + )) + } + Destination::ClassicMultisig(_) => { + return Err(SignerError::SigningError( + DestinationSigError::AttemptedToProduceClassicalMultisigSignatureInUnipartySignatureCode, + )) + } + Destination::ScriptHash(_) => { + return Err(SignerError::SigningError( + DestinationSigError::Unsupported, + )) + } + } + } + Some(FoundPubKey::Standalone(_)) => { + unimplemented!("standalone keys with trezor") + } + None => return Err(SignerError::DestinationNotFromThisWallet), + }; + + let sig = ArbitraryMessageSignature::from_data(data); + Ok(sig) + } + + async fn sign_transaction_intent( + &mut self, + transaction: &Transaction, + input_destinations: &[Destination], + intent: &str, + key_chain: &(impl AccountKeyChains + Sync), + db_tx: &(impl WalletStorageReadUnlocked + Sync), + ) -> SignerResult { + let tx_id = transaction.get_id(); + let message_to_sign = SignedTransactionIntent::get_message_to_sign(intent, &tx_id); + + let mut signatures = Vec::with_capacity(input_destinations.len()); + for dest in input_destinations { + let sig = + self.sign_challenge(message_to_sign.as_bytes(), dest, key_chain, db_tx).await?; + signatures.push(sig.into_raw()); + } + + Ok(SignedTransactionIntent::new_unchecked( + message_to_sign, + signatures, + )) + } +} + +fn to_trezor_input_msgs( + ptx: &PartiallySignedTransaction, + key_chain: &impl AccountKeyChains, + chain_config: &ChainConfig, +) -> SignerResult> { + ptx.tx() + .inputs() + .iter() + .zip(ptx.input_utxos()) + .zip(ptx.destinations()) + .map(|((inp, utxo), dest)| match (inp, utxo, dest) { + (TxInput::Utxo(outpoint), Some(_), Some(dest)) => { + to_trezor_utxo_input(outpoint, chain_config, dest, key_chain) + } + (TxInput::Account(outpoint), _, Some(dest)) => { + to_trezor_account_input(chain_config, dest, key_chain, outpoint) + } + (TxInput::AccountCommand(nonce, command), _, Some(dest)) => { + to_trezor_account_command_input(chain_config, dest, key_chain, nonce, command) + } + (_, _, None) => Err(SignerError::MissingDestinationInTransaction), + (TxInput::Utxo(_), _, _) => Err(SignerError::MissingUtxo), + }) + .collect() +} + +fn to_trezor_account_command_input( + chain_config: &ChainConfig, + dest: &Destination, + key_chain: &impl AccountKeyChains, + nonce: &common::chain::AccountNonce, + command: &AccountCommand, +) -> SignerResult { + let mut inp_req = MintlayerAccountCommandTxInput::new(); + inp_req.set_address(Address::new(chain_config, dest.clone())?.into_string()); + inp_req.address_n = destination_to_address_paths(key_chain, dest); + inp_req.set_nonce(nonce.value()); + match command { + AccountCommand::MintTokens(token_id, amount) => { + let mut req = MintlayerMintTokens::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + req.set_amount(amount.into_atoms().to_be_bytes().to_vec()); + + inp_req.mint = Some(req).into(); + } + AccountCommand::UnmintTokens(token_id) => { + let mut req = MintlayerUnmintTokens::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + + inp_req.unmint = Some(req).into(); + } + AccountCommand::FreezeToken(token_id, unfreezable) => { + let mut req = MintlayerFreezeToken::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + req.set_is_token_unfreezabe(unfreezable.as_bool()); + + inp_req.freeze_token = Some(req).into(); + } + AccountCommand::UnfreezeToken(token_id) => { + let mut req = MintlayerUnfreezeToken::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + + inp_req.unfreeze_token = Some(req).into(); + } + AccountCommand::LockTokenSupply(token_id) => { + let mut req = MintlayerLockTokenSupply::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + + inp_req.lock_token_supply = Some(req).into(); + } + AccountCommand::ChangeTokenAuthority(token_id, dest) => { + let mut req = MintlayerChangeTokenAuhtority::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + req.set_destination(Address::new(chain_config, dest.clone())?.into_string()); + + inp_req.change_token_authority = Some(req).into(); + } + AccountCommand::ChangeTokenMetadataUri(token_id, uri) => { + let mut req = MintlayerChangeTokenMetadataUri::new(); + req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + req.set_metadata_uri(uri.clone()); + + inp_req.change_token_metadata_uri = Some(req).into(); + } + AccountCommand::ConcludeOrder(order_id) => { + let mut req = MintlayerConcludeOrder::new(); + req.set_order_id(Address::new(chain_config, *order_id)?.into_string()); + + inp_req.conclude_order = Some(req).into(); + } + AccountCommand::FillOrder(order_id, amount, dest) => { + let mut req = MintlayerFillOrder::new(); + req.set_order_id(Address::new(chain_config, *order_id)?.into_string()); + req.set_amount(amount.into_atoms().to_be_bytes().to_vec()); + req.set_destination(Address::new(chain_config, dest.clone())?.into_string()); + + inp_req.fill_order = Some(req).into(); + } + } + let mut inp = MintlayerTxInput::new(); + inp.account_command = Some(inp_req).into(); + Ok(inp) +} + +fn to_trezor_account_input( + chain_config: &ChainConfig, + dest: &Destination, + key_chain: &impl AccountKeyChains, + outpoint: &common::chain::AccountOutPoint, +) -> SignerResult { + let mut inp_req = MintlayerAccountTxInput::new(); + inp_req.set_address(Address::new(chain_config, dest.clone())?.into_string()); + inp_req.address_n = destination_to_address_paths(key_chain, dest); + inp_req.set_nonce(outpoint.nonce().value()); + match outpoint.account() { + AccountSpending::DelegationBalance(delegation_id, amount) => { + inp_req.set_delegation_id(Address::new(chain_config, *delegation_id)?.into_string()); + let mut value = MintlayerOutputValue::new(); + value.set_amount(amount.into_atoms().to_be_bytes().to_vec()); + inp_req.value = Some(value).into(); + } + } + let mut inp = MintlayerTxInput::new(); + inp.account = Some(inp_req).into(); + Ok(inp) +} + +fn to_trezor_utxo_input( + outpoint: &common::chain::UtxoOutPoint, + chain_config: &ChainConfig, + dest: &Destination, + key_chain: &impl AccountKeyChains, +) -> SignerResult { + let mut inp_req = MintlayerUtxoTxInput::new(); + let id = match outpoint.source_id() { + OutPointSourceId::Transaction(id) => { + inp_req.set_type(MintlayerUtxoType::TRANSACTION); + id.to_hash().0 + } + OutPointSourceId::BlockReward(id) => { + inp_req.set_type(MintlayerUtxoType::BLOCK); + id.to_hash().0 + } + }; + inp_req.set_prev_hash(id.to_vec()); + inp_req.set_prev_index(outpoint.output_index()); + + inp_req.set_address(Address::new(chain_config, dest.clone())?.into_string()); + inp_req.address_n = destination_to_address_paths(key_chain, dest); + + let mut inp = MintlayerTxInput::new(); + inp.utxo = Some(inp_req).into(); + Ok(inp) +} + +/// Find the derivation paths to the key in the destination, or multiple in the case of a multisig +fn destination_to_address_paths( + key_chain: &impl AccountKeyChains, + dest: &Destination, +) -> Vec { + match key_chain.find_public_key(dest) { + Some(FoundPubKey::Hierarchy(xpub)) => { + let address_n = xpub + .get_derivation_path() + .as_slice() + .iter() + .map(|c| c.into_encoded_index()) + .collect(); + vec![MintlayerAddressPath { + address_n, + ..Default::default() + }] + } + Some(FoundPubKey::Standalone(_)) => { + unimplemented!("standalone keys with trezor") + } + None => { + if let Some(challenge) = key_chain.find_multisig_challenge(dest) { + challenge + .public_keys() + .iter() + .enumerate() + .filter_map(|(idx, pk)| { + match key_chain.find_public_key(&Destination::PublicKey(pk.clone())) { + Some(FoundPubKey::Hierarchy(xpub)) => { + let address_n = xpub + .get_derivation_path() + .as_slice() + .iter() + .map(|c| c.into_encoded_index()) + .collect(); + Some(MintlayerAddressPath { + address_n, + multisig_idx: Some(idx as u32), + special_fields: Default::default(), + }) + } + Some(FoundPubKey::Standalone(_)) => unimplemented!("standalone keys"), + None => None, + } + }) + .collect() + } else { + vec![] + } + } + } +} + +fn to_trezor_output_value( + output_value: &OutputValue, + additional_info: &Option, + chain_config: &ChainConfig, +) -> SignerResult { + let token_info = additional_info.as_ref().and_then(|info| match info { + UtxoAdditionalInfo::TokenInfo(token_info) => Some(token_info), + UtxoAdditionalInfo::PoolInfo { staker_balance: _ } + | UtxoAdditionalInfo::CreateOrder { ask: _, give: _ } => None, + }); + + to_trezor_output_value_with_token_info(output_value, &token_info, chain_config) +} + +fn to_trezor_utxo_msgs( + ptx: &PartiallySignedTransaction, + chain_config: &ChainConfig, +) -> SignerResult>> { + let mut utxos: BTreeMap<[u8; 32], BTreeMap> = BTreeMap::new(); + + for (utxo, inp) in ptx.input_utxos().iter().zip(ptx.tx().inputs()) { + match inp { + TxInput::Utxo(outpoint) => { + let utxo = utxo.as_ref().ok_or(SignerError::MissingUtxo)?; + let id = match outpoint.source_id() { + OutPointSourceId::Transaction(id) => id.to_hash().0, + OutPointSourceId::BlockReward(id) => id.to_hash().0, + }; + let out = to_trezor_output_msg(chain_config, &utxo.utxo, &utxo.additional_info)?; + utxos.entry(id).or_default().insert(outpoint.output_index(), out); + } + TxInput::Account(_) | TxInput::AccountCommand(_, _) => {} + } + } + + Ok(utxos) +} + +fn to_trezor_output_msg( + chain_config: &ChainConfig, + out: &TxOutput, + additional_info: &Option, +) -> SignerResult { + let res = match out { + TxOutput::Transfer(value, dest) => { + let mut out_req = MintlayerTransferTxOutput::new(); + out_req.value = Some(to_trezor_output_value( + value, + additional_info, + chain_config, + )?) + .into(); + out_req.set_address(Address::new(chain_config, dest.clone())?.into_string()); + + let mut out = MintlayerTxOutput::new(); + out.transfer = Some(out_req).into(); + out + } + TxOutput::LockThenTransfer(value, dest, lock) => { + let mut out_req = MintlayerLockThenTransferTxOutput::new(); + out_req.value = Some(to_trezor_output_value( + value, + additional_info, + chain_config, + )?) + .into(); + out_req.set_address(Address::new(chain_config, dest.clone())?.into_string()); + + out_req.lock = Some(to_trezor_output_lock(lock)).into(); + + let mut out = MintlayerTxOutput::new(); + out.lock_then_transfer = Some(out_req).into(); + out + } + TxOutput::Burn(value) => { + let mut out_req = MintlayerBurnTxOutput::new(); + out_req.value = Some(to_trezor_output_value( + value, + additional_info, + chain_config, + )?) + .into(); + + let mut out = MintlayerTxOutput::new(); + out.burn = Some(out_req).into(); + out + } + TxOutput::CreateDelegationId(dest, pool_id) => { + let mut out_req = MintlayerCreateDelegationIdTxOutput::new(); + out_req.set_pool_id(Address::new(chain_config, *pool_id)?.into_string()); + out_req.set_destination(Address::new(chain_config, dest.clone())?.into_string()); + let mut out = MintlayerTxOutput::new(); + out.create_delegation_id = Some(out_req).into(); + out + } + TxOutput::DelegateStaking(amount, delegation_id) => { + let mut out_req = MintlayerDelegateStakingTxOutput::new(); + out_req.set_delegation_id(Address::new(chain_config, *delegation_id)?.into_string()); + out_req.set_amount(amount.into_atoms().to_be_bytes().to_vec()); + let mut out = MintlayerTxOutput::new(); + out.delegate_staking = Some(out_req).into(); + out + } + TxOutput::CreateStakePool(pool_id, pool_data) => { + let mut out_req = MintlayerCreateStakePoolTxOutput::new(); + out_req.set_pool_id(Address::new(chain_config, *pool_id)?.into_string()); + + out_req.set_pledge(pool_data.pledge().into_atoms().to_be_bytes().to_vec()); + out_req + .set_staker(Address::new(chain_config, pool_data.staker().clone())?.into_string()); + out_req.set_decommission_key( + Address::new(chain_config, pool_data.decommission_key().clone())?.into_string(), + ); + out_req.set_vrf_public_key( + Address::new(chain_config, pool_data.vrf_public_key().clone())?.into_string(), + ); + out_req + .set_cost_per_block(pool_data.cost_per_block().into_atoms().to_be_bytes().to_vec()); + out_req.set_margin_ratio_per_thousand( + pool_data.margin_ratio_per_thousand().as_per_thousand_int() as u32, + ); + + let mut out = MintlayerTxOutput::new(); + out.create_stake_pool = Some(out_req).into(); + out + } + TxOutput::ProduceBlockFromStake(dest, pool_id) => { + let mut out_req = MintlayerProduceBlockFromStakeTxOutput::new(); + out_req.set_pool_id(Address::new(chain_config, *pool_id)?.into_string()); + out_req.set_destination(Address::new(chain_config, dest.clone())?.into_string()); + let staker_balance = additional_info + .as_ref() + .and_then(|info| match info { + UtxoAdditionalInfo::PoolInfo { staker_balance } => Some(staker_balance), + UtxoAdditionalInfo::TokenInfo(_) + | UtxoAdditionalInfo::CreateOrder { ask: _, give: _ } => None, + }) + .ok_or(SignerError::MissingUtxoExtraInfo)?; + out_req.set_staker_balance(staker_balance.into_atoms().to_be_bytes().to_vec()); + let mut out = MintlayerTxOutput::new(); + out.produce_block_from_stake = Some(out_req).into(); + out + } + TxOutput::IssueFungibleToken(token_data) => { + let mut out_req = MintlayerIssueFungibleTokenTxOutput::new(); + + match token_data.as_ref() { + TokenIssuance::V1(data) => { + out_req.set_authority( + Address::new(chain_config, data.authority.clone())?.into_string(), + ); + out_req.set_token_ticker(data.token_ticker.clone()); + out_req.set_metadata_uri(data.metadata_uri.clone()); + out_req.set_number_of_decimals(data.number_of_decimals as u32); + out_req.set_is_freezable(data.is_freezable.as_bool()); + let mut total_supply = MintlayerTokenTotalSupply::new(); + match data.total_supply { + TokenTotalSupply::Lockable => { + total_supply.set_type(MintlayerTokenTotalSupplyType::LOCKABLE) + } + TokenTotalSupply::Unlimited => { + total_supply.set_type(MintlayerTokenTotalSupplyType::UNLIMITED) + } + TokenTotalSupply::Fixed(amount) => { + total_supply.set_type(MintlayerTokenTotalSupplyType::FIXED); + total_supply + .set_fixed_amount(amount.into_atoms().to_be_bytes().to_vec()); + } + } + out_req.total_supply = Some(total_supply).into(); + } + }; + + let mut out = MintlayerTxOutput::new(); + out.issue_fungible_token = Some(out_req).into(); + out + } + TxOutput::IssueNft(token_id, nft_data, dest) => { + let mut out_req = MintlayerIssueNftTxOutput::new(); + out_req.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + out_req.set_destination(Address::new(chain_config, dest.clone())?.into_string()); + match nft_data.as_ref() { + NftIssuance::V0(data) => { + // + out_req.set_name(data.metadata.name.clone()); + out_req.set_ticker(data.metadata.ticker().clone()); + out_req.set_icon_uri( + data.metadata.icon_uri().as_ref().clone().unwrap_or_default(), + ); + out_req.set_media_uri( + data.metadata.media_uri().as_ref().clone().unwrap_or_default(), + ); + out_req.set_media_hash(data.metadata.media_hash().clone()); + out_req.set_additional_metadata_uri( + data.metadata + .additional_metadata_uri() + .as_ref() + .clone() + .unwrap_or_default(), + ); + out_req.set_description(data.metadata.description.clone()); + if let Some(creator) = data.metadata.creator() { + out_req.set_creator( + Address::new( + chain_config, + Destination::PublicKey(creator.public_key.clone()), + )? + .into_string(), + ); + } + } + }; + let mut out = MintlayerTxOutput::new(); + out.issue_nft = Some(out_req).into(); + out + } + TxOutput::DataDeposit(data) => { + let mut out_req = MintlayerDataDepositTxOutput::new(); + out_req.set_data(data.clone()); + let mut out = MintlayerTxOutput::new(); + out.data_deposit = Some(out_req).into(); + out + } + TxOutput::Htlc(value, lock) => { + let mut out_req = MintlayerHtlcTxOutput::new(); + out_req.value = Some(to_trezor_output_value( + value, + additional_info, + chain_config, + )?) + .into(); + out_req.secret_hash = Some(lock.secret_hash.as_bytes().to_vec()); + + out_req + .set_spend_key(Address::new(chain_config, lock.spend_key.clone())?.into_string()); + out_req + .set_refund_key(Address::new(chain_config, lock.refund_key.clone())?.into_string()); + + out_req.refund_timelock = Some(to_trezor_output_lock(&lock.refund_timelock)).into(); + + let mut out = MintlayerTxOutput::new(); + out.htlc = Some(out_req).into(); + out + } + TxOutput::CreateOrder(data) => { + let mut out_req = MintlayerCreateOrderTxOutput::new(); + + out_req.set_conclude_key( + Address::new(chain_config, data.conclude_key().clone())?.into_string(), + ); + + match additional_info { + Some(UtxoAdditionalInfo::CreateOrder { ask, give }) => { + out_req.ask = Some(to_trezor_output_value_with_token_info( + data.ask(), + &ask.as_ref(), + chain_config, + )?) + .into(); + out_req.give = Some(to_trezor_output_value_with_token_info( + data.give(), + &give.as_ref(), + chain_config, + )?) + .into(); + } + Some( + UtxoAdditionalInfo::PoolInfo { staker_balance: _ } + | UtxoAdditionalInfo::TokenInfo(_), + ) + | None => return Err(SignerError::MissingUtxoExtraInfo), + } + + let mut out = MintlayerTxOutput::new(); + out.create_order = Some(out_req).into(); + out + } + }; + Ok(res) +} + +fn to_trezor_output_value_with_token_info( + value: &OutputValue, + token_info: &Option<&TokenAdditionalInfo>, + chain_config: &ChainConfig, +) -> Result { + match value { + OutputValue::Coin(amount) => { + let mut value = MintlayerOutputValue::new(); + value.set_amount(amount.into_atoms().to_be_bytes().to_vec()); + Ok(value) + } + OutputValue::TokenV1(token_id, amount) => { + let mut value = MintlayerOutputValue::new(); + value.set_amount(amount.into_atoms().to_be_bytes().to_vec()); + let mut token_value = MintlayerTokenOutputValue::new(); + token_value.set_token_id(Address::new(chain_config, *token_id)?.into_string()); + match &token_info { + Some(info) => { + token_value.set_number_of_decimals(info.num_decimals as u32); + token_value.set_token_ticker(info.ticker.clone()); + } + None => return Err(SignerError::MissingUtxoExtraInfo), + } + value.token = Some(token_value).into(); + Ok(value) + } + OutputValue::TokenV0(_) => Err(SignerError::UnsupportedTokensV0), + } +} + +fn to_trezor_output_lock(lock: &OutputTimeLock) -> trezor_client::protos::MintlayerOutputTimeLock { + let mut lock_req = trezor_client::protos::MintlayerOutputTimeLock::new(); + match lock { + OutputTimeLock::UntilTime(time) => { + lock_req.set_until_time(time.as_int_seconds()); + } + OutputTimeLock::UntilHeight(height) => { + lock_req.set_until_height(height.into_int()); + } + OutputTimeLock::ForSeconds(sec) => { + lock_req.set_for_seconds(*sec); + } + OutputTimeLock::ForBlockCount(count) => { + lock_req.set_for_block_count(*count); + } + } + lock_req +} + +#[derive(Clone)] +pub struct TrezorSignerProvider { + client: Arc>, +} + +impl std::fmt::Debug for TrezorSignerProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("TrezorSignerProvider") + } +} + +impl TrezorSignerProvider { + pub fn new() -> Result { + let client = + find_trezor_device().map_err(|err| TrezorError::DeviceError(err.to_string()))?; + + Ok(Self { + client: Arc::new(Mutex::new(client)), + }) + } + + pub fn load_from_database( + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + ) -> WalletResult { + let client = find_trezor_device().map_err(SignerError::TrezorError)?; + + let provider = Self { + client: Arc::new(Mutex::new(client)), + }; + + check_public_keys(db_tx, &provider, chain_config)?; + + Ok(provider) + } + + fn fetch_extended_pub_key( + &self, + chain_config: &Arc, + account_index: U31, + ) -> SignerResult { + let derivation_path = make_account_path(chain_config, account_index); + let account_path = + derivation_path.as_slice().iter().map(|c| c.into_encoded_index()).collect(); + let xpub = self + .client + .lock() + .expect("poisoned lock") + .mintlayer_get_public_key(account_path) + .map_err(|e| SignerError::TrezorError(TrezorError::DeviceError(e.to_string())))?; + let chain_code = ChainCode::from(xpub.chain_code.0); + let account_pubkey = Secp256k1ExtendedPublicKey::new( + derivation_path, + chain_code, + Secp256k1PublicKey::from_bytes(&xpub.public_key.serialize()) + .map_err(|_| SignerError::TrezorError(TrezorError::InvalidKey))?, + ); + let account_pubkey = ExtendedPublicKey::new(account_pubkey); + Ok(account_pubkey) + } +} + +/// Check that the public keys in the DB are the same as the ones with the connected hardware +/// wallet +fn check_public_keys( + db_tx: &impl WalletStorageReadLocked, + provider: &TrezorSignerProvider, + chain_config: Arc, +) -> Result<(), WalletError> { + let first_acc = db_tx + .get_accounts_info()? + .iter() + .find_map(|(acc_id, info)| { + (info.account_index() == DEFAULT_ACCOUNT_INDEX).then_some(acc_id) + }) + .cloned() + .ok_or(WalletError::WalletNotInitialized)?; + let expected_pk = provider.fetch_extended_pub_key(&chain_config, DEFAULT_ACCOUNT_INDEX)?; + let loaded_acc = provider.load_account_from_database(chain_config, db_tx, &first_acc)?; + ensure!( + loaded_acc.key_chain().account_public_key() == &expected_pk, + WalletError::HardwareWalletDifferentFile + ); + Ok(()) +} + +fn find_trezor_device() -> Result { + let device = find_devices(false) + .into_iter() + .find(|device| device.model == Model::Trezor) + .ok_or(TrezorError::NoDeviceFound)?; + let mut client = device.connect().map_err(|e| TrezorError::DeviceError(e.to_string()))?; + client.init_device(None).map_err(|e| TrezorError::DeviceError(e.to_string()))?; + Ok(client) +} + +impl SignerProvider for TrezorSignerProvider { + type S = TrezorSigner; + type K = AccountKeyChainImplHardware; + + fn provide(&mut self, chain_config: Arc, _account_index: U31) -> Self::S { + TrezorSigner::new(chain_config, self.client.clone()) + } + + fn make_new_account( + &mut self, + chain_config: Arc, + account_index: U31, + name: Option, + db_tx: &mut impl WalletStorageWriteUnlocked, + ) -> WalletResult> { + let account_pubkey = self.fetch_extended_pub_key(&chain_config, account_index)?; + + let lookahead_size = db_tx.get_lookahead_size()?; + + let key_chain = AccountKeyChainImplHardware::new_from_hardware_key( + chain_config.clone(), + db_tx, + account_pubkey, + account_index, + lookahead_size, + )?; + + Account::new(chain_config, db_tx, key_chain, name) + } + + fn load_account_from_database( + &self, + chain_config: Arc, + db_tx: &impl WalletStorageReadLocked, + id: &AccountId, + ) -> WalletResult> { + Account::load_from_database(chain_config, db_tx, id) + } +} + +#[cfg(feature = "trezor-emulator")] +#[cfg(test)] +mod tests; diff --git a/wallet/src/signer/trezor_signer/tests.rs b/wallet/src/signer/trezor_signer/tests.rs new file mode 100644 index 0000000000..aafe51c5ed --- /dev/null +++ b/wallet/src/signer/trezor_signer/tests.rs @@ -0,0 +1,484 @@ +// Copyright (c) 2024 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::panic; +use std::ops::{Add, Div, Mul, Sub}; + +use super::*; +use crate::key_chain::{MasterKeyChain, LOOKAHEAD_SIZE}; +use crate::{Account, SendRequest}; +use common::chain::config::create_regtest; +use common::chain::htlc::HashedTimelockContract; +use common::chain::output_value::OutputValue; +use common::chain::signature::inputsig::arbitrary_message::produce_message_challenge; +use common::chain::stakelock::StakePoolData; +use common::chain::timelock::OutputTimeLock; +use common::chain::tokens::{NftIssuanceV0, TokenId}; +use common::chain::{ + AccountNonce, AccountOutPoint, DelegationId, Destination, GenBlock, OrderData, PoolId, + Transaction, TxInput, +}; +use common::primitives::per_thousand::PerThousand; +use common::primitives::{Amount, BlockHeight, Id, H256}; +use crypto::key::{KeyKind, PrivateKey}; +use randomness::{Rng, RngCore}; +use rstest::rstest; +use test_utils::random::{make_seedable_rng, Seed}; +use trezor_client::find_devices; +use wallet_storage::{DefaultBackend, Store, Transactional}; +use wallet_types::account_info::DEFAULT_ACCOUNT_INDEX; +use wallet_types::seed_phrase::StoreSeedPhrase; +use wallet_types::KeyPurpose::{Change, ReceiveFunds}; + +const MNEMONIC: &str = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_message(#[case] seed: Seed) { + use wallet_storage::TransactionRwUnlocked; + + let mut rng = make_seedable_rng(seed); + + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + chain_config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let destination = account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object(); + let mut devices = find_devices(false); + assert!(!devices.is_empty()); + let client = devices.pop().unwrap().connect().unwrap(); + + let mut signer = TrezorSigner::new(chain_config.clone(), Arc::new(Mutex::new(client))); + let message = vec![rng.gen::(), rng.gen::(), rng.gen::()]; + db_tx.commit().unwrap(); + let db_tx = db.local_rw_unlocked().read_only_store(); + let res = signer + .sign_challenge(&message, &destination, account.key_chain(), &db_tx) + .await + .unwrap(); + + let message_challenge = produce_message_challenge(&message); + res.verify_signature(&chain_config, &destination, &message_challenge).unwrap(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_transaction_intent(#[case] seed: Seed) { + use common::primitives::Idable; + use wallet_storage::TransactionRwUnlocked; + + let mut rng = make_seedable_rng(seed); + + let config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let mut devices = find_devices(false); + assert!(!devices.is_empty()); + let client = devices.pop().unwrap().connect().unwrap(); + + let mut signer = TrezorSigner::new(config.clone(), Arc::new(Mutex::new(client))); + + let inputs: Vec = (0..rng.gen_range(1..5)) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + let input_destinations: Vec<_> = (0..inputs.len()) + .map(|_| account.get_new_address(&mut db_tx, ReceiveFunds).unwrap().1.into_object()) + .collect(); + + let tx = Transaction::new( + 0, + inputs, + vec![TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(rng.gen())), + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), + )], + ) + .unwrap(); + + db_tx.commit().unwrap(); + let db_tx = db.local_rw_unlocked().read_only_store(); + + let intent: String = [rng.gen::(), rng.gen::(), rng.gen::()].iter().collect(); + let res = signer + .sign_transaction_intent( + &tx, + &input_destinations, + &intent, + account.key_chain(), + &db_tx, + ) + .await + .unwrap(); + + let expected_signed_message = + SignedTransactionIntent::get_message_to_sign(&intent, &tx.get_id()); + res.verify(&config, &input_destinations, &expected_signed_message).unwrap(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_transaction(#[case] seed: Seed) { + use std::num::NonZeroU8; + + use common::{ + chain::{ + classic_multisig::ClassicMultisigChallenge, + tokens::{IsTokenUnfreezable, Metadata, TokenIssuanceV1}, + OrderId, + }, + primitives::amount::UnsignedIntType, + }; + use crypto::vrf::VRFPrivateKey; + use serialization::extras::non_empty_vec::DataOrNoVec; + use wallet_storage::TransactionRwUnlocked; + + let mut rng = make_seedable_rng(seed); + + let chain_config = Arc::new(create_regtest()); + let db = Arc::new(Store::new(DefaultBackend::new_in_memory()).unwrap()); + let mut db_tx = db.transaction_rw_unlocked(None).unwrap(); + + let master_key_chain = MasterKeyChain::new_from_mnemonic( + chain_config.clone(), + &mut db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + ) + .unwrap(); + + let key_chain = master_key_chain + .create_account_key_chain(&mut db_tx, DEFAULT_ACCOUNT_INDEX, LOOKAHEAD_SIZE) + .unwrap(); + let mut account = Account::new(chain_config.clone(), &mut db_tx, key_chain, None).unwrap(); + + let amounts: Vec = (0..(2 + rng.next_u32() % 5)) + .map(|_| Amount::from_atoms(rng.gen_range(1..10) as UnsignedIntType)) + .collect(); + + let total_amount = amounts.iter().fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); + + let utxos: Vec = amounts + .iter() + .map(|a| { + let purpose = if rng.gen_bool(0.5) { + ReceiveFunds + } else { + Change + }; + + TxOutput::Transfer( + OutputValue::Coin(*a), + account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object(), + ) + }) + .collect(); + + let inputs: Vec = (0..utxos.len()) + .map(|_| { + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + TxInput::from_utxo(source_id, rng.next_u32()) + }) + .collect(); + + let (_dest_prv, pub_key1) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let pub_key2 = if let Destination::PublicKeyHash(pkh) = + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() + { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + let pub_key3 = if let Destination::PublicKeyHash(pkh) = + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object() + { + account.find_corresponding_pub_key(&pkh).unwrap() + } else { + panic!("not a public key hash") + }; + let min_required_signatures = 2; + let challenge = ClassicMultisigChallenge::new( + &chain_config, + NonZeroU8::new(min_required_signatures).unwrap(), + vec![pub_key1, pub_key2, pub_key3], + ) + .unwrap(); + let multisig_hash = account.add_standalone_multisig(&mut db_tx, challenge, None).unwrap(); + + let multisig_dest = Destination::ClassicMultisig(multisig_hash); + + let source_id = if rng.gen_bool(0.5) { + Id::::new(H256::random_using(&mut rng)).into() + } else { + Id::::new(H256::random_using(&mut rng)).into() + }; + let multisig_input = TxInput::from_utxo(source_id, rng.next_u32()); + let multisig_utxo = TxOutput::Transfer(OutputValue::Coin(Amount::from_atoms(1)), multisig_dest); + + let acc_inputs = vec![ + TxInput::Account(AccountOutPoint::new( + AccountNonce::new(1), + AccountSpending::DelegationBalance( + DelegationId::new(H256::random_using(&mut rng)), + Amount::from_atoms(rng.next_u32() as u128), + ), + )), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::MintTokens( + TokenId::new(H256::random_using(&mut rng)), + Amount::from_atoms(100), + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::UnmintTokens(TokenId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::LockTokenSupply(TokenId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FreezeToken( + TokenId::new(H256::random_using(&mut rng)), + IsTokenUnfreezable::Yes, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::UnfreezeToken(TokenId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ChangeTokenAuthority( + TokenId::new(H256::random_using(&mut rng)), + Destination::AnyoneCanSpend, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ConcludeOrder(OrderId::new(H256::random_using(&mut rng))), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::FillOrder( + OrderId::new(H256::random_using(&mut rng)), + Amount::from_atoms(123), + Destination::AnyoneCanSpend, + ), + ), + TxInput::AccountCommand( + AccountNonce::new(rng.next_u64()), + AccountCommand::ChangeTokenMetadataUri( + TokenId::new(H256::random_using(&mut rng)), + "http://uri".as_bytes().to_vec(), + ), + ), + ]; + let acc_dests: Vec = acc_inputs + .iter() + .map(|_| { + let purpose = if rng.gen_bool(0.5) { + ReceiveFunds + } else { + Change + }; + + account.get_new_address(&mut db_tx, purpose).unwrap().1.into_object() + }) + .collect(); + + let dest_amount = total_amount.div(10).unwrap().mul(5).unwrap(); + let lock_amount = total_amount.div(10).unwrap().mul(1).unwrap(); + let burn_amount = total_amount.div(10).unwrap().mul(1).unwrap(); + let change_amount = total_amount.div(10).unwrap().mul(2).unwrap(); + let outputs_amounts_sum = [dest_amount, lock_amount, burn_amount, change_amount] + .iter() + .fold(Amount::ZERO, |acc, a| acc.add(*a).unwrap()); + let _fee_amount = total_amount.sub(outputs_amounts_sum).unwrap(); + + let (_dest_prv, dest_pub) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let (_, vrf_public_key) = VRFPrivateKey::new_from_entropy(crypto::vrf::VRFKeyKind::Schnorrkel); + + let pool_id = PoolId::new(H256::random()); + let delegation_id = DelegationId::new(H256::random()); + let pool_data = StakePoolData::new( + Amount::from_atoms(5000000), + Destination::PublicKey(dest_pub.clone()), + vrf_public_key, + Destination::PublicKey(dest_pub.clone()), + PerThousand::new_from_rng(&mut rng), + Amount::from_atoms(100), + ); + let token_issuance = TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, + authority: Destination::PublicKey(dest_pub.clone()), + is_freezable: common::chain::tokens::IsTokenFreezable::No, + }); + + let nft_issuance = NftIssuance::V0(NftIssuanceV0 { + metadata: Metadata { + creator: None, + name: "Name".as_bytes().to_vec(), + description: "SomeNFT".as_bytes().to_vec(), + ticker: "NFTX".as_bytes().to_vec(), + icon_uri: DataOrNoVec::from(None), + additional_metadata_uri: DataOrNoVec::from(None), + media_uri: DataOrNoVec::from(None), + media_hash: "123456".as_bytes().to_vec(), + }, + }); + let nft_id = TokenId::new(H256::random()); + + let hash_lock = HashedTimelockContract { + secret_hash: common::chain::htlc::HtlcSecretHash([1; 20]), + spend_key: Destination::PublicKey(dest_pub.clone()), + refund_timelock: OutputTimeLock::UntilHeight(BlockHeight::new(123)), + refund_key: Destination::AnyoneCanSpend, + }; + + let order_data = OrderData::new( + Destination::PublicKey(dest_pub.clone()), + OutputValue::Coin(Amount::from_atoms(100)), + OutputValue::Coin(total_amount), + ); + + let outputs = vec![ + TxOutput::Transfer( + OutputValue::Coin(dest_amount), + Destination::PublicKey(dest_pub), + ), + TxOutput::LockThenTransfer( + OutputValue::Coin(lock_amount), + Destination::AnyoneCanSpend, + OutputTimeLock::ForSeconds(rng.next_u64()), + ), + TxOutput::Burn(OutputValue::Coin(burn_amount)), + TxOutput::CreateStakePool(pool_id, Box::new(pool_data)), + TxOutput::CreateDelegationId( + Destination::AnyoneCanSpend, + PoolId::new(H256::random_using(&mut rng)), + ), + TxOutput::DelegateStaking(burn_amount, delegation_id), + TxOutput::IssueFungibleToken(Box::new(token_issuance)), + TxOutput::IssueNft( + nft_id, + Box::new(nft_issuance.clone()), + Destination::AnyoneCanSpend, + ), + TxOutput::DataDeposit(vec![1, 2, 3]), + TxOutput::Htlc(OutputValue::Coin(burn_amount), Box::new(hash_lock)), + TxOutput::CreateOrder(Box::new(order_data)), + TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(100_000_000_000)), + account.get_new_address(&mut db_tx, Change).unwrap().1.into_object(), + ), + ]; + + let req = SendRequest::new() + .with_inputs(inputs.clone().into_iter().zip(utxos.clone()), &|_| None) + .unwrap() + .with_inputs( + [multisig_input.clone()].into_iter().zip([multisig_utxo.clone()]), + &|_| None, + ) + .unwrap() + .with_inputs_and_destinations(acc_inputs.into_iter().zip(acc_dests.clone())) + .with_outputs(outputs); + let destinations = req.destinations().to_vec(); + let additional_info = BTreeMap::new(); + let ptx = req.into_partially_signed_tx(&additional_info).unwrap(); + + let mut devices = find_devices(false); + assert!(!devices.is_empty()); + let client = devices.pop().unwrap().connect().unwrap(); + + db_tx.commit().unwrap(); + let db_tx = db.local_rw_unlocked().read_only_store(); + let mut signer = TrezorSigner::new(chain_config.clone(), Arc::new(Mutex::new(client))); + let (ptx, _, _) = signer.sign_tx(ptx, account.key_chain(), &db_tx).await.unwrap(); + + eprintln!("num inputs in tx: {} {:?}", inputs.len(), ptx.witnesses()); + assert!(ptx.all_signatures_available()); + + let utxos_ref = utxos + .iter() + .map(Some) + .chain([Some(&multisig_utxo)]) + .chain(acc_dests.iter().map(|_| None)) + .collect::>(); + + for (i, dest) in destinations.iter().enumerate() { + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + dest, + &ptx, + &utxos_ref, + i, + ) + .unwrap(); + } +} diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 11755f16bb..560f25acd2 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -14,24 +14,23 @@ // limitations under the License. use std::collections::{BTreeMap, BTreeSet}; +use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::Arc; -use crate::account::transaction_list::TransactionList; -use crate::account::{CoinSelectionAlgo, TxInfo}; use crate::account::{ - CurrentFeeRate, DelegationData, PoolData, TransactionToSign, UnconfirmedTokenInfo, - UtxoSelectorError, + transaction_list::TransactionList, CoinSelectionAlgo, CurrentFeeRate, DelegationData, PoolData, + TxInfo, UnconfirmedTokenInfo, UtxoSelectorError, }; use crate::key_chain::{ - make_account_path, make_path_to_vrf_key, KeyChainError, MasterKeyChain, LOOKAHEAD_SIZE, - VRF_INDEX, + make_account_path, make_path_to_vrf_key, AccountKeyChainImplSoftware, KeyChainError, + MasterKeyChain, LOOKAHEAD_SIZE, VRF_INDEX, }; use crate::send_request::{ - make_issue_token_outputs, IssueNftArguments, SelectedInputs, StakePoolDataArguments, + make_issue_token_outputs, IssueNftArguments, PoolOrTokenId, SelectedInputs, + StakePoolDataArguments, }; -use crate::signer::software_signer::SoftwareSigner; -use crate::signer::{Signer, SignerError}; +use crate::signer::{Signer, SignerError, SignerProvider}; use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; use crate::{Account, SendRequest}; pub use bip39::{Language, Mnemonic}; @@ -41,7 +40,6 @@ use common::chain::block::timestamp::BlockTimestamp; use common::chain::classic_multisig::ClassicMultisigChallenge; use common::chain::htlc::HashedTimelockContract; use common::chain::output_value::OutputValue; -use common::chain::partially_signed_transaction::PartiallySignedTransaction; use common::chain::signature::inputsig::arbitrary_message::{ ArbitraryMessageSignature, SignArbitraryMessageError, }; @@ -50,9 +48,10 @@ use common::chain::tokens::{ make_token_id, IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance, }; use common::chain::{ - make_order_id, AccountNonce, Block, ChainConfig, DelegationId, Destination, GenBlock, OrderId, - OutPointSourceId, PoolId, RpcOrderInfo, SignedTransaction, SignedTransactionIntent, - Transaction, TransactionCreationError, TxInput, TxOutput, UtxoOutPoint, + make_order_id, AccountCommand, AccountNonce, AccountOutPoint, Block, ChainConfig, DelegationId, + Destination, GenBlock, OrderId, OutPointSourceId, PoolId, RpcOrderInfo, SignedTransaction, + SignedTransactionIntent, Transaction, TransactionCreationError, TxInput, TxOutput, + UtxoOutPoint, }; use common::primitives::id::{hash_encoded, WithId}; use common::primitives::{Amount, BlockHeight, Id, H256}; @@ -70,17 +69,22 @@ use tx_verifier::{check_transaction, CheckTransactionError}; use utils::ensure; pub use wallet_storage::Error; use wallet_storage::{ - DefaultBackend, Store, StoreTxRw, StoreTxRwUnlocked, TransactionRoLocked, TransactionRwLocked, - TransactionRwUnlocked, Transactional, WalletStorageReadLocked, WalletStorageReadUnlocked, - WalletStorageWriteLocked, WalletStorageWriteUnlocked, + DefaultBackend, Store, StoreLocalReadWriteUnlocked, StoreTxRo, StoreTxRw, StoreTxRwUnlocked, + TransactionRoLocked, TransactionRwLocked, TransactionRwUnlocked, Transactional, + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, + WalletStorageWriteUnlocked, }; use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; use wallet_types::chain_info::ChainInfo; -use wallet_types::seed_phrase::{SerializableSeedPhrase, StoreSeedPhrase}; +use wallet_types::partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionCreationError, TokenAdditionalInfo, + UtxoAdditionalInfo, +}; +use wallet_types::seed_phrase::SerializableSeedPhrase; use wallet_types::signature_status::SignatureStatus; use wallet_types::utxo_types::{UtxoStates, UtxoTypes}; use wallet_types::wallet_tx::{TxData, TxState}; -use wallet_types::wallet_type::WalletType; +use wallet_types::wallet_type::{WalletControllerMode, WalletType}; use wallet_types::with_locked::WithLocked; use wallet_types::{ AccountId, AccountKeyPurposeId, BlockInfo, Currency, KeyPurpose, KeychainUsageState, @@ -102,7 +106,7 @@ pub enum WalletError { #[error("Wallet is not initialized")] WalletNotInitialized, #[error("A {0} wallet is trying to open a {1} wallet file")] - DifferentWalletType(WalletType, WalletType), + DifferentWalletType(WalletControllerMode, WalletType), #[error("The wallet belongs to a different chain than the one specified")] DifferentChainType, #[error("Unsupported wallet version: {0}, max supported version of this software is {CURRENT_WALLET_VERSION}")] @@ -159,6 +163,8 @@ pub enum WalletError { UnknownOrderId(OrderId), #[error("Transaction creation error: {0}")] TransactionCreation(#[from] TransactionCreationError), + #[error("Transaction creation error: {0}")] + PartiallySignedTransactionCreation(#[from] PartiallySignedTransactionCreationError), #[error("Transaction signing error: {0}")] TransactionSig(#[from] DestinationSigError), #[error("Delegation not found with id {0}")] @@ -251,6 +257,18 @@ pub enum WalletError { OrderInfoMissing(OrderId), #[error("Calculating filled amount for order {0} failed")] CalculateOrderFilledAmountFailed(OrderId), + #[error("Cannot change a Trezor wallet type")] + CannotChangeTrezorWalletType, + #[error("The file being loaded does not correspond to the connected hardware wallet")] + HardwareWalletDifferentFile, + #[error("Missing additional data for Pool {0}")] + MissingPoolAdditionalData(PoolId), + #[error("Missing additional data for Token {0}")] + MissingTokenAdditionalData(TokenId), + #[error("Mismatched additional data for token {0}")] + MismatchedTokenAdditionalData(TokenId), + #[error("Unsupported operation for a Hardware wallet")] + UnsupportedHardwareWalletOperation, } /// Result type used for the wallet @@ -264,13 +282,13 @@ pub enum WalletPoolsFilter { Stake, } -pub struct Wallet { +pub struct Wallet { chain_config: Arc, db: Store, - key_chain: MasterKeyChain, - accounts: BTreeMap, + accounts: BTreeMap>, latest_median_time: BlockTimestamp, - next_unused_account: (U31, Account), + next_unused_account: (U31, Account), + signer_provider: P, } #[derive(PartialEq, Eq, PartialOrd, Ord)] @@ -287,85 +305,62 @@ pub fn create_wallet_in_memory() -> WalletResult> { Ok(Store::new(DefaultBackend::new_in_memory())?) } -impl Wallet { - pub fn create_new_wallet( +impl Wallet +where + B: storage::Backend + 'static, + P: SignerProvider, +{ + pub fn create_new_wallet) -> WalletResult

>( chain_config: Arc, db: Store, - mnemonic: &str, - passphrase: Option<&str>, - save_seed_phrase: StoreSeedPhrase, best_block: (BlockHeight, Id), wallet_type: WalletType, + signer_provider: F, ) -> WalletResult { - let mut wallet = Self::new_wallet( - chain_config, - db, - mnemonic, - passphrase, - save_seed_phrase, - wallet_type, - )?; + let mut wallet = Self::new_wallet(chain_config, db, wallet_type, signer_provider)?; wallet.set_best_block(best_block.0, best_block.1)?; Ok(wallet) } - pub fn recover_wallet( + pub fn recover_wallet) -> WalletResult

>( chain_config: Arc, db: Store, - mnemonic: &str, - passphrase: Option<&str>, - save_seed_phrase: StoreSeedPhrase, wallet_type: WalletType, + signer_provider: F, ) -> WalletResult { - Self::new_wallet( - chain_config, - db, - mnemonic, - passphrase, - save_seed_phrase, - wallet_type, - ) + Self::new_wallet(chain_config, db, wallet_type, signer_provider) } - fn new_wallet( + fn new_wallet) -> WalletResult

>( chain_config: Arc, db: Store, - mnemonic: &str, - passphrase: Option<&str>, - save_seed_phrase: StoreSeedPhrase, wallet_type: WalletType, + signer_provider: F, ) -> WalletResult { let mut db_tx = db.transaction_rw_unlocked(None)?; - let key_chain = MasterKeyChain::new_from_mnemonic( - chain_config.clone(), - &mut db_tx, - mnemonic, - passphrase, - save_seed_phrase, - )?; - db_tx.set_storage_version(CURRENT_WALLET_VERSION)?; db_tx.set_chain_info(&ChainInfo::new(chain_config.as_ref()))?; db_tx.set_lookahead_size(LOOKAHEAD_SIZE)?; db_tx.set_wallet_type(wallet_type)?; + let mut signer_provider = signer_provider(&mut db_tx)?; - let default_account = Wallet::::create_next_unused_account( + let default_account = Wallet::::create_next_unused_account( U31::ZERO, chain_config.clone(), - &key_chain, &mut db_tx, None, + &mut signer_provider, )?; - let next_unused_account = Wallet::::create_next_unused_account( + let next_unused_account = Wallet::::create_next_unused_account( U31::ONE, chain_config.clone(), - &key_chain, &mut db_tx, None, + &mut signer_provider, )?; db_tx.commit()?; @@ -374,10 +369,10 @@ impl Wallet { let wallet = Wallet { chain_config, db, - key_chain, accounts: [default_account].into(), latest_median_time, next_unused_account, + signer_provider, }; Ok(wallet) @@ -386,7 +381,11 @@ impl Wallet { /// Migrate the wallet DB from version 1 to version 2 /// * save the chain info in the DB based on the chain type specified by the user /// * reset transactions - fn migration_v2(db: &Store, chain_config: Arc) -> WalletResult<()> { + fn migration_v2( + db: &Store, + chain_config: Arc, + signer_provider: &mut P, + ) -> WalletResult<()> { let mut db_tx = db.transaction_rw_unlocked(None)?; // set new chain info to the one provided by the user assuming it is the correct one db_tx.set_chain_info(&ChainInfo::new(chain_config.as_ref()))?; @@ -396,7 +395,7 @@ impl Wallet { Self::reset_wallet_transactions(chain_config.clone(), &mut db_tx)?; // Create the next unused account - Self::migrate_next_unused_account(chain_config, &mut db_tx)?; + Self::migrate_next_unused_account(chain_config, &mut db_tx, signer_provider)?; db_tx.set_storage_version(WALLET_VERSION_V2)?; db_tx.commit()?; @@ -444,7 +443,11 @@ impl Wallet { /// Migrate the wallet DB from version 4 to version 5 /// * set vrf key_chain usage - fn migration_v5(db: &Store, chain_config: Arc) -> WalletResult<()> { + fn migration_v5( + db: &Store, + chain_config: Arc, + signer_provider: &P, + ) -> WalletResult<()> { let mut db_tx = db.transaction_rw_unlocked(None)?; for (id, info) in db_tx.get_accounts_info()? { @@ -473,7 +476,11 @@ impl Wallet { )?; } - Self::reset_wallet_transactions_and_load(chain_config.clone(), &mut db_tx)?; + Self::reset_wallet_transactions_and_load( + chain_config.clone(), + &mut db_tx, + signer_provider, + )?; db_tx.set_storage_version(WALLET_VERSION_V5)?; db_tx.commit()?; @@ -501,7 +508,7 @@ impl Wallet { fn migration_v7( db: &Store, chain_config: Arc, - wallet_type: WalletType, + controller_mode: WalletControllerMode, ) -> WalletResult<()> { let mut db_tx = db.transaction_rw(None)?; let accs = db_tx.get_accounts_info()?; @@ -510,11 +517,11 @@ impl Wallet { accs.values().all(|acc| acc.best_block_id() == chain_config.genesis_block_id()); ensure!( - wallet_type == WalletType::Hot || cold_wallet, - WalletError::DifferentWalletType(wallet_type, WalletType::Hot) + controller_mode == WalletControllerMode::Hot || cold_wallet, + WalletError::DifferentWalletType(controller_mode, WalletType::Hot) ); - db_tx.set_wallet_type(wallet_type)?; + db_tx.set_wallet_type(controller_mode.into())?; db_tx.set_storage_version(WALLET_VERSION_V7)?; db_tx.commit()?; @@ -527,53 +534,64 @@ impl Wallet { } /// Check the wallet DB version and perform any migrations needed - fn check_and_migrate_db Result<(), WalletError>>( + fn check_and_migrate_db< + F: Fn(u32) -> Result<(), WalletError>, + F2: FnOnce(&StoreTxRo) -> WalletResult

, + >( db: &Store, chain_config: Arc, pre_migration: F, - wallet_type: WalletType, - ) -> WalletResult<()> { + controller_mode: WalletControllerMode, + signer_provider: F2, + ) -> WalletResult

{ let version = db.transaction_ro()?.get_storage_version()?; + ensure!( + version != WALLET_VERSION_UNINITIALIZED, + WalletError::WalletNotInitialized + ); + let mut signer_provider = signer_provider(&db.transaction_ro()?)?; - match version { - WALLET_VERSION_UNINITIALIZED => return Err(WalletError::WalletNotInitialized), - WALLET_VERSION_V1 => { - pre_migration(WALLET_VERSION_V1)?; - Self::migration_v2(db, chain_config.clone())?; - } - WALLET_VERSION_V2 => { - pre_migration(WALLET_VERSION_V2)?; - Self::migration_v3(db, chain_config.clone())?; - } - WALLET_VERSION_V3 => { - pre_migration(WALLET_VERSION_V3)?; - Self::migration_v4(db)?; - } - WALLET_VERSION_V4 => { - pre_migration(WALLET_VERSION_V4)?; - Self::migration_v5(db, chain_config.clone())?; - } - WALLET_VERSION_V5 => { - pre_migration(WALLET_VERSION_V5)?; - Self::migration_v6(db, chain_config.clone())?; - } - WALLET_VERSION_V6 => { - pre_migration(WALLET_VERSION_V6)?; - Self::migration_v7(db, chain_config.clone(), wallet_type)?; - } - CURRENT_WALLET_VERSION => return Ok(()), - unsupported_version => { - return Err(WalletError::UnsupportedWalletVersion(unsupported_version)) + loop { + let version = db.transaction_ro()?.get_storage_version()?; + + match version { + WALLET_VERSION_UNINITIALIZED => return Err(WalletError::WalletNotInitialized), + WALLET_VERSION_V1 => { + pre_migration(WALLET_VERSION_V1)?; + Self::migration_v2(db, chain_config.clone(), &mut signer_provider)?; + } + WALLET_VERSION_V2 => { + pre_migration(WALLET_VERSION_V2)?; + Self::migration_v3(db, chain_config.clone())?; + } + WALLET_VERSION_V3 => { + pre_migration(WALLET_VERSION_V3)?; + Self::migration_v4(db)?; + } + WALLET_VERSION_V4 => { + pre_migration(WALLET_VERSION_V4)?; + Self::migration_v5(db, chain_config.clone(), &signer_provider)?; + } + WALLET_VERSION_V5 => { + pre_migration(WALLET_VERSION_V5)?; + Self::migration_v6(db, chain_config.clone())?; + } + WALLET_VERSION_V6 => { + pre_migration(WALLET_VERSION_V6)?; + Self::migration_v7(db, chain_config.clone(), controller_mode)?; + } + CURRENT_WALLET_VERSION => return Ok(signer_provider), + unsupported_version => { + return Err(WalletError::UnsupportedWalletVersion(unsupported_version)) + } } } - - Self::check_and_migrate_db(db, chain_config, pre_migration, wallet_type) } fn validate_chain_info( chain_config: &ChainConfig, db_tx: &impl WalletStorageReadLocked, - wallet_type: WalletType, + controller_mode: WalletControllerMode, ) -> WalletResult<()> { let chain_info = db_tx.get_chain_info()?; ensure!( @@ -583,8 +601,8 @@ impl Wallet { let this_wallet_type = db_tx.get_wallet_type()?; ensure!( - this_wallet_type == wallet_type, - WalletError::DifferentWalletType(wallet_type, this_wallet_type) + this_wallet_type.is_compatible(controller_mode), + WalletError::DifferentWalletType(controller_mode, this_wallet_type) ); Ok(()) @@ -600,10 +618,11 @@ impl Wallet { fn migrate_hot_to_cold_wallet( db: &Store, chain_config: Arc, + signer_provider: &P, ) -> WalletResult<()> { let mut db_tx = db.transaction_rw(None)?; db_tx.set_wallet_type(WalletType::Cold)?; - Self::reset_wallet_transactions_and_load(chain_config, &mut db_tx)?; + Self::reset_wallet_transactions_and_load(chain_config, &mut db_tx, signer_provider)?; db_tx.commit()?; Ok(()) } @@ -612,15 +631,23 @@ impl Wallet { wallet_type: WalletType, db: &Store, chain_config: Arc, + signer_provider: &P, ) -> Result<(), WalletError> { let current_wallet_type = db.transaction_ro()?.get_wallet_type()?; match (current_wallet_type, wallet_type) { (WalletType::Cold, WalletType::Hot) => Self::migrate_cold_to_hot_wallet(db)?, (WalletType::Hot, WalletType::Cold) => { - Self::migrate_hot_to_cold_wallet(db, chain_config)? + Self::migrate_hot_to_cold_wallet(db, chain_config, signer_provider)? + } + #[cfg(feature = "trezor")] + (WalletType::Cold | WalletType::Hot, WalletType::Trezor) + | (WalletType::Trezor, WalletType::Hot | WalletType::Cold) => { + return Err(WalletError::CannotChangeTrezorWalletType) } (WalletType::Cold, WalletType::Cold) => {} (WalletType::Hot, WalletType::Hot) => {} + #[cfg(feature = "trezor")] + (WalletType::Trezor, WalletType::Trezor) => {} } Ok(()) } @@ -632,8 +659,11 @@ impl Wallet { "Resetting the wallet to genesis and starting to rescan the blockchain" ); let mut db_tx = self.db.transaction_rw(None)?; - let mut accounts = - Self::reset_wallet_transactions_and_load(self.chain_config.clone(), &mut db_tx)?; + let mut accounts = Self::reset_wallet_transactions_and_load( + self.chain_config.clone(), + &mut db_tx, + &self.signer_provider, + )?; self.next_unused_account = accounts.pop_last().expect("not empty accounts"); self.accounts = accounts; db_tx.commit()?; @@ -674,7 +704,8 @@ impl Wallet { fn reset_wallet_transactions_and_load( chain_config: Arc, db_tx: &mut impl WalletStorageWriteLocked, - ) -> WalletResult> { + signer_provider: &P, + ) -> WalletResult>> { Self::reset_wallet_transactions(chain_config.clone(), db_tx)?; // set all accounts best block to genesis @@ -682,7 +713,8 @@ impl Wallet { .get_accounts_info()? .into_keys() .map(|id| { - let mut account = Account::load_from_database(chain_config.clone(), db_tx, &id)?; + let mut account = + signer_provider.load_account_from_database(chain_config.clone(), db_tx, &id)?; account.top_up_addresses(db_tx)?; account.scan_genesis(db_tx, &WalletEventsNoOp)?; @@ -694,12 +726,14 @@ impl Wallet { fn migrate_next_unused_account( chain_config: Arc, db_tx: &mut impl WalletStorageWriteUnlocked, + signer_provider: &mut P, ) -> Result<(), WalletError> { - let key_chain = MasterKeyChain::new_from_existing_database(chain_config.clone(), db_tx)?; let accounts_info = db_tx.get_accounts_info()?; - let mut accounts: BTreeMap = accounts_info + let mut accounts: BTreeMap> = accounts_info .keys() - .map(|account_id| Account::load_from_database(chain_config.clone(), db_tx, account_id)) + .map(|account_id| { + signer_provider.load_account_from_database(chain_config.clone(), db_tx, account_id) + }) .collect::, _>>()? .into_iter() .map(|account| (account.account_index(), account)) @@ -709,46 +743,63 @@ impl Wallet { .0 .plus_one() .map_err(|_| WalletError::AbsoluteMaxNumAccountsExceeded(last_account.0))?; - Wallet::::create_next_unused_account( + Wallet::::create_next_unused_account( next_account_index, chain_config.clone(), - &key_chain, db_tx, None, + signer_provider, )?; Ok(()) } - pub fn load_wallet WalletResult<()>>( + pub fn load_wallet< + F: Fn(u32) -> WalletResult<()>, + F2: FnOnce(&StoreTxRo) -> WalletResult

, + >( chain_config: Arc, mut db: Store, password: Option, pre_migration: F, - wallet_type: WalletType, + controller_mode: WalletControllerMode, force_change_wallet_type: bool, + signer_provider: F2, ) -> WalletResult { if let Some(password) = password { db.unlock_private_keys(&password)?; } - Self::check_and_migrate_db(&db, chain_config.clone(), pre_migration, wallet_type)?; + let signer_provider = Self::check_and_migrate_db( + &db, + chain_config.clone(), + pre_migration, + controller_mode, + signer_provider, + )?; if force_change_wallet_type { - Self::force_migrate_wallet_type(wallet_type, &db, chain_config.clone())?; + Self::force_migrate_wallet_type( + controller_mode.into(), + &db, + chain_config.clone(), + &signer_provider, + )?; } // Please continue to use read-only transaction here. // Some unit tests expect that loading the wallet does not change the DB. let db_tx = db.transaction_ro()?; - Self::validate_chain_info(chain_config.as_ref(), &db_tx, wallet_type)?; - - let key_chain = MasterKeyChain::new_from_existing_database(chain_config.clone(), &db_tx)?; + Self::validate_chain_info(chain_config.as_ref(), &db_tx, controller_mode)?; let accounts_info = db_tx.get_accounts_info()?; - let mut accounts: BTreeMap = accounts_info + let mut accounts: BTreeMap> = accounts_info .keys() .map(|account_id| { - Account::load_from_database(Arc::clone(&chain_config), &db_tx, account_id) + signer_provider.load_account_from_database( + Arc::clone(&chain_config), + &db_tx, + account_id, + ) }) .collect::, _>>()? .into_iter() @@ -765,10 +816,10 @@ impl Wallet { Ok(Wallet { chain_config, db, - key_chain, accounts, latest_median_time, next_unused_account, + signer_provider, }) } @@ -823,8 +874,11 @@ impl Wallet { let mut db_tx = self.db.transaction_rw(None)?; db_tx.set_lookahead_size(lookahead_size)?; - let mut accounts = - Self::reset_wallet_transactions_and_load(self.chain_config.clone(), &mut db_tx)?; + let mut accounts = Self::reset_wallet_transactions_and_load( + self.chain_config.clone(), + &mut db_tx, + &self.signer_provider, + )?; self.next_unused_account = accounts.pop_last().expect("not empty accounts"); self.accounts = accounts; db_tx.commit()?; @@ -849,20 +903,21 @@ impl Wallet { fn create_next_unused_account( next_account_index: U31, chain_config: Arc, - master_key_chain: &MasterKeyChain, db_tx: &mut impl WalletStorageWriteUnlocked, name: Option, - ) -> WalletResult<(U31, Account)> { + signer_provider: &mut P, + ) -> WalletResult<(U31, Account)> { ensure!( name.as_ref().map_or(true, |name| !name.is_empty()), WalletError::EmptyAccountName ); - let lookahead_size = db_tx.get_lookahead_size()?; - let account_key_chain = - master_key_chain.create_account_key_chain(db_tx, next_account_index, lookahead_size)?; - - let account = Account::new(chain_config, db_tx, account_key_chain, name)?; + let account = signer_provider.make_new_account( + chain_config.clone(), + next_account_index, + name, + db_tx, + )?; Ok((next_account_index, account)) } @@ -896,9 +951,9 @@ impl Wallet { let mut next_unused_account = Self::create_next_unused_account( next_account_index, self.chain_config.clone(), - &self.key_chain, &mut db_tx, None, + &mut self.signer_provider, )?; self.next_unused_account.1.set_name(name.clone(), &mut db_tx)?; @@ -939,7 +994,7 @@ impl Wallet { fn for_account_rw( &mut self, account_index: U31, - f: impl FnOnce(&mut Account, &mut StoreTxRw) -> WalletResult, + f: impl FnOnce(&mut Account, &mut StoreTxRw) -> WalletResult, ) -> WalletResult { let mut db_tx = self.db.transaction_rw(None)?; let account = Self::get_account_mut(&mut self.accounts, account_index)?; @@ -955,11 +1010,21 @@ impl Wallet { fn for_account_rw_unlocked( &mut self, account_index: U31, - f: impl FnOnce(&mut Account, &mut StoreTxRwUnlocked, &ChainConfig) -> WalletResult, + f: impl FnOnce( + &mut Account, + &mut StoreTxRwUnlocked, + &ChainConfig, + &mut P, + ) -> WalletResult, ) -> WalletResult { let mut db_tx = self.db.transaction_rw_unlocked(None)?; let account = Self::get_account_mut(&mut self.accounts, account_index)?; - match f(account, &mut db_tx, &self.chain_config) { + match f( + account, + &mut db_tx, + &self.chain_config, + &mut self.signer_provider, + ) { Ok(value) => { // Abort the process if the DB transaction fails. See `for_account_rw` for more information. db_tx.commit().expect("RW transaction commit failed unexpectedly"); @@ -967,8 +1032,9 @@ impl Wallet { } Err(err) => { db_tx.abort(); - // In case of an error reload the keys in case the operation issued new ones and - // are saved in the cache but not in the DB + // In case of an error we should reload the keys, in the case that the operation has issued new ones keys + // we do this to prevent exhausting the keys from many failed operations, and to + // keep the cache in sync with the DB, as the DB transaction will roll back. let db_tx = self.db.transaction_ro()?; account.reload_keys(&db_tx)?; Err(err) @@ -976,82 +1042,198 @@ impl Wallet { } } - fn for_account_rw_unlocked_and_check_tx_generic( + async fn async_for_account_rw_unlocked( + &mut self, + account_index: U31, + f: impl FnOnce( + Account, + &mut StoreLocalReadWriteUnlocked, + Arc, +

::S, + ) -> Fut, + ) -> WalletResult + where + Fut: Future, WalletResult)>, + { + Self::use_account_mut(&mut self.accounts, account_index, |account| async { + let mut db_tx = self.db.local_rw_unlocked(); + + let signer = self.signer_provider.provide(self.chain_config.clone(), account_index); + let (mut account, result) = + f(account, &mut db_tx, self.chain_config.clone(), signer).await; + match result { + Ok(value) => { + match self + .db + .transaction_rw(None) + .and_then(|mut tx| db_tx.perform_operations(&mut tx).map(|_| tx)) + { + Err(err) => return (account, Err(err.into())), + Ok(tx) => { + // Abort the process if the DB transaction fails. See `for_account_rw` for more information. + tx.commit().expect("RW transaction commit failed unexpectedly"); + } + } + (account, Ok(value)) + } + Err(err) => { + // In case of an error we should reload the keys, in the case that the operation has issued new ones keys + // we do this to prevent exhausting the keys from many failed operations + if let Err(err) = self + .db + .transaction_ro() + .map_err(Into::into) + .and_then(|db_tx| account.reload_keys(&db_tx)) + { + return (account, Err(err)); + }; + + (account, Err(err)) + } + } + }) + .await? + } + + async fn async_for_account_rw_unlocked_and_check_tx_custom_error( &mut self, account_index: U31, - f: impl FnOnce(&mut Account, &mut StoreTxRwUnlocked) -> WalletResult<(SendRequest, AddlData)>, + additional_utxo_infos: &BTreeMap, + f: impl FnOnce( + &mut Account, + &mut StoreLocalReadWriteUnlocked, + ) -> WalletResult<(SendRequest, AddlData)>, error_mapper: impl FnOnce(WalletError) -> WalletError, ) -> WalletResult<(SignedTransaction, AddlData)> { let (_, block_height) = self.get_best_block_for_account(account_index)?; - self.for_account_rw_unlocked(account_index, |account, db_tx, chain_config| { - let (request, additional_data) = f(account, db_tx)?; - - let ptx = request.into_partially_signed_tx()?; - - let signer = SoftwareSigner::new(db_tx, Arc::new(chain_config.clone()), account_index); - let ptx = signer.sign_tx(ptx, account.key_chain()).map(|(ptx, _, _)| ptx)?; - let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); - let is_fully_signed = ptx.destinations().iter().enumerate().zip(ptx.witnesses()).all( - |((i, destination), witness)| match (witness, destination) { - (None | Some(_), None) | (None, Some(_)) => false, - (Some(_), Some(destination)) => { - tx_verifier::input_check::signature_only_check::verify_tx_signature( - chain_config, - destination, - &ptx, - &inputs_utxo_refs, - i, - ) - .is_ok() + self.async_for_account_rw_unlocked( + account_index, + |mut account, db_tx, chain_config, mut signer| { + let request = f(&mut account, db_tx); + let store = db_tx.read_only_store(); + async move { + let (request, additional_data) = match request { + Ok(x) => x, + Err(err) => return (account, Err(err)), + }; + + let ptx = match request.into_partially_signed_tx(additional_utxo_infos) { + Ok(x) => x, + Err(err) => return (account, Err(err)), + }; + + let ptx = match signer + .sign_tx(ptx, account.key_chain(), &store) + .await + .map(|(ptx, _, _)| ptx) + { + Ok(x) => x, + Err(err) => return (account, Err(err.into())), + }; + + let inputs_utxo_refs: Vec<_> = + ptx.input_utxos().iter().map(|u| u.as_ref().map(|x| &x.utxo)).collect(); + let is_fully_signed = ptx + .destinations() + .iter() + .enumerate() + .zip(ptx.witnesses()) + .all(|((i, destination), witness)| match (witness, destination) { + (None | Some(_), None) | (None, Some(_)) => false, + (Some(_), Some(destination)) => { + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + destination, + &ptx, + &inputs_utxo_refs, + i, + ) + .is_ok() + } + }); + + if !is_fully_signed { + return ( + account, + Err(error_mapper(WalletError::FailedToConvertPartiallySignedTx( + ptx, + ))), + ); } - }, - ); - if !is_fully_signed { - return Err(error_mapper(WalletError::FailedToConvertPartiallySignedTx( - ptx, - ))); - } - - let tx = ptx - .into_signed_tx() - .map_err(|e| error_mapper(WalletError::TransactionCreation(e)))?; + let tx = match ptx.into_signed_tx().map_err(|e| { + error_mapper(WalletError::PartiallySignedTransactionCreation(e)) + }) { + Ok(x) => x, + Err(err) => return (account, Err(err)), + }; + + if let Err(err) = + check_transaction(&chain_config, block_height.next_height(), &tx) + { + return (account, Err(err.into())); + } - check_transaction(chain_config, block_height.next_height(), &tx)?; - Ok((tx, additional_data)) - }) + (account, Ok((tx, additional_data))) + } + }, + ) + .await } - fn for_account_rw_unlocked_and_check_tx( + async fn async_for_account_rw_unlocked_and_check_tx( &mut self, account_index: U31, - f: impl FnOnce(&mut Account, &mut StoreTxRwUnlocked) -> WalletResult, + additional_utxo_infos: &BTreeMap, + f: impl FnOnce( + &mut Account, + &mut StoreLocalReadWriteUnlocked, + ) -> WalletResult, ) -> WalletResult { - Ok(self - .for_account_rw_unlocked_and_check_tx_generic( - account_index, - |account, db_tx| Ok((f(account, db_tx)?, ())), - |err| err, - )? - .0) + self.async_for_account_rw_unlocked_and_check_tx_custom_error( + account_index, + additional_utxo_infos, + |account, db_tx| Ok((f(account, db_tx)?, ())), + |err| err, + ) + .await + .map(|(tx, _)| tx) } - fn get_account(&self, account_index: U31) -> WalletResult<&Account> { + fn get_account(&self, account_index: U31) -> WalletResult<&Account> { self.accounts .get(&account_index) .ok_or(WalletError::NoAccountFoundWithIndex(account_index)) } fn get_account_mut( - accounts: &mut BTreeMap, + accounts: &mut BTreeMap>, account_index: U31, - ) -> WalletResult<&mut Account> { + ) -> WalletResult<&mut Account> { accounts .get_mut(&account_index) .ok_or(WalletError::NoAccountFoundWithIndex(account_index)) } + async fn use_account_mut( + accounts: &mut BTreeMap>, + account_index: U31, + f: impl FnOnce(Account) -> Fut, + ) -> WalletResult + where + Fut: Future, T)>, + { + let acc = accounts + .remove(&account_index) + .ok_or(WalletError::NoAccountFoundWithIndex(account_index))?; + + let (acc, res) = f(acc).await; + + accounts.insert(account_index, acc); + Ok(res) + } + pub fn get_balance( &self, account_index: U31, @@ -1071,7 +1253,7 @@ impl Wallet { utxo_types: UtxoTypes, utxo_states: UtxoStates, with_locked: WithLocked, - ) -> WalletResult)>> { + ) -> WalletResult> { let account = self.get_account(account_index)?; let utxos = account.get_multisig_utxos( utxo_types, @@ -1079,10 +1261,7 @@ impl Wallet { utxo_states, with_locked, ); - let utxos = utxos - .into_iter() - .map(|(outpoint, (txo, token_id))| (outpoint, txo.clone(), token_id)) - .collect(); + let utxos = utxos.into_iter().map(|(outpoint, txo)| (outpoint, txo.clone())).collect(); Ok(utxos) } @@ -1092,7 +1271,7 @@ impl Wallet { utxo_types: UtxoTypes, utxo_states: UtxoStates, with_locked: WithLocked, - ) -> WalletResult)>> { + ) -> WalletResult> { let account = self.get_account(account_index)?; let utxos = account.get_utxos( utxo_types, @@ -1100,18 +1279,27 @@ impl Wallet { utxo_states, with_locked, ); - let utxos = utxos - .into_iter() - .map(|(outpoint, (txo, token_id))| (outpoint, txo.clone(), token_id)) - .collect(); + let utxos = utxos.into_iter().map(|(outpoint, txo)| (outpoint, txo.clone())).collect(); Ok(utxos) } + pub fn find_account_destination(&self, acc_outpoint: &AccountOutPoint) -> Option { + self.accounts + .values() + .find_map(|acc| acc.find_account_destination(acc_outpoint).ok()) + } + + pub fn find_account_command_destination(&self, cmd: &AccountCommand) -> Option { + self.accounts + .values() + .find_map(|acc| acc.find_account_command_destination(cmd).ok()) + } + pub fn find_unspent_utxo_with_destination( &self, outpoint: &UtxoOutPoint, ) -> Option<(TxOutput, Destination)> { - self.accounts.values().find_map(|acc: &Account| { + self.accounts.values().find_map(|acc: &Account| { let current_block_info = BlockInfo { height: acc.best_block().1, timestamp: self.latest_median_time, @@ -1155,8 +1343,7 @@ impl Wallet { account_index: U31, filter: WalletPoolsFilter, ) -> WalletResult> { - let db_tx = self.db.transaction_ro_unlocked()?; - let pool_ids = self.get_account(account_index)?.get_pool_ids(filter, &db_tx); + let pool_ids = self.get_account(account_index)?.get_pool_ids(filter); Ok(pool_ids) } @@ -1212,7 +1399,7 @@ impl Wallet { private_key: PrivateKey, label: Option, ) -> WalletResult<()> { - self.for_account_rw_unlocked(account_index, |account, db_tx, _| { + self.for_account_rw_unlocked(account_index, |account, db_tx, _, _| { account.add_standalone_private_key(db_tx, private_key, label) }) } @@ -1237,15 +1424,6 @@ impl Wallet { }) } - pub fn get_vrf_key( - &mut self, - account_index: U31, - ) -> WalletResult<(ChildNumber, Address)> { - self.for_account_rw(account_index, |account, db_tx| { - account.get_new_vrf_key(db_tx) - }) - } - pub fn find_public_key( &mut self, account_index: U31, @@ -1316,22 +1494,6 @@ impl Wallet { account.get_all_standalone_address_details(address, self.latest_median_time) } - pub fn get_all_issued_vrf_public_keys( - &self, - account_index: U31, - ) -> WalletResult, bool)>> { - let account = self.get_account(account_index)?; - Ok(account.get_all_issued_vrf_public_keys()) - } - - pub fn get_legacy_vrf_public_key( - &self, - account_index: U31, - ) -> WalletResult> { - let account = self.get_account(account_index)?; - Ok(account.get_legacy_vrf_public_key()) - } - pub fn get_addresses_usage(&self, account_index: U31) -> WalletResult<&KeychainUsageState> { let account = self.get_account(account_index)?; Ok(account.get_addresses_usage()) @@ -1353,11 +1515,14 @@ impl Wallet { /// current_fee_rate is lower than the consolidate_fee_rate then the wallet will tend to /// use and consolidate multiple smaller inputs, else if the current_fee_rate is higher it will /// tend to use inputs with lowest fee. + /// * `additional_utxo_infos` - Any additional info for Tokens or Pools used in the UTXOs of + /// the transaction to be created /// /// # Returns /// /// A `WalletResult` containing the signed transaction if successful, or an error indicating the reason for failure. - pub fn create_transaction_to_addresses( + #[allow(clippy::too_many_arguments)] + pub async fn create_transaction_to_addresses( &mut self, account_index: U31, outputs: impl IntoIterator, @@ -1365,6 +1530,7 @@ impl Wallet { change_addresses: BTreeMap>, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, ) -> WalletResult { Ok(self .create_transaction_to_addresses_impl( @@ -1375,14 +1541,16 @@ impl Wallet { current_fee_rate, consolidate_fee_rate, |_s| (), - )? + &additional_utxo_infos, + ) + .await? .0) } /// Same as `create_transaction_to_addresses`, but it also allows to specify the "intent" for the transaction, /// which will be concatenated with the transaction id and signed with all the keys used to sign the transaction's inputs. #[allow(clippy::too_many_arguments)] - pub fn create_transaction_to_addresses_with_intent( + pub async fn create_transaction_to_addresses_with_intent( &mut self, account_index: U31, outputs: impl IntoIterator, @@ -1391,35 +1559,49 @@ impl Wallet { intent: String, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(SignedTransaction, SignedTransactionIntent)> { - let (signed_tx, input_destinations) = self.create_transaction_to_addresses_impl( - account_index, - outputs, - inputs, - change_addresses, - current_fee_rate, - consolidate_fee_rate, - |send_request| send_request.destinations().to_owned(), - )?; + let (signed_tx, input_destinations) = self + .create_transaction_to_addresses_impl( + account_index, + outputs, + inputs, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + |send_request| send_request.destinations().to_owned(), + additional_utxo_infos, + ) + .await?; - let signed_intent = - self.for_account_rw_unlocked(account_index, |account, db_tx, chain_config| { - let signer = - SoftwareSigner::new(db_tx, Arc::new(chain_config.clone()), account_index); - - Ok(signer.sign_transaction_intent( - signed_tx.transaction(), - &input_destinations, - &intent, - account.key_chain(), - )?) - })?; + let transaction = signed_tx.transaction(); + let signed_intent = self + .async_for_account_rw_unlocked( + account_index, + |account, db_tx, _chain_config, mut signer| { + let store = db_tx.read_only_store(); + async move { + let result = signer + .sign_transaction_intent( + transaction, + &input_destinations, + &intent, + account.key_chain(), + &store, + ) + .await + .map_err(Into::into); + (account, result) + } + }, + ) + .await?; Ok((signed_tx, signed_intent)) } #[allow(clippy::too_many_arguments)] - fn create_transaction_to_addresses_impl( + async fn create_transaction_to_addresses_impl( &mut self, account_index: U31, outputs: impl IntoIterator, @@ -1428,11 +1610,13 @@ impl Wallet { current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, additional_data_getter: impl Fn(&SendRequest) -> AddlData, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(SignedTransaction, AddlData)> { let request = SendRequest::new().with_outputs(outputs); let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx_generic( + self.async_for_account_rw_unlocked_and_check_tx_custom_error( account_index, + additional_utxo_infos, |account, db_tx| { let send_request = account.process_send_request_and_sign( db_tx, @@ -1451,6 +1635,7 @@ impl Wallet { }, |err| err, ) + .await } #[allow(clippy::too_many_arguments)] @@ -1463,6 +1648,7 @@ impl Wallet { change_addresses: BTreeMap>, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { let request = SendRequest::new().with_outputs(outputs); let latest_median_time = self.latest_median_time; @@ -1478,30 +1664,33 @@ impl Wallet { current_fee_rate, consolidate_fee_rate, }, + additional_utxo_infos, ) }) } - pub fn create_sweep_transaction( + pub async fn create_sweep_transaction( &mut self, account_index: U31, destination: Destination, - inputs: Vec<(UtxoOutPoint, TxOutput, Option)>, + inputs: Vec<(UtxoOutPoint, TxOutput)>, current_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult { let request = SendRequest::new().with_inputs( - inputs - .into_iter() - .map(|(outpoint, output, _)| (TxInput::Utxo(outpoint), output)), + inputs.into_iter().map(|(outpoint, output)| (TxInput::Utxo(outpoint), output)), &|_| None, )?; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, _| { - account.sweep_addresses(destination, request, current_fee_rate) - }) + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + additional_utxo_infos, + |account, _| account.sweep_addresses(destination, request, current_fee_rate), + ) + .await } - pub fn create_sweep_from_delegation_transaction( + pub async fn create_sweep_from_delegation_transaction( &mut self, account_index: U31, address: Address, @@ -1509,12 +1698,17 @@ impl Wallet { delegation_share: Amount, current_fee_rate: FeeRate, ) -> WalletResult { - self.for_account_rw_unlocked_and_check_tx(account_index, |account, _| { - account.sweep_delegation(address, delegation_id, delegation_share, current_fee_rate) - }) + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &BTreeMap::new(), + |account, _| { + account.sweep_delegation(address, delegation_id, delegation_share, current_fee_rate) + }, + ) + .await } - pub fn create_transaction_to_addresses_from_delegation( + pub async fn create_transaction_to_addresses_from_delegation( &mut self, account_index: U31, address: Address, @@ -1523,18 +1717,23 @@ impl Wallet { delegation_share: Amount, current_fee_rate: FeeRate, ) -> WalletResult { - self.for_account_rw_unlocked_and_check_tx(account_index, |account, _| { - account.spend_from_delegation( - address, - amount, - delegation_id, - delegation_share, - current_fee_rate, - ) - }) + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &BTreeMap::new(), + |account, _| { + account.spend_from_delegation( + address, + amount, + delegation_id, + delegation_share, + current_fee_rate, + ) + }, + ) + .await } - pub fn mint_tokens( + pub async fn mint_tokens( &mut self, account_index: U31, token_info: &UnconfirmedTokenInfo, @@ -1544,22 +1743,28 @@ impl Wallet { consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.mint_tokens( - db_tx, - token_info, - destination, - amount, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.mint_tokens( + db_tx, + token_info, + destination, + amount, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn unmint_tokens( + pub async fn unmint_tokens( &mut self, account_index: U31, token_info: &UnconfirmedTokenInfo, @@ -1568,21 +1773,27 @@ impl Wallet { consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.unmint_tokens( - db_tx, - token_info, - amount, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.unmint_tokens( + db_tx, + token_info, + amount, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn lock_token_supply( + pub async fn lock_token_supply( &mut self, account_index: U31, token_info: &UnconfirmedTokenInfo, @@ -1590,20 +1801,26 @@ impl Wallet { consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.lock_token_supply( - db_tx, - token_info, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.lock_token_supply( + db_tx, + token_info, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn freeze_token( + pub async fn freeze_token( &mut self, account_index: U31, token_info: &UnconfirmedTokenInfo, @@ -1612,21 +1829,27 @@ impl Wallet { consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.freeze_token( - db_tx, - token_info, - is_token_unfreezable, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.freeze_token( + db_tx, + token_info, + is_token_unfreezable, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn unfreeze_token( + pub async fn unfreeze_token( &mut self, account_index: U31, token_info: &UnconfirmedTokenInfo, @@ -1634,43 +1857,55 @@ impl Wallet { consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.unfreeze_token( - db_tx, - token_info, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) - } - - pub fn change_token_authority( - &mut self, - account_index: U31, - token_info: &UnconfirmedTokenInfo, - address: Address, + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.unfreeze_token( + db_tx, + token_info, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await + } + + pub async fn change_token_authority( + &mut self, + account_index: U31, + token_info: &UnconfirmedTokenInfo, + address: Address, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.change_token_authority( - db_tx, - token_info, - address, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.change_token_authority( + db_tx, + token_info, + address, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn change_token_metadata_uri( + pub async fn change_token_metadata_uri( &mut self, account_index: U31, token_info: &UnconfirmedTokenInfo, @@ -1679,18 +1914,24 @@ impl Wallet { consolidate_fee_rate: FeeRate, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.change_token_metadata_uri( - db_tx, - token_info, - metadata_uri, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + let additional_utxo_infos = to_token_additional_info(token_info); + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &additional_utxo_infos, + |account, db_tx| { + account.change_token_metadata_uri( + db_tx, + token_info, + metadata_uri, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } pub fn find_used_tokens( @@ -1705,32 +1946,35 @@ impl Wallet { pub fn get_token_unconfirmed_info( &self, account_index: U31, - token_info: &RPCFungibleTokenInfo, + token_info: RPCFungibleTokenInfo, ) -> WalletResult { self.get_account(account_index)?.get_token_unconfirmed_info(token_info) } - pub fn create_delegation( + pub async fn create_delegation( &mut self, account_index: U31, outputs: Vec, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, ) -> WalletResult<(DelegationId, SignedTransaction)> { - let tx = self.create_transaction_to_addresses( - account_index, - outputs, - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - current_fee_rate, - consolidate_fee_rate, - )?; + let tx = self + .create_transaction_to_addresses( + account_index, + outputs, + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + current_fee_rate, + consolidate_fee_rate, + BTreeMap::new(), + ) + .await?; let input0_outpoint = crate::utils::get_first_utxo_outpoint(tx.transaction().inputs())?; let delegation_id = make_delegation_id(input0_outpoint); Ok((delegation_id, tx)) } - pub fn issue_new_token( + pub async fn issue_new_token( &mut self, account_index: U31, token_issuance: TokenIssuance, @@ -1739,20 +1983,23 @@ impl Wallet { ) -> WalletResult<(TokenId, SignedTransaction)> { let outputs = make_issue_token_outputs(token_issuance, self.chain_config.as_ref())?; - let tx = self.create_transaction_to_addresses( - account_index, - outputs, - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - current_fee_rate, - consolidate_fee_rate, - )?; + let tx = self + .create_transaction_to_addresses( + account_index, + outputs, + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + current_fee_rate, + consolidate_fee_rate, + BTreeMap::new(), + ) + .await?; let token_id = make_token_id(tx.transaction().inputs()).ok_or(WalletError::MissingTokenId)?; Ok((token_id, tx)) } - pub fn issue_new_nft( + pub async fn issue_new_nft( &mut self, account_index: U31, address: Address, @@ -1763,129 +2010,176 @@ impl Wallet { let destination = address.into_object(); let latest_median_time = self.latest_median_time; - let signed_transaction = - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.create_issue_nft_tx( - db_tx, - IssueNftArguments { - metadata, - destination, - }, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - })?; + let signed_transaction = self + .async_for_account_rw_unlocked_and_check_tx( + account_index, + &BTreeMap::new(), + |account, db_tx| { + account.create_issue_nft_tx( + db_tx, + IssueNftArguments { + metadata, + destination, + }, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await?; let token_id = make_token_id(signed_transaction.transaction().inputs()) .ok_or(WalletError::MissingTokenId)?; Ok((token_id, signed_transaction)) } - pub fn create_stake_pool_tx( - &mut self, - account_index: U31, - current_fee_rate: FeeRate, - consolidate_fee_rate: FeeRate, - stake_pool_arguments: StakePoolDataArguments, - ) -> WalletResult { - let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.create_stake_pool_tx( - db_tx, - stake_pool_arguments, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) - } - - pub fn decommission_stake_pool( + pub async fn decommission_stake_pool( &mut self, account_index: U31, pool_id: PoolId, - pool_balance: Amount, + staker_balance: Amount, output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { - Ok(self - .for_account_rw_unlocked_and_check_tx_generic( - account_index, - |account, db_tx| { - Ok(( - account.decommission_stake_pool( - db_tx, - pool_id, - pool_balance, - output_address, - current_fee_rate, - )?, - (), - )) - }, - |_err| WalletError::PartiallySignedTransactionInDecommissionCommand, - )? - .0) + let additional_utxo_infos = BTreeMap::from_iter([( + PoolOrTokenId::PoolId(pool_id), + UtxoAdditionalInfo::PoolInfo { staker_balance }, + )]); + self.async_for_account_rw_unlocked_and_check_tx_custom_error( + account_index, + &additional_utxo_infos, + |account: &mut Account<

::K>, db_tx| { + account + .decommission_stake_pool( + db_tx, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .map(|r| (r, ())) + }, + |_err| WalletError::PartiallySignedTransactionInDecommissionCommand, + ) + .await + .map(|(x, _)| x) } - pub fn decommission_stake_pool_request( + pub async fn decommission_stake_pool_request( &mut self, account_index: U31, pool_id: PoolId, - pool_balance: Amount, + staker_balance: Amount, output_address: Option, current_fee_rate: FeeRate, ) -> WalletResult { - self.for_account_rw_unlocked(account_index, |account, db_tx, chain_config| { - let request = account.decommission_stake_pool_request( - db_tx, - pool_id, - pool_balance, - output_address, - current_fee_rate, - )?; - - let ptx = request.into_partially_signed_tx()?; + let additional_utxo_infos = BTreeMap::from_iter([( + PoolOrTokenId::PoolId(pool_id), + UtxoAdditionalInfo::PoolInfo { staker_balance }, + )]); - let signer = SoftwareSigner::new(db_tx, Arc::new(chain_config.clone()), account_index); - let ptx = signer.sign_tx(ptx, account.key_chain()).map(|(ptx, _, _)| ptx)?; + self.async_for_account_rw_unlocked( + account_index, + |mut account, db_tx, chain_config, signer| { + let request = account.decommission_stake_pool_request( + db_tx, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ); + + let store = db_tx.read_only_store(); + async move { + let mut signer = signer; + let request = match request { + Ok(x) => x, + Err(err) => return (account, Err(err)), + }; + + let ptx = match request.into_partially_signed_tx(&additional_utxo_infos) { + Ok(x) => x, + Err(err) => return (account, Err(err)), + }; + + let ptx = match signer + .sign_tx(ptx, account.key_chain(), &store) + .await + .map(|(ptx, _, _)| ptx) + { + Ok(x) => x, + Err(err) => return (account, Err(err.into())), + }; + + let inputs_utxo_refs: Vec<_> = + ptx.input_utxos().iter().map(|u| u.as_ref().map(|x| &x.utxo)).collect(); + let is_fully_signed = ptx + .destinations() + .iter() + .enumerate() + .zip(ptx.witnesses()) + .all(|((i, destination), witness)| match (witness, destination) { + (None | Some(_), None) | (None, Some(_)) => false, + (Some(_), Some(destination)) => { + tx_verifier::input_check::signature_only_check::verify_tx_signature( + &chain_config, + destination, + &ptx, + &inputs_utxo_refs, + i, + ) + .is_ok() + } + }); + + if is_fully_signed { + return ( + account, + Err(WalletError::FullySignedTransactionInDecommissionReq), + ); + } - if ptx.all_signatures_available() { - return Err(WalletError::FullySignedTransactionInDecommissionReq); - } - Ok(ptx) - }) + (account, Ok(ptx)) + } + }, + ) + .await } - pub fn create_htlc_tx( + pub async fn create_htlc_tx( &mut self, account_index: U31, output_value: OutputValue, htlc: HashedTimelockContract, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.create_htlc_tx( - db_tx, - output_value, - htlc, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + additional_utxo_infos, + |account, db_tx| { + account.create_htlc_tx( + db_tx, + output_value, + htlc, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn create_order_tx( + #[allow(clippy::too_many_arguments)] + pub async fn create_order_tx( &mut self, account_index: U31, ask_value: OutputValue, @@ -1893,27 +2187,35 @@ impl Wallet { conclude_key: Address, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult<(OrderId, SignedTransaction)> { let latest_median_time = self.latest_median_time; - let tx = self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.create_order_tx( - db_tx, - ask_value, - give_value, - conclude_key, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, + let tx = self + .async_for_account_rw_unlocked_and_check_tx( + account_index, + additional_utxo_infos, + |account, db_tx| { + account.create_order_tx( + db_tx, + ask_value, + give_value, + conclude_key, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) }, ) - })?; + .await?; let input0_outpoint = crate::utils::get_first_utxo_outpoint(tx.transaction().inputs())?; let order_id = make_order_id(input0_outpoint); Ok((order_id, tx)) } - pub fn create_conclude_order_tx( + #[allow(clippy::too_many_arguments)] + pub async fn create_conclude_order_tx( &mut self, account_index: U31, order_id: OrderId, @@ -1921,25 +2223,31 @@ impl Wallet { output_address: Option, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.create_conclude_order_tx( - db_tx, - order_id, - order_info, - output_address, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + additional_utxo_infos, + |account, db_tx| { + account.create_conclude_order_tx( + db_tx, + order_id, + order_info, + output_address, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } #[allow(clippy::too_many_arguments)] - pub fn create_fill_order_tx( + pub async fn create_fill_order_tx( &mut self, account_index: U31, order_id: OrderId, @@ -1948,83 +2256,74 @@ impl Wallet { output_address: Option, current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, ) -> WalletResult { let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { - account.create_fill_order_tx( - db_tx, - order_id, - order_info, - fill_amount_in_ask_currency, - output_address, - latest_median_time, - CurrentFeeRate { - current_fee_rate, - consolidate_fee_rate, - }, - ) - }) + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + additional_utxo_infos, + |account, db_tx| { + account.create_fill_order_tx( + db_tx, + order_id, + order_info, + fill_amount_in_ask_currency, + output_address, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await } - pub fn sign_raw_transaction( + pub async fn sign_raw_transaction( &mut self, account_index: U31, - tx: TransactionToSign, + ptx: PartiallySignedTransaction, ) -> WalletResult<( PartiallySignedTransaction, Vec, Vec, )> { - let latest_median_time = self.latest_median_time; - self.for_account_rw_unlocked(account_index, |account, db_tx, chain_config| { - let ptx = match tx { - TransactionToSign::Partial(ptx) => ptx, - TransactionToSign::Tx(tx) => { - account.tx_to_partially_signed_tx(tx, latest_median_time)? - } - }; - let signer = SoftwareSigner::new(db_tx, Arc::new(chain_config.clone()), account_index); + self.async_for_account_rw_unlocked( + account_index, + |account, db_tx, _chain_config, mut signer| { + let store = db_tx.read_only_store(); - let res = signer.sign_tx(ptx, account.key_chain())?; - Ok(res) - }) + async move { + let result = + signer.sign_tx(ptx, account.key_chain(), &store).await.map_err(Into::into); + (account, result) + } + }, + ) + .await } - pub fn sign_challenge( + pub async fn sign_challenge( &mut self, account_index: U31, challenge: &[u8], destination: &Destination, ) -> WalletResult { - self.for_account_rw_unlocked(account_index, |account, db_tx, chain_config| { - let signer = SoftwareSigner::new(db_tx, Arc::new(chain_config.clone()), account_index); - let msg = signer.sign_challenge(challenge, destination, account.key_chain())?; - Ok(msg) - }) - } - - pub fn get_pos_gen_block_data( - &self, - account_index: U31, - pool_id: PoolId, - ) -> WalletResult { - let db_tx = self.db.transaction_ro_unlocked()?; - self.get_account(account_index)?.get_pos_gen_block_data(&db_tx, pool_id) - } - - pub fn get_pos_gen_block_data_by_pool_id( - &self, - pool_id: PoolId, - ) -> WalletResult { - let db_tx = self.db.transaction_ro_unlocked()?; - - for acc in self.accounts.values() { - if acc.pool_exists(pool_id) { - return acc.get_pos_gen_block_data(&db_tx, pool_id); - } - } - - Err(WalletError::UnknownPoolId(pool_id)) + self.async_for_account_rw_unlocked( + account_index, + |account, db_tx, _chain_config, mut signer| { + let store = db_tx.read_only_store(); + async move { + let result = signer + .sign_challenge(challenge, destination, account.key_chain(), &store) + .await + .map_err(Into::into); + (account, result) + } + }, + ) + .await } /// Returns the last scanned block hash and height for all accounts. @@ -2194,5 +2493,97 @@ impl Wallet { } } +fn to_token_additional_info( + token_info: &UnconfirmedTokenInfo, +) -> BTreeMap { + BTreeMap::from_iter([( + PoolOrTokenId::TokenId(token_info.token_id()), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.num_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + )]) +} + +impl Wallet +where + B: storage::Backend + 'static, + P: SignerProvider, +{ + pub fn get_vrf_key( + &mut self, + account_index: U31, + ) -> WalletResult<(ChildNumber, Address)> { + self.for_account_rw(account_index, |account, db_tx| { + account.get_new_vrf_key(db_tx) + }) + } + + pub fn get_all_issued_vrf_public_keys( + &self, + account_index: U31, + ) -> WalletResult, bool)>> { + let account = self.get_account(account_index)?; + Ok(account.get_all_issued_vrf_public_keys()) + } + + pub fn get_legacy_vrf_public_key( + &self, + account_index: U31, + ) -> WalletResult> { + let account = self.get_account(account_index)?; + Ok(account.get_legacy_vrf_public_key()) + } + + pub async fn create_stake_pool_tx( + &mut self, + account_index: U31, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + stake_pool_arguments: StakePoolDataArguments, + ) -> WalletResult { + let latest_median_time = self.latest_median_time; + self.async_for_account_rw_unlocked_and_check_tx( + account_index, + &BTreeMap::new(), + |account, db_tx| { + account.create_stake_pool_tx( + db_tx, + stake_pool_arguments, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }, + ) + .await + } + pub fn get_pos_gen_block_data( + &self, + account_index: U31, + pool_id: PoolId, + ) -> WalletResult { + let db_tx = self.db.transaction_ro_unlocked()?; + self.get_account(account_index)?.get_pos_gen_block_data(&db_tx, pool_id) + } + + pub fn get_pos_gen_block_data_by_pool_id( + &self, + pool_id: PoolId, + ) -> WalletResult { + let db_tx = self.db.transaction_ro_unlocked()?; + + for acc in self.accounts.values() { + if acc.pool_exists(pool_id) { + return acc.get_pos_gen_block_data(&db_tx, pool_id); + } + } + + Err(WalletError::UnknownPoolId(pool_id)) + } +} + #[cfg(test)] mod tests; diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 3aaebc9d89..7943603909 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -16,6 +16,7 @@ use crate::{ key_chain::{make_account_path, LOOKAHEAD_SIZE}, send_request::{make_address_output, make_create_delegation_output}, + signer::software_signer::SoftwareSignerProvider, wallet_events::WalletEventsNoOp, DefaultWallet, }; @@ -53,7 +54,11 @@ use test_utils::random::{make_seedable_rng, Seed}; use wallet_storage::{schema, WalletStorageEncryptionRead}; use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, - seed_phrase::PassPhrase, + partially_signed_transaction::{ + PartiallySignedTransaction, PartiallySignedTransactionCreationError, UtxoAdditionalInfo, + UtxoWithAdditionalInfo, + }, + seed_phrase::{PassPhrase, StoreSeedPhrase}, utxo_types::{UtxoState, UtxoType}, Currency, }; @@ -69,12 +74,20 @@ const MNEMONIC2: &str = const NETWORK_FEE: u128 = 10000; -fn get_best_block(wallet: &DefaultWallet) -> (Id, BlockHeight) { +fn get_best_block(wallet: &Wallet) -> (Id, BlockHeight) +where + B: storage::Backend + 'static, + P: SignerProvider, +{ *wallet.get_best_block().first_key_value().unwrap().1 } #[track_caller] -fn scan_wallet(wallet: &mut DefaultWallet, height: BlockHeight, blocks: Vec) { +fn scan_wallet(wallet: &mut Wallet, height: BlockHeight, blocks: Vec) +where + B: storage::Backend + 'static, + P: SignerProvider, +{ for account in wallet.get_best_block().keys() { wallet .scan_new_blocks(*account, height, blocks.clone(), &WalletEventsNoOp) @@ -173,7 +186,11 @@ fn get_address( .unwrap() } -fn get_coin_balance_for_acc(wallet: &DefaultWallet, account: U31) -> Amount { +fn get_coin_balance_for_acc(wallet: &Wallet, account: U31) -> Amount +where + B: storage::Backend + 'static, + P: SignerProvider, +{ let coin_balance = wallet .get_balance(account, UtxoState::Confirmed.into(), WithLocked::Unlocked) .unwrap() @@ -183,7 +200,11 @@ fn get_coin_balance_for_acc(wallet: &DefaultWallet, account: U31) -> Amount { coin_balance } -fn get_coin_balance_with_inactive(wallet: &DefaultWallet) -> Amount { +fn get_coin_balance_with_inactive(wallet: &Wallet) -> Amount +where + B: storage::Backend + 'static, + P: SignerProvider, +{ let coin_balance = wallet .get_balance( DEFAULT_ACCOUNT_INDEX, @@ -197,11 +218,19 @@ fn get_coin_balance_with_inactive(wallet: &DefaultWallet) -> Amount { coin_balance } -fn get_coin_balance(wallet: &DefaultWallet) -> Amount { +fn get_coin_balance(wallet: &Wallet) -> Amount +where + B: storage::Backend + 'static, + P: SignerProvider, +{ get_coin_balance_for_acc(wallet, DEFAULT_ACCOUNT_INDEX) } -fn get_currency_balances(wallet: &DefaultWallet) -> (Amount, Vec<(TokenId, Amount)>) { +fn get_currency_balances(wallet: &Wallet) -> (Amount, Vec<(TokenId, Amount)>) +where + B: storage::Backend + 'static, + P: SignerProvider, +{ let mut currency_balances = wallet .get_balance( DEFAULT_ACCOUNT_INDEX, @@ -223,11 +252,14 @@ fn get_currency_balances(wallet: &DefaultWallet) -> (Amount, Vec<(TokenId, Amoun } #[track_caller] -fn verify_wallet_balance( +fn verify_wallet_balance( chain_config: &Arc, - wallet: &DefaultWallet, + wallet: &Wallet, expected_balance: Amount, -) { +) where + B: storage::Backend + 'static, + P: SignerProvider, +{ let coin_balance = get_coin_balance(wallet); assert_eq!(coin_balance, expected_balance); @@ -238,47 +270,58 @@ fn verify_wallet_balance( db_copy, None, |_| Ok(()), - WalletType::Hot, + WalletControllerMode::Hot, false, + |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) .unwrap(); + + wallet.get_best_block(); + let coin_balance = get_coin_balance(&wallet); // Check that the loaded wallet has the same balance assert_eq!(coin_balance, expected_balance); } #[track_caller] -fn create_wallet(chain_config: Arc) -> Wallet { +fn create_wallet(chain_config: Arc) -> DefaultWallet { create_wallet_with_mnemonic(chain_config, MNEMONIC) } #[track_caller] -fn create_wallet_with_mnemonic( - chain_config: Arc, - mnemonic: &str, -) -> Wallet { +fn create_wallet_with_mnemonic(chain_config: Arc, mnemonic: &str) -> DefaultWallet { let db = create_wallet_in_memory().unwrap(); let genesis_block_id = chain_config.genesis_block_id(); Wallet::create_new_wallet( - chain_config, + chain_config.clone(), db, - mnemonic, - None, - StoreSeedPhrase::DoNotStore, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config, + db_tx, + mnemonic, + None, + StoreSeedPhrase::DoNotStore, + )?) + }, ) .unwrap() } #[track_caller] -fn create_block( +fn create_block( chain_config: &Arc, - wallet: &mut DefaultWallet, + wallet: &mut Wallet, transactions: Vec, reward: Amount, block_height: u64, -) -> (Address, Block) { +) -> (Address, Block) +where + B: storage::Backend + 'static, + P: SignerProvider, +{ let address = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; let block1 = Block::new( @@ -316,6 +359,7 @@ fn test_balance_from_genesis( fn wallet_creation_in_memory() { let chain_config = Arc::new(create_regtest()); let empty_db = create_wallet_in_memory().unwrap(); + let chain_config2 = chain_config.clone(); // fail to load an empty wallet match Wallet::load_wallet( @@ -323,8 +367,9 @@ fn wallet_creation_in_memory() { empty_db, None, |_| Ok(()), - WalletType::Hot, + WalletControllerMode::Hot, false, + |db_tx| SoftwareSignerProvider::load_from_database(chain_config2, db_tx), ) { Ok(_) => panic!("Wallet loading should fail"), Err(err) => assert_eq!(err, WalletError::WalletNotInitialized), @@ -336,12 +381,13 @@ fn wallet_creation_in_memory() { // successfully load a wallet from initialized db let _wallet = Wallet::load_wallet( - chain_config, + chain_config.clone(), initialized_db, None, |_| Ok(()), - WalletType::Hot, + WalletControllerMode::Hot, false, + |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) .unwrap(); } @@ -371,11 +417,17 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { let mut wallet = Wallet::create_new_wallet( Arc::clone(&chain_config), db, - MNEMONIC, - None, - StoreSeedPhrase::DoNotStore, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + )?) + }, ) .unwrap(); verify_wallet_balance(&chain_config, &wallet, genesis_amount); @@ -424,8 +476,9 @@ fn wallet_migration_to_v2(#[case] seed: Seed) { new_db, password, |_| Ok(()), - WalletType::Hot, + WalletControllerMode::Hot, false, + |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) .unwrap(); @@ -469,11 +522,17 @@ fn wallet_seed_phrase_retrieval(#[case] seed: Seed) { let mut wallet = Wallet::create_new_wallet( Arc::clone(&chain_config), db, - MNEMONIC, - wallet_passphrase.as_ref().map(|p| p.as_ref()), - StoreSeedPhrase::Store, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + MNEMONIC, + wallet_passphrase.as_ref().map(|p| p.as_ref()), + StoreSeedPhrase::Store, + )?) + }, ) .unwrap(); @@ -558,11 +617,17 @@ fn wallet_seed_phrase_check_address() { let mut wallet = Wallet::create_new_wallet( Arc::clone(&chain_config), db, - MNEMONIC, - wallet_passphrase.as_ref().map(|p| p.as_ref()), - StoreSeedPhrase::Store, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + MNEMONIC, + wallet_passphrase.as_ref().map(|p| p.as_ref()), + StoreSeedPhrase::Store, + )?) + }, ) .unwrap(); @@ -578,11 +643,17 @@ fn wallet_seed_phrase_check_address() { let mut wallet = Wallet::create_new_wallet( Arc::clone(&chain_config), db, - MNEMONIC, - wallet_passphrase.as_ref().map(|p| p.as_ref()), - StoreSeedPhrase::Store, (BlockHeight::new(0), genesis_block_id), WalletType::Hot, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + MNEMONIC, + wallet_passphrase.as_ref().map(|p| p.as_ref()), + StoreSeedPhrase::Store, + )?) + }, ) .unwrap(); @@ -867,11 +938,14 @@ fn wallet_balance_parent_child_transactions() { } #[track_caller] -fn test_wallet_accounts( +fn test_wallet_accounts( chain_config: &Arc, - wallet: &DefaultWallet, + wallet: &Wallet, expected_accounts: Vec, -) { +) where + B: storage::Backend + 'static, + P: SignerProvider, +{ let accounts = wallet.account_indexes().cloned().collect::>(); assert_eq!(accounts, expected_accounts); @@ -881,16 +955,17 @@ fn test_wallet_accounts( db_copy, None, |_| Ok(()), - WalletType::Hot, + WalletControllerMode::Hot, false, + |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), ) .unwrap(); let accounts = wallet.account_indexes().cloned().collect::>(); assert_eq!(accounts, expected_accounts); } -#[test] -fn wallet_accounts_creation() { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_accounts_creation() { let chain_config = Arc::new(create_mainnet()); let mut wallet = create_wallet(chain_config.clone()); @@ -924,7 +999,9 @@ fn wallet_accounts_creation() { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); // even with an unconfirmed transaction we cannot create a new account @@ -992,7 +1069,7 @@ fn wallet_recover_new_account(#[case] seed: Seed) { let mut total_amounts = BTreeMap::new(); let mut last_account_index = DEFAULT_ACCOUNT_INDEX; - let blocks = (0..rng.gen_range(1..1000)) + let blocks = (0..rng.gen_range(1..100)) .map(|idx| { let tx_amount1 = Amount::from_atoms(rng.gen_range(1..10)); total_amounts @@ -1048,7 +1125,8 @@ fn wallet_recover_new_account(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1076,17 +1154,20 @@ fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) { ); assert_eq!( - wallet.create_transaction_to_addresses( - DEFAULT_ACCOUNT_INDEX, - [new_output.clone()], - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - FeeRate::from_amount_per_kb(Amount::ZERO), - FeeRate::from_amount_per_kb(Amount::ZERO), - ), - Err(WalletError::DatabaseError( - wallet_storage::Error::WalletLocked - )) + wallet + .create_transaction_to_addresses( + DEFAULT_ACCOUNT_INDEX, + [new_output.clone()], + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + FeeRate::from_amount_per_kb(Amount::ZERO), + FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), + ) + .await, + Err(WalletError::SignerError(SignerError::KeyChainError( + KeyChainError::DatabaseError(wallet_storage::Error::WalletLocked) + ))) ); // success after unlock @@ -1100,7 +1181,9 @@ fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); } else { // check if we remove the password it should fail to lock @@ -1129,14 +1212,17 @@ fn locked_wallet_cant_sign_transaction(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); } } #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_get_transaction(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_get_transaction(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1156,7 +1242,9 @@ fn wallet_get_transaction(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); let tx_id = tx.transaction().get_id(); @@ -1193,7 +1281,8 @@ fn wallet_get_transaction(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_list_mainchain_transactions(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_list_mainchain_transactions(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -1215,7 +1304,9 @@ fn wallet_list_mainchain_transactions(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); let send_tx_id = tx.transaction().get_id(); @@ -1237,7 +1328,9 @@ fn wallet_list_mainchain_transactions(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); let spend_from_tx_id = tx.transaction().get_id(); @@ -1259,7 +1352,8 @@ fn wallet_list_mainchain_transactions(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_transaction_with_fees(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_transaction_with_fees(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1291,7 +1385,9 @@ fn wallet_transaction_with_fees(#[case] seed: Seed) { BTreeMap::new(), very_big_feerate, very_big_feerate, + BTreeMap::new(), ) + .await .unwrap_err(); match err { @@ -1318,7 +1414,9 @@ fn wallet_transaction_with_fees(#[case] seed: Seed) { BTreeMap::new(), feerate, feerate, + BTreeMap::new(), ) + .await .unwrap(); let tx_size = serialization::Encode::encoded_size(&transaction); @@ -1353,7 +1451,8 @@ fn lock_wallet_fail_empty_password() { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn spend_from_user_specified_utxos(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn spend_from_user_specified_utxos(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1404,7 +1503,9 @@ fn spend_from_user_specified_utxos(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotFindUtxo(missing_utxo.clone())); @@ -1417,7 +1518,7 @@ fn spend_from_user_specified_utxos(#[case] seed: Seed) { let selected_utxos = utxos .iter() - .map(|(outpoint, _, _)| outpoint) + .map(|(outpoint, _)| outpoint) .take(rng.gen_range(1..utxos.len())) .cloned() .collect_vec(); @@ -1430,7 +1531,9 @@ fn spend_from_user_specified_utxos(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); // check that we only have the selected_utxo as inputs @@ -1467,7 +1570,9 @@ fn spend_from_user_specified_utxos(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap_err(); assert_eq!( @@ -1480,7 +1585,8 @@ fn spend_from_user_specified_utxos(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { use crypto::vrf::transcript::no_rng::VRFTranscript; let mut rng = make_seedable_rng(seed); @@ -1517,6 +1623,7 @@ fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { decommission_key: decommission_key.as_object().clone(), }, ) + .await .unwrap(); let stake_pool_transaction_id = stake_pool_transaction.transaction().get_id(); let (addr, block2) = create_block( @@ -1549,7 +1656,7 @@ fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { ) .unwrap(); assert_eq!(create_stake_pool_utxos.len(), 1); - let (_, output, _) = create_stake_pool_utxos.pop().unwrap(); + let (_, output) = create_stake_pool_utxos.pop().unwrap(); match output { TxOutput::CreateStakePool(id, data) => { assert_eq!(id, *pool_id); @@ -1609,6 +1716,7 @@ fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) + .await .unwrap(); let _ = create_block( @@ -1628,7 +1736,8 @@ fn create_stake_pool_and_list_pool_ids(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn reset_keys_after_failed_transaction(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn reset_keys_after_failed_transaction(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1649,17 +1758,19 @@ fn reset_keys_after_failed_transaction(#[case] seed: Seed) { let last_issued_address = wallet.get_addresses_usage(DEFAULT_ACCOUNT_INDEX).unwrap().last_issued(); - let result = wallet.create_stake_pool_tx( - DEFAULT_ACCOUNT_INDEX, - FeeRate::from_amount_per_kb(Amount::ZERO), - FeeRate::from_amount_per_kb(Amount::ZERO), - StakePoolDataArguments { - amount: not_enough, - margin_ratio_per_thousand: PerThousand::new_from_rng(&mut rng), - cost_per_block: Amount::ZERO, - decommission_key: Destination::AnyoneCanSpend, - }, - ); + let result = wallet + .create_stake_pool_tx( + DEFAULT_ACCOUNT_INDEX, + FeeRate::from_amount_per_kb(Amount::ZERO), + FeeRate::from_amount_per_kb(Amount::ZERO), + StakePoolDataArguments { + amount: not_enough, + margin_ratio_per_thousand: PerThousand::new_from_rng(&mut rng), + cost_per_block: Amount::ZERO, + decommission_key: Destination::AnyoneCanSpend, + }, + ) + .await; // check that result is an error and we last issued address is still the same assert!(result.is_err()); assert_eq!( @@ -1671,7 +1782,8 @@ fn reset_keys_after_failed_transaction(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn send_to_unknown_delegation(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn send_to_unknown_delegation(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1733,6 +1845,7 @@ fn send_to_unknown_delegation(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block_height = 2; @@ -1764,7 +1877,9 @@ fn send_to_unknown_delegation(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); let block_height = 3; @@ -1802,6 +1917,7 @@ fn send_to_unknown_delegation(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let _ = create_block( @@ -1826,7 +1942,8 @@ fn send_to_unknown_delegation(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_spend_from_delegations(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_spend_from_delegations(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -1860,6 +1977,7 @@ fn create_spend_from_delegations(#[case] seed: Seed) { decommission_key: Destination::AnyoneCanSpend, }, ) + .await .unwrap(); let (address, _) = create_block( @@ -1884,6 +2002,7 @@ fn create_spend_from_delegations(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let _ = create_block( @@ -1911,7 +2030,9 @@ fn create_spend_from_delegations(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); let _ = create_block( @@ -1931,6 +2052,7 @@ fn create_spend_from_delegations(#[case] seed: Seed) { Amount::from_atoms(2), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet @@ -1963,6 +2085,7 @@ fn create_spend_from_delegations(#[case] seed: Seed) { Amount::from_atoms(1), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet .add_account_unconfirmed_tx( @@ -2034,6 +2157,7 @@ fn create_spend_from_delegations(#[case] seed: Seed) { Amount::from_atoms(1), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet .add_account_unconfirmed_tx(DEFAULT_ACCOUNT_INDEX, delegation_tx3, &WalletEventsNoOp) @@ -2060,7 +2184,8 @@ fn create_spend_from_delegations(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn issue_and_transfer_tokens(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn issue_and_transfer_tokens(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -2115,10 +2240,12 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let amount_fraction = (block1_amount.into_atoms() - NETWORK_FEE) / 10; let mut token_amount_to_issue = Amount::from_atoms(rng.gen_range(1..amount_fraction)); + let mut number_of_decimals = rng.gen_range(1..18); + let token_ticker = "XXXX".as_bytes().to_vec(); let (issued_token_id, token_issuance_transactions) = if issue_fungible_token { let token_issuance = TokenIssuanceV1 { - token_ticker: "XXXX".as_bytes().to_vec(), - number_of_decimals: rng.gen_range(1..18), + token_ticker: token_ticker.clone(), + number_of_decimals, metadata_uri: "http://uri".as_bytes().to_vec(), total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, authority: token_authority_and_destination.as_object().clone(), @@ -2132,6 +2259,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let freezable = token_issuance.is_freezable.as_bool(); @@ -2175,7 +2303,9 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet .add_account_unconfirmed_tx( @@ -2187,7 +2317,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { // wallet1 should know about the issued token from the random wallet let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info).unwrap(); let mint_transaction = wallet .mint_tokens( DEFAULT_ACCOUNT_INDEX, @@ -2197,6 +2327,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); ( @@ -2204,6 +2335,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { vec![token_issuance_transaction, transfer_tx, mint_transaction], ) } else { + number_of_decimals = 0; token_amount_to_issue = Amount::from_atoms(1); let (issued_token_id, nft_issuance_transaction) = random_issuing_wallet .issue_new_nft( @@ -2213,7 +2345,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { creator: None, name: "Name".as_bytes().to_vec(), description: "SomeNFT".as_bytes().to_vec(), - ticker: "XXXX".as_bytes().to_vec(), + ticker: token_ticker.clone(), icon_uri: DataOrNoVec::from(None), additional_metadata_uri: DataOrNoVec::from(None), media_uri: DataOrNoVec::from(None), @@ -2222,6 +2354,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); random_issuing_wallet .add_unconfirmed_tx(nft_issuance_transaction.clone(), &WalletEventsNoOp) @@ -2238,7 +2371,9 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); (issued_token_id, vec![nft_issuance_transaction, transfer_tx]) }; @@ -2275,6 +2410,13 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { Destination::PublicKeyHash(some_other_address), ); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(*token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: number_of_decimals, + ticker: token_ticker.clone(), + }), + )]); let transfer_tokens_transaction = wallet .create_transaction_to_addresses( DEFAULT_ACCOUNT_INDEX, @@ -2283,7 +2425,9 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + additional_info.clone(), ) + .await .unwrap(); wallet .add_account_unconfirmed_tx( @@ -2336,7 +2480,9 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + additional_info, ) + .await .err() .unwrap(); @@ -2360,7 +2506,8 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn check_tokens_v0_are_ignored(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn check_tokens_v0_are_ignored(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -2380,24 +2527,29 @@ fn check_tokens_v0_are_ignored(#[case] seed: Seed) { assert_eq!(coin_balance, block1_amount); let address2 = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; - let result = wallet.create_transaction_to_addresses( - DEFAULT_ACCOUNT_INDEX, - [TxOutput::Transfer( - OutputValue::TokenV0(Box::new(TokenData::TokenIssuance(Box::new( - TokenIssuanceV0 { - token_ticker: "XXXX".as_bytes().to_vec(), - number_of_decimals: rng.gen_range(1..18), - metadata_uri: "http://uri".as_bytes().to_vec(), - amount_to_issue: Amount::from_atoms(rng.gen_range(1..10000)), - }, - )))), - address2.into_object(), - )], - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - FeeRate::from_amount_per_kb(Amount::ZERO), - FeeRate::from_amount_per_kb(Amount::ZERO), - ); + let token_ticker = "XXXX".as_bytes().to_vec(); + let number_of_decimals = rng.gen_range(1..18); + let result = wallet + .create_transaction_to_addresses( + DEFAULT_ACCOUNT_INDEX, + [TxOutput::Transfer( + OutputValue::TokenV0(Box::new(TokenData::TokenIssuance(Box::new( + TokenIssuanceV0 { + token_ticker, + number_of_decimals, + metadata_uri: "http://uri".as_bytes().to_vec(), + amount_to_issue: Amount::from_atoms(rng.gen_range(1..10000)), + }, + )))), + address2.into_object(), + )], + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + FeeRate::from_amount_per_kb(Amount::ZERO), + FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), + ) + .await; matches!( result.unwrap_err(), @@ -2415,7 +2567,8 @@ fn check_tokens_v0_are_ignored(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -2452,6 +2605,7 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -2476,8 +2630,9 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let amount_to_mint = Amount::from_atoms(rng.gen_range(1..=fixed_max_amount.into_atoms())); let mint_tx = wallet @@ -2489,12 +2644,14 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let _ = create_block(&chain_config, &mut wallet, vec![mint_tx], block2_amount, 2); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let freeze_tx = wallet .freeze_token( @@ -2504,12 +2661,14 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet.add_unconfirmed_tx(freeze_tx.clone(), &WalletEventsNoOp).unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.check_can_freeze().unwrap_err(), @@ -2531,12 +2690,14 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet.add_unconfirmed_tx(unfreeze_tx.clone(), &WalletEventsNoOp).unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); unconfirmed_token_info.check_can_freeze().unwrap(); assert_eq!( @@ -2554,8 +2715,9 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { .abandon_transaction(DEFAULT_ACCOUNT_INDEX, freeze_tx.transaction().get_id()) .unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); unconfirmed_token_info.check_can_freeze().unwrap(); assert_eq!( @@ -2576,8 +2738,9 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { 3, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -2599,6 +2762,7 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let tokens_to_transfer = Amount::from_atoms(rng.gen_range(1..=amount_to_mint.into_atoms())); @@ -2608,6 +2772,13 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { Destination::PublicKeyHash(some_other_address), ); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let transfer_tokens_transaction = wallet .create_transaction_to_addresses( DEFAULT_ACCOUNT_INDEX, @@ -2616,7 +2787,9 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + additional_info, ) + .await .unwrap(); wallet @@ -2624,8 +2797,9 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { .unwrap(); wallet.add_unconfirmed_tx(freeze_tx.clone(), &WalletEventsNoOp).unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.check_can_freeze().unwrap_err(), @@ -2650,6 +2824,7 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotFreezeAlreadyFrozenToken); @@ -2661,6 +2836,7 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotUnfreezeToken); @@ -2699,7 +2875,8 @@ fn freeze_and_unfreeze_tokens(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn change_token_supply_fixed(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn change_token_supply_fixed(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -2735,6 +2912,7 @@ fn change_token_supply_fixed(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -2759,8 +2937,9 @@ fn change_token_supply_fixed(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.authority().unwrap(), @@ -2787,12 +2966,14 @@ fn change_token_supply_fixed(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet.add_unconfirmed_tx(mint_transaction.clone(), &WalletEventsNoOp).unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); // Try to mint more then the fixed maximum let leftover = (fixed_max_amount - token_amount_to_mint).unwrap(); @@ -2807,6 +2988,7 @@ fn change_token_supply_fixed(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!( @@ -2837,8 +3019,9 @@ fn change_token_supply_fixed(#[case] seed: Seed) { ) .unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -2860,8 +3043,9 @@ fn change_token_supply_fixed(#[case] seed: Seed) { ); token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -2883,6 +3067,7 @@ fn change_token_supply_fixed(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!( err, @@ -2899,13 +3084,15 @@ fn change_token_supply_fixed(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet .add_unconfirmed_tx(unmint_transaction.clone(), &WalletEventsNoOp) .unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let _ = create_block( &chain_config, @@ -2916,8 +3103,9 @@ fn change_token_supply_fixed(#[case] seed: Seed) { ); token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -2940,6 +3128,7 @@ fn change_token_supply_fixed(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotLockTokenSupply("Fixed")); } @@ -2947,7 +3136,8 @@ fn change_token_supply_fixed(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn change_token_supply_unlimited(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn change_token_supply_unlimited(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -2982,6 +3172,7 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -3007,8 +3198,9 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.authority().unwrap(), @@ -3035,11 +3227,13 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet.add_unconfirmed_tx(mint_transaction.clone(), &WalletEventsNoOp).unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let _ = create_block( &chain_config, @@ -3050,8 +3244,9 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { ); token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -3073,6 +3268,7 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!( err, @@ -3089,12 +3285,14 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet .add_unconfirmed_tx(unmint_transaction.clone(), &WalletEventsNoOp) .unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let _ = create_block( &chain_config, @@ -3105,8 +3303,9 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { ); token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -3129,6 +3328,7 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotLockTokenSupply("Unlimited")); } @@ -3136,7 +3336,8 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -3171,6 +3372,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -3196,8 +3398,9 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.authority().unwrap(), @@ -3224,10 +3427,12 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet.add_unconfirmed_tx(mint_transaction.clone(), &WalletEventsNoOp).unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let _ = create_block( &chain_config, @@ -3238,8 +3443,9 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { ); token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -3261,6 +3467,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!( err, @@ -3277,13 +3484,15 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); wallet .add_unconfirmed_tx(unmint_transaction.clone(), &WalletEventsNoOp) .unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let _ = create_block( &chain_config, @@ -3294,8 +3503,9 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { ); token_info.circulating_supply = unconfirmed_token_info.current_supply().unwrap(); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -3315,6 +3525,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let _ = create_block( @@ -3326,8 +3537,9 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { ); token_info.is_locked = true; - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); assert_eq!( unconfirmed_token_info.get_next_nonce().unwrap(), @@ -3353,6 +3565,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotChangeLockedTokenSupply); let err = wallet @@ -3363,6 +3576,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotChangeLockedTokenSupply); @@ -3373,6 +3587,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap_err(); assert_eq!(err, WalletError::CannotLockTokenSupply("Locked")); } @@ -3380,7 +3595,8 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn lock_then_transfer(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lock_then_transfer(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -3445,7 +3661,9 @@ fn lock_then_transfer(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet .add_unconfirmed_tx(lock_then_transfer_transaction.clone(), &WalletEventsNoOp) @@ -3516,7 +3734,8 @@ fn lock_then_transfer(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -3566,7 +3785,9 @@ fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet.add_unconfirmed_tx(transaction.clone(), &WalletEventsNoOp).unwrap(); @@ -3594,7 +3815,8 @@ fn wallet_multiple_transactions_in_single_block(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -3657,7 +3879,9 @@ fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet.add_unconfirmed_tx(transaction.clone(), &WalletEventsNoOp).unwrap(); @@ -3692,7 +3916,9 @@ fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet.add_unconfirmed_tx(transaction.clone(), &WalletEventsNoOp).unwrap(); @@ -3731,7 +3957,9 @@ fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap_err(); assert_eq!( err, @@ -3757,7 +3985,9 @@ fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet.add_unconfirmed_tx(transaction.clone(), &WalletEventsNoOp).unwrap(); @@ -3780,7 +4010,8 @@ fn wallet_scan_multiple_transactions_from_mempool(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn wallet_abandone_transactions(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn wallet_abandone_transactions(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -3841,7 +4072,9 @@ fn wallet_abandone_transactions(#[case] seed: Seed) { BTreeMap::new(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); wallet .add_account_unconfirmed_tx( @@ -3986,7 +4219,8 @@ fn wallet_set_lookahead_size(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn decommission_pool_wrong_account(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn decommission_pool_wrong_account(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4027,6 +4261,7 @@ fn decommission_pool_wrong_account(#[case] seed: Seed) { decommission_key: decommission_key.into_object(), }, ) + .await .unwrap(); let _ = create_block( &chain_config, @@ -4041,13 +4276,15 @@ fn decommission_pool_wrong_account(#[case] seed: Seed) { // Try to decommission the pool with default account let pool_id = pool_ids.first().unwrap().0; - let decommission_cmd_res = wallet.decommission_stake_pool( - acc_0_index, - pool_id, - pool_amount, - None, - FeeRate::from_amount_per_kb(Amount::from_atoms(0)), - ); + let decommission_cmd_res = wallet + .decommission_stake_pool( + acc_0_index, + pool_id, + pool_amount, + None, + FeeRate::from_amount_per_kb(Amount::from_atoms(0)), + ) + .await; assert_eq!( decommission_cmd_res.unwrap_err(), WalletError::PartiallySignedTransactionInDecommissionCommand @@ -4062,6 +4299,7 @@ fn decommission_pool_wrong_account(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) + .await .unwrap(); let _ = create_block( @@ -4079,7 +4317,8 @@ fn decommission_pool_wrong_account(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn decommission_pool_request_wrong_account(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn decommission_pool_request_wrong_account(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); @@ -4120,6 +4359,7 @@ fn decommission_pool_request_wrong_account(#[case] seed: Seed) { decommission_key: decommission_key.into_object(), }, ) + .await .unwrap(); let _ = create_block( &chain_config, @@ -4134,13 +4374,15 @@ fn decommission_pool_request_wrong_account(#[case] seed: Seed) { // Try to create decommission request from account that holds the key let pool_id = pool_ids.first().unwrap().0; - let decommission_req_res = wallet.decommission_stake_pool_request( - acc_1_index, - pool_id, - pool_amount, - None, - FeeRate::from_amount_per_kb(Amount::from_atoms(0)), - ); + let decommission_req_res = wallet + .decommission_stake_pool_request( + acc_1_index, + pool_id, + pool_amount, + None, + FeeRate::from_amount_per_kb(Amount::from_atoms(0)), + ) + .await; assert_eq!( decommission_req_res.unwrap_err(), WalletError::FullySignedTransactionInDecommissionReq @@ -4154,18 +4396,20 @@ fn decommission_pool_request_wrong_account(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) + .await .unwrap(); assert!(!decommission_partial_tx.all_signatures_available()); matches!( decommission_partial_tx.into_signed_tx().unwrap_err(), - TransactionCreationError::FailedToConvertPartiallySignedTx(_) + PartiallySignedTransactionCreationError::FailedToConvertPartiallySignedTx(_) ); } #[rstest] #[trace] #[case(Seed::from_entropy())] -fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4179,7 +4423,8 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { // Generate a new block which sends reward to the wallet let block1_amount = Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)); - let _ = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let (addr, _) = create_block(&chain_config, &mut wallet, vec![], block1_amount, 0); + let utxo = make_address_output(addr.clone(), block1_amount); let pool_ids = wallet.get_pool_ids(acc_0_index, WalletPoolsFilter::All).unwrap(); assert!(pool_ids.is_empty()); @@ -4206,12 +4451,25 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { decommission_key: decommission_key.into_object(), }, ) + .await .unwrap(); // remove the signatures and try to sign it again let tx = stake_pool_transaction.transaction().clone(); + let inps = tx.inputs().len(); + let outs = tx.outputs().len(); + let ptx = PartiallySignedTransaction::new( + tx, + vec![None; inps], + vec![Some(UtxoWithAdditionalInfo::new(utxo, None))], + vec![Some(addr.into_object())], + None, + vec![None; outs], + ) + .unwrap(); let stake_pool_transaction = wallet - .sign_raw_transaction(acc_0_index, TransactionToSign::Tx(tx)) + .sign_raw_transaction(acc_0_index, ptx) + .await .unwrap() .0 .into_signed_tx() @@ -4239,24 +4497,21 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) + .await .unwrap(); // Try to sign decommission request with wrong account let sign_from_acc0_res = wallet - .sign_raw_transaction( - acc_0_index, - TransactionToSign::Partial(decommission_partial_tx.clone()), - ) + .sign_raw_transaction(acc_0_index, decommission_partial_tx.clone()) + .await .unwrap() .0; // the tx is still not fully signed assert!(!sign_from_acc0_res.all_signatures_available()); let signed_tx = wallet - .sign_raw_transaction( - acc_1_index, - TransactionToSign::Partial(decommission_partial_tx), - ) + .sign_raw_transaction(acc_1_index, decommission_partial_tx) + .await .unwrap() .0 .into_signed_tx() @@ -4272,7 +4527,8 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4314,6 +4570,7 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { decommission_key: decommission_key.into_object(), }, ) + .await .unwrap(); let _ = create_block( &chain_config, @@ -4335,14 +4592,13 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::from_atoms(0)), ) + .await .unwrap(); // sign the tx with cold wallet let partially_signed_transaction = cold_wallet - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(decommission_partial_tx), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, decommission_partial_tx) + .await .unwrap() .0; assert!(partially_signed_transaction.all_signatures_available()); @@ -4350,10 +4606,8 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // sign it with the hot wallet should leave the signatures in place even if it can't find the // destinations for the inputs let partially_signed_transaction = hot_wallet - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(partially_signed_transaction), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, partially_signed_transaction) + .await .unwrap() .0; assert!(partially_signed_transaction.all_signatures_available()); @@ -4375,7 +4629,8 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn filter_pools(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn filter_pools(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4415,6 +4670,7 @@ fn filter_pools(#[case] seed: Seed) { decommission_key: decommission_key.into_object(), }, ) + .await .unwrap(); // sync for wallet1 let _ = create_block( @@ -4461,7 +4717,8 @@ fn filter_pools(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn sign_send_request_cold_wallet(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn sign_send_request_cold_wallet(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4508,15 +4765,14 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { [(Currency::Coin, cold_wallet_address.clone())].into(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &BTreeMap::new(), ) .unwrap(); // Try to sign request with the hot wallet let tx = hot_wallet - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(send_req.clone()), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req.clone()) + .await .unwrap() .0; // the tx is not fully signed @@ -4524,7 +4780,8 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { // sign the tx with cold wallet let signed_tx = cold_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(send_req)) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, send_req) + .await .unwrap() .0 .into_signed_tx() @@ -4564,7 +4821,7 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { .unwrap(); assert_eq!(utxos.len(), 1); - let (_, output, _) = utxos.pop().unwrap(); + let (_, output) = utxos.pop().unwrap(); matches!(output, TxOutput::Transfer(OutputValue::Coin(value), dest) if value == balance && dest == cold_wallet_address.into_object()); @@ -4573,7 +4830,8 @@ fn sign_send_request_cold_wallet(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn test_not_exhaustion_of_keys(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_not_exhaustion_of_keys(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4613,7 +4871,9 @@ fn test_not_exhaustion_of_keys(#[case] seed: Seed) { [].into(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + BTreeMap::new(), ) + .await .unwrap(); } } @@ -4674,7 +4934,8 @@ fn test_add_standalone_private_key(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn test_add_standalone_multisig(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_add_standalone_multisig(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_regtest()); @@ -4746,13 +5007,21 @@ fn test_add_standalone_multisig(#[case] seed: Seed) { )], ) .unwrap(); + let outs = tx.outputs().len(); + let spend_multisig_tx = PartiallySignedTransaction::new( + spend_multisig_tx, + vec![None; 1], + vec![Some(UtxoWithAdditionalInfo::new(tx.outputs()[0].clone(), None))], + vec![Some(multisig_address.as_object().clone())], + None, + vec![None; outs], + ) + .unwrap(); // sign it with wallet1 let (ptx, _, statuses) = wallet1 - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Tx(spend_multisig_tx), - ) + .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_multisig_tx) + .await .unwrap(); // check it is still not fully signed @@ -4760,27 +5029,22 @@ fn test_add_standalone_multisig(#[case] seed: Seed) { assert!(!statuses.iter().all(|s| *s == SignatureStatus::FullySigned)); // try to sign it with wallet1 again - let (ptx, _, statuses) = wallet1 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(ptx)) - .unwrap(); + let (ptx, _, statuses) = + wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).await.unwrap(); // check it is still not fully signed assert!(ptx.all_signatures_available()); assert!(!statuses.iter().all(|s| *s == SignatureStatus::FullySigned)); // try to sign it with wallet2 but wallet2 does not have the multisig added as standalone - let ptx = wallet2 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(ptx)) - .unwrap() - .0; + let ptx = wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).await.unwrap().0; // add it to wallet2 as well wallet2.add_standalone_multisig(DEFAULT_ACCOUNT_INDEX, challenge, None).unwrap(); // now we can sign it - let (ptx, _, statuses) = wallet2 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(ptx)) - .unwrap(); + let (ptx, _, statuses) = + wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, ptx).await.unwrap(); // now it is fully signed assert!(ptx.all_signatures_available()); @@ -4790,7 +5054,8 @@ fn test_add_standalone_multisig(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_htlc_and_spend(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_htlc_and_spend(#[case] seed: Seed) { use common::chain::htlc::HtlcSecret; let mut rng = make_seedable_rng(seed); @@ -4848,7 +5113,9 @@ fn create_htlc_and_spend(#[case] seed: Seed) { htlc.clone(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &BTreeMap::new(), ) + .await .unwrap(); let create_htlc_tx_id = create_htlc_tx.transaction().get_id(); let (_, block2) = create_block( @@ -4883,7 +5150,7 @@ fn create_htlc_and_spend(#[case] seed: Seed) { ) .unwrap(); assert_eq!(wallet2_utxos.len(), 1); - let (_, output, _) = wallet2_utxos.pop().unwrap(); + let (_, output) = wallet2_utxos.pop().unwrap(); match output { TxOutput::Htlc(actual_output_value, actual_htlc) => { assert_eq!(actual_output_value, output_value); @@ -4899,19 +5166,25 @@ fn create_htlc_and_spend(#[case] seed: Seed) { vec![TxOutput::Transfer(output_value, address2.into_object())], ) .unwrap(); - let spend_utxos = vec![create_htlc_tx.transaction().outputs().first().cloned()]; + let spend_utxos = vec![create_htlc_tx + .transaction() + .outputs() + .first() + .cloned() + .map(|out| UtxoWithAdditionalInfo::new(out, None))]; + let outs = create_htlc_tx.outputs().len(); let spend_ptx = PartiallySignedTransaction::new( spend_tx, vec![None], spend_utxos, vec![Some(spend_key.into_object())], Some(vec![Some(secret)]), + vec![None; outs], ) .unwrap(); - let (spend_ptx, _, new_statuses) = wallet2 - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, TransactionToSign::Partial(spend_ptx)) - .unwrap(); + let (spend_ptx, _, new_statuses) = + wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, spend_ptx).await.unwrap(); assert_eq!(vec![SignatureStatus::FullySigned], new_statuses); let spend_tx = spend_ptx.into_signed_tx().unwrap(); @@ -4927,7 +5200,8 @@ fn create_htlc_and_spend(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_htlc_and_refund(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_htlc_and_refund(#[case] seed: Seed) { use common::chain::htlc::HtlcSecret; let mut rng = make_seedable_rng(seed); @@ -4987,7 +5261,9 @@ fn create_htlc_and_refund(#[case] seed: Seed) { htlc.clone(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &BTreeMap::new(), ) + .await .unwrap(); let create_htlc_tx_id = create_htlc_tx.transaction().get_id(); @@ -4997,13 +5273,20 @@ fn create_htlc_and_refund(#[case] seed: Seed) { vec![TxOutput::Transfer(output_value, address1.into_object())], ) .unwrap(); - let refund_utxos = vec![create_htlc_tx.transaction().outputs().first().cloned()]; + let refund_utxos = vec![create_htlc_tx + .transaction() + .outputs() + .first() + .cloned() + .map(|out| UtxoWithAdditionalInfo::new(out, None))]; + let outs = create_htlc_tx.outputs().len(); let refund_ptx = PartiallySignedTransaction::new( refund_tx, vec![None], refund_utxos, vec![Some(refund_key)], None, + vec![None; outs], ) .unwrap(); @@ -5039,12 +5322,8 @@ fn create_htlc_and_refund(#[case] seed: Seed) { .unwrap(); assert_eq!(wallet2_multisig_utxos.len(), 1); - let (refund_ptx, prev_statuses, new_statuses) = wallet2 - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(refund_ptx), - ) - .unwrap(); + let (refund_ptx, prev_statuses, new_statuses) = + wallet2.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, refund_ptx).await.unwrap(); assert_eq!(vec![SignatureStatus::NotSigned], prev_statuses); assert_eq!( @@ -5055,12 +5334,8 @@ fn create_htlc_and_refund(#[case] seed: Seed) { new_statuses ); - let (refund_ptx, prev_statuses, new_statuses) = wallet1 - .sign_raw_transaction( - DEFAULT_ACCOUNT_INDEX, - TransactionToSign::Partial(refund_ptx), - ) - .unwrap(); + let (refund_ptx, prev_statuses, new_statuses) = + wallet1.sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, refund_ptx).await.unwrap(); assert_eq!( vec![SignatureStatus::PartialMultisig { required_signatures: 2, @@ -5089,7 +5364,8 @@ fn create_htlc_and_refund(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_order(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_order(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); @@ -5120,6 +5396,7 @@ fn create_order(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -5145,8 +5422,9 @@ fn create_order(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let token_amount_to_mint = Amount::from_atoms(rng.gen_range(2..100)); let mint_transaction = wallet @@ -5158,6 +5436,7 @@ fn create_order(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let _ = create_block( @@ -5179,6 +5458,13 @@ fn create_order(#[case] seed: Seed) { // Create an order selling tokens for coins let ask_value = OutputValue::Coin(Amount::from_atoms(111)); let give_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let (_, create_order_tx) = wallet .create_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5187,7 +5473,9 @@ fn create_order(#[case] seed: Seed) { address2.clone(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let _ = create_block( @@ -5206,7 +5494,8 @@ fn create_order(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_order_and_conclude(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_order_and_conclude(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); @@ -5237,6 +5526,7 @@ fn create_order_and_conclude(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -5262,8 +5552,9 @@ fn create_order_and_conclude(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let token_amount_to_mint = Amount::from_atoms(rng.gen_range(2..100)); let mint_transaction = wallet @@ -5275,6 +5566,7 @@ fn create_order_and_conclude(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let _ = create_block( @@ -5296,6 +5588,13 @@ fn create_order_and_conclude(#[case] seed: Seed) { // Create an order selling tokens for coins let ask_value = OutputValue::Coin(Amount::from_atoms(111)); let give_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let (order_id, create_order_tx) = wallet .create_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5304,7 +5603,9 @@ fn create_order_and_conclude(#[case] seed: Seed) { address2.clone(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let order_info = RpcOrderInfo { conclude_key: address2.clone().into_object(), @@ -5332,6 +5633,13 @@ fn create_order_and_conclude(#[case] seed: Seed) { assert_eq!(coin_balance, expected_balance); assert!(token_balances.is_empty()); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let conclude_order_tx = wallet .create_conclude_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5340,7 +5648,9 @@ fn create_order_and_conclude(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let _ = create_block( @@ -5362,7 +5672,8 @@ fn create_order_and_conclude(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_order_fill_completely_conclude(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_order_fill_completely_conclude(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); @@ -5395,6 +5706,7 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -5423,8 +5735,9 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet1.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet1 + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let token_amount_to_mint = Amount::from_atoms(100); let mint_transaction = wallet1 @@ -5436,6 +5749,7 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let (_, block3) = create_block( @@ -5468,6 +5782,13 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { let ask_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); let sell_amount = Amount::from_atoms(1000); let give_value = OutputValue::Coin(sell_amount); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let (order_id, create_order_tx) = wallet1 .create_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5476,7 +5797,9 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { address1.clone(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let order_info = RpcOrderInfo { conclude_key: address1.clone().into_object(), @@ -5518,6 +5841,13 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { Some(&(issued_token_id, token_amount_to_mint)) ); } + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); // Fill order partially let fill_order_tx_1 = wallet2 @@ -5529,7 +5859,9 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let (_, block5) = create_block( @@ -5576,6 +5908,13 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { nonce: Some(AccountNonce::new(0)), }; + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let fill_order_tx_2 = wallet2 .create_fill_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5585,7 +5924,9 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let (_, block6) = create_block( @@ -5625,6 +5966,13 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { ask_balance: Amount::ZERO, nonce: Some(AccountNonce::new(1)), }; + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let conclude_order_tx = wallet1 .create_conclude_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5633,7 +5981,9 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let (_, block7) = create_block( @@ -5666,7 +6016,8 @@ fn create_order_fill_completely_conclude(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn create_order_fill_partially_conclude(#[case] seed: Seed) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn create_order_fill_partially_conclude(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_unit_test_config()); @@ -5699,6 +6050,7 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let block2_amount = chain_config.token_supply_change_fee(BlockHeight::zero()); @@ -5727,8 +6079,9 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { token_issuance.authority, ); - let unconfirmed_token_info = - wallet1.get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, &token_info).unwrap(); + let unconfirmed_token_info = wallet1 + .get_token_unconfirmed_info(DEFAULT_ACCOUNT_INDEX, token_info.clone()) + .unwrap(); let token_amount_to_mint = Amount::from_atoms(100); let mint_transaction = wallet1 @@ -5740,6 +6093,7 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), ) + .await .unwrap(); let (_, block3) = create_block( @@ -5772,6 +6126,13 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { let ask_value = OutputValue::TokenV1(issued_token_id, token_amount_to_mint); let sell_amount = Amount::from_atoms(1000); let give_value = OutputValue::Coin(sell_amount); + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let (order_id, create_order_tx) = wallet1 .create_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5780,7 +6141,9 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { address1.clone(), FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let order_info = RpcOrderInfo { conclude_key: address1.clone().into_object(), @@ -5823,6 +6186,13 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { ); } + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); // Fill order partially let fill_order_tx_1 = wallet2 .create_fill_order_tx( @@ -5833,7 +6203,9 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let (_, block5) = create_block( @@ -5880,6 +6252,13 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { nonce: Some(AccountNonce::new(0)), }; + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(issued_token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: unconfirmed_token_info.num_decimals(), + ticker: unconfirmed_token_info.token_ticker().to_vec(), + }), + )]); let conclude_order_tx = wallet1 .create_conclude_order_tx( DEFAULT_ACCOUNT_INDEX, @@ -5888,7 +6267,9 @@ fn create_order_fill_partially_conclude(#[case] seed: Seed) { None, FeeRate::from_amount_per_kb(Amount::ZERO), FeeRate::from_amount_per_kb(Amount::ZERO), + &additional_info, ) + .await .unwrap(); let (_, block6) = create_block( diff --git a/wallet/storage/src/internal/mod.rs b/wallet/storage/src/internal/mod.rs index 8b6948827a..67387ee14e 100644 --- a/wallet/storage/src/internal/mod.rs +++ b/wallet/storage/src/internal/mod.rs @@ -24,7 +24,9 @@ mod password; use password::{challenge_to_sym_key, password_to_sym_key}; mod store_tx; -pub use store_tx::{StoreTxRo, StoreTxRoUnlocked, StoreTxRw, StoreTxRwUnlocked}; +pub use store_tx::{ + StoreLocalReadWriteUnlocked, StoreTxRo, StoreTxRoUnlocked, StoreTxRw, StoreTxRwUnlocked, +}; use self::store_tx::EncryptionState; @@ -151,6 +153,10 @@ impl Store { pub fn dump_raw(&self) -> crate::Result> { self.storage.transaction_ro()?.dump_raw().map_err(crate::Error::from) } + + pub fn local_rw_unlocked(&self) -> StoreLocalReadWriteUnlocked { + StoreLocalReadWriteUnlocked::new(self.clone()) + } } impl Clone for Store diff --git a/wallet/storage/src/internal/store_tx.rs b/wallet/storage/src/internal/store_tx.rs index 5cac709390..bf8dacf20f 100644 --- a/wallet/storage/src/internal/store_tx.rs +++ b/wallet/storage/src/internal/store_tx.rs @@ -17,8 +17,9 @@ use std::collections::BTreeMap; use crate::{ schema::{self as db, Schema}, - WalletStorageEncryptionRead, WalletStorageEncryptionWrite, WalletStorageReadLocked, - WalletStorageReadUnlocked, WalletStorageWriteLocked, WalletStorageWriteUnlocked, + Transactional, WalletStorageEncryptionRead, WalletStorageEncryptionWrite, + WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, + WalletStorageWriteUnlocked, }; use common::{ address::Address, @@ -30,7 +31,7 @@ use crypto::{ symkey::SymmetricKey, }; use serialization::{Codec, DecodeAll, Encode, EncodeLike}; -use storage::{schema, MakeMapRef}; +use storage::{schema, Backend, MakeMapRef}; use utils::{ ensure, maybe_encrypted::{MaybeEncrypted, MaybeEncryptedError}, @@ -47,6 +48,8 @@ use wallet_types::{ AccountDerivationPathId, AccountId, AccountInfo, AccountKeyPurposeId, AccountWalletCreatedTxId, AccountWalletTxId, KeychainUsageState, WalletTx, }; + +use super::Store; mod well_known { use common::chain::block::timestamp::BlockTimestamp; use crypto::kdf::KdfChallenge; @@ -153,6 +156,565 @@ impl<'st, B: storage::Backend> StoreTxRwUnlocked<'st, B> { } } +type TxOperation = dyn FnOnce(&mut StoreTxRw<'_, B>) -> crate::Result<()> + 'static + Send; + +/// A local read/write object, stores each write operation and performs them only at the end +/// Avoids references to avoid lifetime issues in async functions +pub struct StoreLocalReadWriteUnlocked { + operations: Vec>>, + local_read: Store, +} + +/// A wrapper around the store itself that opens a new read only transaction on each read operation +/// Can be used in async contexts +pub struct StoreLocalReadOnlyUnlocked { + local_read: Store, +} + +impl StoreLocalReadWriteUnlocked { + pub fn new(local_read: Store) -> Self { + Self { + operations: vec![], + local_read, + } + } + + pub fn add_operation(&mut self, op: Box>) { + self.operations.push(op); + } + + /// perform the local operations + pub fn perform_operations(self, dbtx: &mut StoreTxRw<'_, B>) -> crate::Result<()> { + for op in self.operations { + op(dbtx)?; + } + + Ok(()) + } + + pub fn read_only_store(&self) -> StoreLocalReadOnlyUnlocked { + StoreLocalReadOnlyUnlocked { + local_read: self.local_read.clone(), + } + } + + pub fn transaction_ro_unlocked(&self) -> crate::Result> { + self.local_read.transaction_ro_unlocked() + } +} + +impl WalletStorageReadLocked for StoreLocalReadWriteUnlocked { + fn get_storage_version(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_storage_version() + } + + fn get_wallet_type(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_wallet_type() + } + + fn get_chain_info(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_chain_info() + } + + fn get_transaction(&self, id: &AccountWalletTxId) -> crate::Result> { + self.local_read.transaction_ro()?.get_transaction(id) + } + + fn get_accounts_info(&self) -> crate::Result> { + self.local_read.transaction_ro()?.get_accounts_info() + } + + fn get_address(&self, id: &AccountDerivationPathId) -> crate::Result> { + self.local_read.transaction_ro()?.get_address(id) + } + + fn get_addresses( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_addresses(account_id) + } + + fn check_root_keys_sanity(&self) -> crate::Result<()> { + self.local_read.transaction_ro()?.check_root_keys_sanity() + } + + /// Collect and return all transactions from the storage + fn get_transactions( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_transactions(account_id) + } + + /// Collect and return all signed transactions from the storage + fn get_user_transactions(&self) -> crate::Result> { + self.local_read.transaction_ro()?.get_user_transactions() + } + + fn get_account_unconfirmed_tx_counter( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_account_unconfirmed_tx_counter(account_id) + } + + fn get_account_vrf_public_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_account_vrf_public_keys(account_id) + } + + fn get_account_standalone_watch_only_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read + .transaction_ro()? + .get_account_standalone_watch_only_keys(account_id) + } + fn get_account_standalone_multisig_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read + .transaction_ro()? + .get_account_standalone_multisig_keys(account_id) + } + + fn get_account_standalone_private_keys( + &self, + account_id: &AccountId, + ) -> crate::Result)>> { + self.local_read + .transaction_ro()? + .get_account_standalone_private_keys(account_id) + } + + fn get_keychain_usage_state( + &self, + id: &AccountKeyPurposeId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_keychain_usage_state(id) + } + + fn get_vrf_keychain_usage_state( + &self, + id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_vrf_keychain_usage_state(id) + } + + fn get_keychain_usage_states( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_keychain_usage_states(account_id) + } + + fn get_public_key( + &self, + id: &AccountDerivationPathId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_public_key(id) + } + + fn get_public_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_public_keys(account_id) + } + + fn get_median_time(&self) -> crate::Result> { + self.local_read.transaction_ro()?.get_median_time() + } + + fn get_lookahead_size(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_lookahead_size() + } +} + +impl WalletStorageReadUnlocked for StoreLocalReadWriteUnlocked { + fn get_root_key(&self) -> crate::Result> { + self.local_read.transaction_ro_unlocked()?.get_root_key() + } + fn get_seed_phrase(&self) -> crate::Result> { + self.local_read.transaction_ro_unlocked()?.get_seed_phrase() + } + + fn get_account_standalone_private_key( + &self, + account_pubkey: &AccountPublicKey, + ) -> crate::Result> { + self.local_read + .transaction_ro_unlocked()? + .get_account_standalone_private_key(account_pubkey) + } +} + +impl WalletStorageReadLocked for StoreLocalReadOnlyUnlocked { + fn get_storage_version(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_storage_version() + } + + fn get_wallet_type(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_wallet_type() + } + + fn get_chain_info(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_chain_info() + } + + fn get_transaction(&self, id: &AccountWalletTxId) -> crate::Result> { + self.local_read.transaction_ro()?.get_transaction(id) + } + + fn get_accounts_info(&self) -> crate::Result> { + self.local_read.transaction_ro()?.get_accounts_info() + } + + fn get_address(&self, id: &AccountDerivationPathId) -> crate::Result> { + self.local_read.transaction_ro()?.get_address(id) + } + + fn get_addresses( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_addresses(account_id) + } + + fn check_root_keys_sanity(&self) -> crate::Result<()> { + self.local_read.transaction_ro()?.check_root_keys_sanity() + } + + /// Collect and return all transactions from the storage + fn get_transactions( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_transactions(account_id) + } + + /// Collect and return all signed transactions from the storage + fn get_user_transactions(&self) -> crate::Result> { + self.local_read.transaction_ro()?.get_user_transactions() + } + + fn get_account_unconfirmed_tx_counter( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_account_unconfirmed_tx_counter(account_id) + } + + fn get_account_vrf_public_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_account_vrf_public_keys(account_id) + } + + fn get_account_standalone_watch_only_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read + .transaction_ro()? + .get_account_standalone_watch_only_keys(account_id) + } + fn get_account_standalone_multisig_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read + .transaction_ro()? + .get_account_standalone_multisig_keys(account_id) + } + + fn get_account_standalone_private_keys( + &self, + account_id: &AccountId, + ) -> crate::Result)>> { + self.local_read + .transaction_ro()? + .get_account_standalone_private_keys(account_id) + } + + fn get_keychain_usage_state( + &self, + id: &AccountKeyPurposeId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_keychain_usage_state(id) + } + + fn get_vrf_keychain_usage_state( + &self, + id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_vrf_keychain_usage_state(id) + } + + fn get_keychain_usage_states( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_keychain_usage_states(account_id) + } + + fn get_public_key( + &self, + id: &AccountDerivationPathId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_public_key(id) + } + + fn get_public_keys( + &self, + account_id: &AccountId, + ) -> crate::Result> { + self.local_read.transaction_ro()?.get_public_keys(account_id) + } + + fn get_median_time(&self) -> crate::Result> { + self.local_read.transaction_ro()?.get_median_time() + } + + fn get_lookahead_size(&self) -> crate::Result { + self.local_read.transaction_ro()?.get_lookahead_size() + } +} + +impl WalletStorageReadUnlocked for StoreLocalReadOnlyUnlocked { + fn get_root_key(&self) -> crate::Result> { + self.local_read.transaction_ro_unlocked()?.get_root_key() + } + fn get_seed_phrase(&self) -> crate::Result> { + self.local_read.transaction_ro_unlocked()?.get_seed_phrase() + } + + fn get_account_standalone_private_key( + &self, + account_pubkey: &AccountPublicKey, + ) -> crate::Result> { + self.local_read + .transaction_ro_unlocked()? + .get_account_standalone_private_key(account_pubkey) + } +} + +impl WalletStorageWriteLocked for StoreLocalReadWriteUnlocked { + fn set_storage_version(&mut self, version: u32) -> crate::Result<()> { + self.add_operation(Box::new(move |dbtx| dbtx.set_storage_version(version))); + Ok(()) + } + + fn set_wallet_type(&mut self, wallet_type: WalletType) -> crate::Result<()> { + self.add_operation(Box::new(move |dbtx| dbtx.set_wallet_type(wallet_type))); + Ok(()) + } + + fn set_chain_info(&mut self, chain_info: &ChainInfo) -> crate::Result<()> { + let chain_info = chain_info.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.set_chain_info(&chain_info))); + Ok(()) + } + + fn set_transaction(&mut self, id: &AccountWalletTxId, tx: &WalletTx) -> crate::Result<()> { + let id = id.clone(); + let tx = tx.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.set_transaction(&id, &tx))); + Ok(()) + } + + fn del_transaction(&mut self, id: &AccountWalletTxId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_transaction(&id))); + Ok(()) + } + + fn clear_transactions(&mut self) -> crate::Result<()> { + self.add_operation(Box::new(|dbtx| dbtx.clear_transactions())); + Ok(()) + } + + fn clear_public_keys(&mut self) -> crate::Result<()> { + self.add_operation(Box::new(|dbtx| dbtx.clear_public_keys())); + Ok(()) + } + + fn clear_addresses(&mut self) -> crate::Result<()> { + self.add_operation(Box::new(|dbtx| dbtx.clear_addresses())); + Ok(()) + } + + fn set_account_unconfirmed_tx_counter( + &mut self, + id: &AccountId, + counter: u64, + ) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| { + dbtx.set_account_unconfirmed_tx_counter(&id, counter) + })); + Ok(()) + } + + fn set_account_vrf_public_keys( + &mut self, + id: &AccountId, + account_vrf_keys: &AccountVrfKeys, + ) -> crate::Result<()> { + let id = id.clone(); + let account_vrf_keys = account_vrf_keys.clone(); + self.add_operation(Box::new(move |dbtx| { + dbtx.set_account_vrf_public_keys(&id, &account_vrf_keys) + })); + Ok(()) + } + + fn set_user_transaction( + &mut self, + id: &AccountWalletCreatedTxId, + tx: &SignedTransaction, + ) -> crate::Result<()> { + let id = id.clone(); + let tx = tx.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.set_user_transaction(&id, &tx))); + Ok(()) + } + + fn del_user_transaction(&mut self, id: &AccountWalletCreatedTxId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_user_transaction(&id))); + Ok(()) + } + + fn set_standalone_watch_only_key( + &mut self, + id: &AccountAddress, + key: &StandaloneWatchOnlyKey, + ) -> crate::Result<()> { + let id = id.clone(); + let key = key.clone(); + self.add_operation(Box::new(move |dbtx| { + dbtx.set_standalone_watch_only_key(&id, &key) + })); + Ok(()) + } + fn set_standalone_multisig_key( + &mut self, + id: &AccountAddress, + key: &StandaloneMultisig, + ) -> crate::Result<()> { + let id = id.clone(); + let key = key.clone(); + self.add_operation(Box::new(move |dbtx| { + dbtx.set_standalone_multisig_key(&id, &key) + })); + Ok(()) + } + + fn set_account(&mut self, id: &AccountId, tx: &AccountInfo) -> crate::Result<()> { + let id = id.clone(); + let tx = tx.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.set_account(&id, &tx))); + Ok(()) + } + + fn del_account(&mut self, id: &AccountId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_account(&id))); + Ok(()) + } + + fn set_address( + &mut self, + id: &AccountDerivationPathId, + address: &Address, + ) -> crate::Result<()> { + let id = id.clone(); + let address = address.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.set_address(&id, &address))); + Ok(()) + } + + fn del_address(&mut self, id: &AccountDerivationPathId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_address(&id))); + Ok(()) + } + + fn set_keychain_usage_state( + &mut self, + id: &AccountKeyPurposeId, + usage_state: &KeychainUsageState, + ) -> crate::Result<()> { + let id = id.clone(); + let usage_state = usage_state.clone(); + self.add_operation(Box::new(move |dbtx| { + dbtx.set_keychain_usage_state(&id, &usage_state) + })); + Ok(()) + } + + fn set_vrf_keychain_usage_state( + &mut self, + id: &AccountId, + usage_state: &KeychainUsageState, + ) -> crate::Result<()> { + let id = id.clone(); + let usage_state = usage_state.clone(); + self.add_operation(Box::new(move |dbtx| { + dbtx.set_vrf_keychain_usage_state(&id, &usage_state) + })); + Ok(()) + } + + fn del_keychain_usage_state(&mut self, id: &AccountKeyPurposeId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_keychain_usage_state(&id))); + Ok(()) + } + + fn del_vrf_keychain_usage_state(&mut self, id: &AccountId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_vrf_keychain_usage_state(&id))); + Ok(()) + } + + fn set_public_key( + &mut self, + id: &AccountDerivationPathId, + pub_key: &ExtendedPublicKey, + ) -> crate::Result<()> { + let id = id.clone(); + let pub_key = pub_key.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.set_public_key(&id, &pub_key))); + Ok(()) + } + + fn del_public_key(&mut self, id: &AccountDerivationPathId) -> crate::Result<()> { + let id = id.clone(); + self.add_operation(Box::new(move |dbtx| dbtx.del_public_key(&id))); + Ok(()) + } + + fn set_median_time(&mut self, median_time: BlockTimestamp) -> crate::Result<()> { + self.add_operation(Box::new(move |dbtx| dbtx.set_median_time(median_time))); + Ok(()) + } + + fn set_lookahead_size(&mut self, lookahead_size: u32) -> crate::Result<()> { + self.add_operation(Box::new(move |dbtx| { + dbtx.set_lookahead_size(lookahead_size) + })); + Ok(()) + } +} + macro_rules! impl_read_ops { ($TxType:ident) => { /// Wallet data storage transaction @@ -589,7 +1151,7 @@ macro_rules! impl_write_ops { self.write::(id, pub_key) } - fn det_public_key(&mut self, id: &AccountDerivationPathId) -> crate::Result<()> { + fn del_public_key(&mut self, id: &AccountDerivationPathId) -> crate::Result<()> { self.storage.get_mut::().del(id).map_err(Into::into) } @@ -755,3 +1317,4 @@ impl<'st, B: storage::Backend> crate::IsTransaction for StoreTxRo<'st, B> {} impl<'st, B: storage::Backend> crate::IsTransaction for StoreTxRw<'st, B> {} impl<'st, B: storage::Backend> crate::IsTransaction for StoreTxRoUnlocked<'st, B> {} impl<'st, B: storage::Backend> crate::IsTransaction for StoreTxRwUnlocked<'st, B> {} +impl crate::IsTransaction for StoreLocalReadWriteUnlocked {} diff --git a/wallet/storage/src/is_transaction_seal.rs b/wallet/storage/src/is_transaction_seal.rs index f2f03135b9..d037a2d6eb 100644 --- a/wallet/storage/src/is_transaction_seal.rs +++ b/wallet/storage/src/is_transaction_seal.rs @@ -20,3 +20,4 @@ impl<'st, B: storage::Backend> Seal for crate::internal::StoreTxRo<'st, B> {} impl<'st, B: storage::Backend> Seal for crate::internal::StoreTxRw<'st, B> {} impl<'st, B: storage::Backend> Seal for crate::internal::StoreTxRoUnlocked<'st, B> {} impl<'st, B: storage::Backend> Seal for crate::internal::StoreTxRwUnlocked<'st, B> {} +impl Seal for crate::internal::StoreLocalReadWriteUnlocked {} diff --git a/wallet/storage/src/lib.rs b/wallet/storage/src/lib.rs index 433d2638c8..e38a8919f5 100644 --- a/wallet/storage/src/lib.rs +++ b/wallet/storage/src/lib.rs @@ -28,7 +28,9 @@ use crypto::{ key::{extended::ExtendedPublicKey, PrivateKey}, symkey::SymmetricKey, }; -pub use internal::{Store, StoreTxRo, StoreTxRoUnlocked, StoreTxRw, StoreTxRwUnlocked}; +pub use internal::{ + Store, StoreLocalReadWriteUnlocked, StoreTxRo, StoreTxRoUnlocked, StoreTxRw, StoreTxRwUnlocked, +}; use std::collections::BTreeMap; use wallet_types::{ @@ -192,7 +194,7 @@ pub trait WalletStorageWriteLocked: WalletStorageReadLocked { id: &AccountDerivationPathId, content: &ExtendedPublicKey, ) -> Result<()>; - fn det_public_key(&mut self, id: &AccountDerivationPathId) -> Result<()>; + fn del_public_key(&mut self, id: &AccountDerivationPathId) -> Result<()>; fn set_median_time(&mut self, median_time: BlockTimestamp) -> Result<()>; fn set_lookahead_size(&mut self, lookahead_size: u32) -> Result<()>; fn clear_public_keys(&mut self) -> Result<()>; @@ -200,7 +202,7 @@ pub trait WalletStorageWriteLocked: WalletStorageReadLocked { } /// Modifying operations on persistent wallet data with access to encrypted data -pub trait WalletStorageWriteUnlocked: WalletStorageReadUnlocked + WalletStorageWriteLocked { +pub trait WalletStorageWriteUnlocked: WalletStorageWriteLocked + WalletStorageReadUnlocked { fn set_root_key(&mut self, content: &RootKeys) -> Result<()>; fn del_root_key(&mut self) -> Result<()>; fn set_seed_phrase(&mut self, seed_phrase: SerializableSeedPhrase) -> Result<()>; diff --git a/wallet/types/Cargo.toml b/wallet/types/Cargo.toml index ee16b1d4f1..90d5761755 100644 --- a/wallet/types/Cargo.toml +++ b/wallet/types/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] common = { path = "../../common/" } crypto = { path = "../../crypto/" } +tx-verifier = { path = "../../chainstate/tx-verifier" } rpc-description = { path = "../../rpc/description" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } @@ -28,3 +29,6 @@ zeroize.workspace = true test-utils = { path = "../../test-utils" } rstest.workspace = true + +[features] +trezor = [] diff --git a/wallet/types/src/lib.rs b/wallet/types/src/lib.rs index 00fed549c2..aecc3aa015 100644 --- a/wallet/types/src/lib.rs +++ b/wallet/types/src/lib.rs @@ -18,6 +18,7 @@ pub mod account_info; pub mod chain_info; pub mod currency; pub mod keys; +pub mod partially_signed_transaction; pub mod seed_phrase; pub mod signature_status; pub mod utxo_types; diff --git a/common/src/chain/transaction/partially_signed_transaction.rs b/wallet/types/src/partially_signed_transaction.rs similarity index 54% rename from common/src/chain/transaction/partially_signed_transaction.rs rename to wallet/types/src/partially_signed_transaction.rs index 4705aa83c5..44e7336f09 100644 --- a/common/src/chain/transaction/partially_signed_transaction.rs +++ b/wallet/types/src/partially_signed_transaction.rs @@ -13,34 +13,90 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{ - htlc::HtlcSecret, - signature::{inputsig::InputWitness, Signable, Transactable}, - Destination, Transaction, TxOutput, +use common::{ + chain::{ + htlc::HtlcSecret, + signature::{inputsig::InputWitness, Signable, Transactable}, + Destination, SignedTransaction, Transaction, TransactionCreationError, TxInput, TxOutput, + }, + primitives::Amount, }; -use crate::chain::{SignedTransaction, TransactionCreationError, TxInput}; use serialization::{Decode, Encode}; +use thiserror::Error; +use tx_verifier::input_check::signature_only_check::SignatureOnlyVerifiable; use utils::ensure; +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum PartiallySignedTransactionCreationError { + #[error("Failed to convert partially signed tx to signed")] + FailedToConvertPartiallySignedTx(PartiallySignedTransaction), + #[error("The number of output additional infos does not match the number of outputs")] + InvalidOutputAdditionalInfoCount, + #[error("Failed to create transaction: {0}")] + TxCreationError(#[from] TransactionCreationError), + #[error("The number of input utxos does not match the number of inputs")] + InvalidInputUtxosCount, + #[error("The number of destinations does not match the number of inputs")] + InvalidDestinationsCount, + #[error("The number of htlc secrets does not match the number of inputs")] + InvalidHtlcSecretsCount, +} + +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +pub struct TokenAdditionalInfo { + pub num_decimals: u8, + pub ticker: Vec, +} + +/// Additional info for UTXOs +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +pub enum UtxoAdditionalInfo { + TokenInfo(TokenAdditionalInfo), + PoolInfo { + staker_balance: Amount, + }, + CreateOrder { + ask: Option, + give: Option, + }, +} + +#[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] +pub struct UtxoWithAdditionalInfo { + pub utxo: TxOutput, + pub additional_info: Option, +} + +impl UtxoWithAdditionalInfo { + pub fn new(utxo: TxOutput, additional_info: Option) -> Self { + Self { + utxo, + additional_info, + } + } +} + #[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] pub struct PartiallySignedTransaction { tx: Transaction, witnesses: Vec>, - input_utxos: Vec>, + input_utxos: Vec>, destinations: Vec>, htlc_secrets: Vec>, + output_additional_infos: Vec>, } impl PartiallySignedTransaction { pub fn new( tx: Transaction, witnesses: Vec>, - input_utxos: Vec>, + input_utxos: Vec>, destinations: Vec>, htlc_secrets: Option>>, - ) -> Result { + output_additional_infos: Vec>, + ) -> Result { let htlc_secrets = htlc_secrets.unwrap_or_else(|| vec![None; tx.inputs().len()]); let this = Self { @@ -49,6 +105,7 @@ impl PartiallySignedTransaction { input_utxos, destinations, htlc_secrets, + output_additional_infos, }; this.ensure_consistency()?; @@ -56,7 +113,7 @@ impl PartiallySignedTransaction { Ok(this) } - pub fn ensure_consistency(&self) -> Result<(), TransactionCreationError> { + pub fn ensure_consistency(&self) -> Result<(), PartiallySignedTransactionCreationError> { ensure!( self.tx.inputs().len() == self.witnesses.len(), TransactionCreationError::InvalidWitnessCount @@ -64,17 +121,22 @@ impl PartiallySignedTransaction { ensure!( self.tx.inputs().len() == self.input_utxos.len(), - TransactionCreationError::InvalidInputUtxosCount, + PartiallySignedTransactionCreationError::InvalidInputUtxosCount, ); ensure!( self.tx.inputs().len() == self.destinations.len(), - TransactionCreationError::InvalidDestinationsCount + PartiallySignedTransactionCreationError::InvalidDestinationsCount ); ensure!( self.tx.inputs().len() == self.htlc_secrets.len(), - TransactionCreationError::InvalidHtlcSecretsCount + PartiallySignedTransactionCreationError::InvalidHtlcSecretsCount + ); + + ensure!( + self.tx.outputs().len() == self.output_additional_infos.len(), + PartiallySignedTransactionCreationError::InvalidOutputAdditionalInfoCount ); Ok(()) @@ -93,7 +155,7 @@ impl PartiallySignedTransaction { self.tx } - pub fn input_utxos(&self) -> &[Option] { + pub fn input_utxos(&self) -> &[Option] { self.input_utxos.as_ref() } @@ -113,6 +175,10 @@ impl PartiallySignedTransaction { self.tx.inputs().len() } + pub fn output_additional_infos(&self) -> &[Option] { + &self.output_additional_infos + } + pub fn all_signatures_available(&self) -> bool { self.witnesses .iter() @@ -127,14 +193,14 @@ impl PartiallySignedTransaction { }) } - pub fn into_signed_tx(self) -> Result { + pub fn into_signed_tx( + self, + ) -> Result { if self.all_signatures_available() { let witnesses = self.witnesses.into_iter().map(|w| w.expect("cannot fail")).collect(); Ok(SignedTransaction::new(self.tx, witnesses)?) } else { - Err(TransactionCreationError::FailedToConvertPartiallySignedTx( - self, - )) + Err(PartiallySignedTransactionCreationError::FailedToConvertPartiallySignedTx(self)) } } } @@ -162,3 +228,5 @@ impl Transactable for PartiallySignedTransaction { self.witnesses.clone() } } + +impl SignatureOnlyVerifiable for PartiallySignedTransaction {} diff --git a/wallet/types/src/seed_phrase.rs b/wallet/types/src/seed_phrase.rs index e4aa6a5f8b..b4a78eeff3 100644 --- a/wallet/types/src/seed_phrase.rs +++ b/wallet/types/src/seed_phrase.rs @@ -17,6 +17,7 @@ use serialization::{Decode, Encode}; pub const MNEMONIC_24_WORDS_ENTROPY_SIZE: usize = 32; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StoreSeedPhrase { Store, DoNotStore, diff --git a/wallet/types/src/wallet_type.rs b/wallet/types/src/wallet_type.rs index e8e00af62a..2b84a0a3b5 100644 --- a/wallet/types/src/wallet_type.rs +++ b/wallet/types/src/wallet_type.rs @@ -22,6 +22,51 @@ pub enum WalletType { Cold, #[codec(index = 1)] Hot, + #[cfg(feature = "trezor")] + #[codec(index = 2)] + Trezor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum WalletControllerMode { + Cold, + Hot, +} + +impl Display for WalletControllerMode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Hot => write!(f, "Hot"), + Self::Cold => write!(f, "Cold"), + } + } +} + +impl WalletType { + /// Check if current Wallet type is compatible to be opened as the other wallet type + pub fn is_compatible(&self, other: WalletControllerMode) -> bool { + match (*self, other) { + (Self::Hot, WalletControllerMode::Hot) | (Self::Cold, WalletControllerMode::Cold) => { + true + } + (Self::Hot, WalletControllerMode::Cold) | (Self::Cold, WalletControllerMode::Hot) => { + false + } + #[cfg(feature = "trezor")] + (Self::Trezor, WalletControllerMode::Hot) => true, + #[cfg(feature = "trezor")] + (Self::Trezor, WalletControllerMode::Cold) => false, + } + } +} + +impl From for WalletType { + fn from(value: WalletControllerMode) -> Self { + match value { + WalletControllerMode::Hot => WalletType::Hot, + WalletControllerMode::Cold => WalletType::Cold, + } + } } impl Display for WalletType { @@ -29,6 +74,8 @@ impl Display for WalletType { match self { Self::Hot => write!(f, "Hot"), Self::Cold => write!(f, "Cold"), + #[cfg(feature = "trezor")] + Self::Trezor => write!(f, "Trezor"), } } } diff --git a/wallet/wallet-cli-commands/Cargo.toml b/wallet/wallet-cli-commands/Cargo.toml index ee84204b80..0a0b4a1ff8 100644 --- a/wallet/wallet-cli-commands/Cargo.toml +++ b/wallet/wallet-cli-commands/Cargo.toml @@ -56,3 +56,6 @@ test-utils = { path = "../../test-utils" } wallet-test-node = { path = "../wallet-test-node" } rstest.workspace = true + +[features] +trezor = ["wallet-types/trezor", "wallet-rpc-lib/trezor"] diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 04ff40cecd..bdc45439ee 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -20,9 +20,8 @@ use std::{fmt::Write, str::FromStr}; use common::{ address::Address, chain::{ - config::checkpoints_data::print_block_heights_ids_as_checkpoints_data, - partially_signed_transaction::PartiallySignedTransaction, ChainConfig, Destination, - SignedTransaction, TxOutput, UtxoOutPoint, + config::checkpoints_data::print_block_heights_ids_as_checkpoints_data, ChainConfig, + Destination, SignedTransaction, TxOutput, UtxoOutPoint, }, primitives::{Idable as _, H256}, text_summary::TextSummary, @@ -34,7 +33,7 @@ use node_comm::node_traits::NodeInterface; use serialization::{hex::HexEncode, hex_encoded::HexEncoded}; use utils::qrcode::{QrCode, QrCodeError}; use wallet::version::get_version; -use wallet_controller::types::GenericTokenTransfer; +use wallet_controller::types::{GenericTokenTransfer, WalletTypeArgs}; use wallet_rpc_client::wallet_rpc_traits::{PartialOrSignedTx, WalletInterface}; use wallet_rpc_lib::types::{ Balances, ComposedTransaction, ControllerConfig, MnemonicInfo, NewTransaction, NftMetadata, @@ -42,6 +41,14 @@ use wallet_rpc_lib::types::{ RpcValidatedSignatures, TokenMetadata, }; +#[cfg(feature = "trezor")] +use crate::helper_types::CliHardwareWalletType; +#[cfg(feature = "trezor")] +use wallet_rpc_lib::types::HardwareWalletType; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, seed_phrase::StoreSeedPhrase, +}; + use crate::{ errors::WalletCliCommandError, helper_types::parse_generic_token_transfer, ManageableWalletCommand, WalletManagementCommand, @@ -145,17 +152,23 @@ where mnemonic, whether_to_store_seed_phrase, passphrase, + hardware_wallet, } => { - let newly_generated_mnemonic = self - .wallet() - .await? - .create_wallet( - wallet_path, - whether_to_store_seed_phrase.to_bool(), + let store_seed_phrase = + whether_to_store_seed_phrase.map_or(StoreSeedPhrase::DoNotStore, Into::into); + let wallet_args = hardware_wallet.map_or( + WalletTypeArgs::Software { mnemonic, passphrase, - ) - .await?; + store_seed_phrase, + }, + |t| match t { + #[cfg(feature = "trezor")] + CliHardwareWalletType::Trezor => WalletTypeArgs::Trezor, + }, + ); + let newly_generated_mnemonic = + self.wallet().await?.create_wallet(wallet_path, wallet_args).await?; self.wallet.update_wallet::().await; @@ -188,17 +201,56 @@ where }) } + WalletManagementCommand::RecoverWallet { + wallet_path, + mnemonic, + whether_to_store_seed_phrase, + passphrase, + hardware_wallet, + } => { + let hardware_wallet = hardware_wallet.map(|t| match t { + #[cfg(feature = "trezor")] + CliHardwareWalletType::Trezor => HardwareWalletType::Trezor, + }); + + self.wallet() + .await? + .recover_wallet( + wallet_path, + whether_to_store_seed_phrase.map_or(false, |x| x.to_bool()), + mnemonic, + passphrase, + hardware_wallet, + ) + .await?; + + self.wallet.update_wallet::().await; + + let msg = "Wallet recovered successfully"; + + Ok(ConsoleCommand::SetStatus { + status: self.repl_status().await?, + print_message: msg.to_owned(), + }) + } + WalletManagementCommand::OpenWallet { wallet_path, encryption_password, force_change_wallet_type, + hardware_wallet, } => { + let hardware_wallet = hardware_wallet.map(|t| match t { + #[cfg(feature = "trezor")] + CliHardwareWalletType::Trezor => HardwareWalletType::Trezor, + }); self.wallet() .await? .open_wallet( wallet_path, encryption_password, Some(force_change_wallet_type), + hardware_wallet, ) .await?; self.wallet.update_wallet::().await; diff --git a/wallet/wallet-cli-commands/src/helper_types.rs b/wallet/wallet-cli-commands/src/helper_types.rs index d2ab021667..57e9c669a8 100644 --- a/wallet/wallet-cli-commands/src/helper_types.rs +++ b/wallet/wallet-cli-commands/src/helper_types.rs @@ -25,6 +25,7 @@ use common::{ use wallet_controller::types::{GenericCurrencyTransfer, GenericTokenTransfer}; use wallet_rpc_lib::types::{NodeInterface, PoolInfo, TokenTotalSupply}; use wallet_types::{ + seed_phrase::StoreSeedPhrase, utxo_types::{UtxoState, UtxoType}, with_locked::WithLocked, }; @@ -142,6 +143,15 @@ impl CliStoreSeedPhrase { } } +impl From for StoreSeedPhrase { + fn from(value: CliStoreSeedPhrase) -> Self { + match value { + CliStoreSeedPhrase::StoreSeedPhrase => StoreSeedPhrase::Store, + CliStoreSeedPhrase::DoNotStoreSeedPhrase => StoreSeedPhrase::DoNotStore, + } + } +} + #[derive(Debug, Clone, Copy, ValueEnum)] pub enum EnableOrDisable { Enable, @@ -434,6 +444,12 @@ impl YesNo { } } +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum CliHardwareWalletType { + #[cfg(feature = "trezor")] + Trezor, +} + #[cfg(test)] mod tests { use rstest::rstest; diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 4db0b1dedf..a47d11ff53 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -19,6 +19,7 @@ mod helper_types; pub use command_handler::CommandHandler; pub use errors::WalletCliCommandError; +pub use helper_types::CliHardwareWalletType; use helper_types::YesNo; use rpc::description::{Described, Module}; use wallet_rpc_lib::{types::NodeInterface, ColdWalletRpcDescription, WalletRpcDescription}; @@ -54,7 +55,8 @@ pub enum WalletManagementCommand { /// Not storing the seed-phrase can be seen as a security measure /// to ensure sufficient secrecy in case that seed-phrase is reused /// elsewhere if this wallet is compromised. - whether_to_store_seed_phrase: CliStoreSeedPhrase, + #[arg(required_unless_present("hardware_wallet"))] + whether_to_store_seed_phrase: Option, /// Mnemonic phrase (12, 15, or 24 words as a single quoted argument). If not specified, a new mnemonic phrase is generated and printed. mnemonic: Option, @@ -62,6 +64,39 @@ pub enum WalletManagementCommand { /// Passphrase along the mnemonic #[arg(long = "passphrase")] passphrase: Option, + + /// Create a wallet using a connected hardware wallet. Only the public keys will be kept in + /// the software wallet. Cannot specify a mnemonic or passphrase here, input them on the + /// hardware wallet instead when initializing the device. + #[arg(long, conflicts_with_all(["mnemonic", "passphrase"]))] + hardware_wallet: Option, + }, + + #[clap(name = "wallet-recover")] + RecoverWallet { + /// File path of the wallet file + wallet_path: PathBuf, + + /// If 'store-seed-phrase', the seed-phrase will be stored in the wallet file. + /// Not storing the seed-phrase can be seen as a security measure + /// to ensure sufficient secrecy in case that seed-phrase is reused + /// elsewhere if this wallet is compromised. + #[arg(required_unless_present("hardware_wallet"))] + whether_to_store_seed_phrase: Option, + + /// Mnemonic phrase (12, 15, or 24 words as a single quoted argument). + #[arg(required_unless_present("hardware_wallet"))] + mnemonic: Option, + + /// Passphrase along the mnemonic + #[arg(long = "passphrase")] + passphrase: Option, + + /// Create a wallet using a connected hardware wallet. Only the public keys will be kept in + /// the software wallet. Cannot specify a mnemonic or passphrase here, input them on the + /// hardware wallet instead when initializing the device. + #[arg(long, conflicts_with_all(["passphrase"]))] + hardware_wallet: Option, }, #[clap(name = "wallet-open")] @@ -73,6 +108,10 @@ pub enum WalletManagementCommand { /// Force change the wallet type from hot to cold or from cold to hot #[arg(long)] force_change_wallet_type: bool, + + /// Open a wallet file related to a connected hardware wallet. + #[arg(long, conflicts_with_all(["force_change_wallet_type"]))] + hardware_wallet: Option, }, #[clap(name = "wallet-close")] diff --git a/wallet/wallet-cli-lib/Cargo.toml b/wallet/wallet-cli-lib/Cargo.toml index 84bec932f6..00043dc082 100644 --- a/wallet/wallet-cli-lib/Cargo.toml +++ b/wallet/wallet-cli-lib/Cargo.toml @@ -56,3 +56,6 @@ test-utils = { path = "../../test-utils" } wallet-test-node = { path = "../wallet-test-node" } rstest.workspace = true + +[features] +trezor = ["wallet/trezor", "wallet-cli-commands/trezor", "wallet-types/trezor", "wallet-rpc-lib/trezor", "wallet-rpc-client/trezor"] diff --git a/wallet/wallet-cli-lib/src/config.rs b/wallet/wallet-cli-lib/src/config.rs index 436d7844a9..87e5923e31 100644 --- a/wallet/wallet-cli-lib/src/config.rs +++ b/wallet/wallet-cli-lib/src/config.rs @@ -20,6 +20,7 @@ use common::chain::config::{regtest_options::ChainConfigOptions, ChainType}; use crypto::key::hdkd::u31::U31; use utils::clap_utils; use utils_networking::NetworkAddressWithPort; +use wallet_rpc_lib::cmdline::CliHardwareWalletType; #[derive(Subcommand, Clone, Debug)] pub enum Network { @@ -84,6 +85,10 @@ pub struct CliArgs { #[clap(long, requires("wallet_file"))] pub force_change_wallet_type: bool, + /// Specified if the wallet file is of a hardware wallet type e.g. Trezor + #[arg(long, requires("wallet_file"))] + pub hardware_wallet: Option, + /// DEPRECATED: use start_staking_for_account instead! /// Start staking for the DEFAULT account after starting the wallet #[clap(long, requires("wallet_file"))] diff --git a/wallet/wallet-cli-lib/src/lib.rs b/wallet/wallet-cli-lib/src/lib.rs index abac293473..385b0ad532 100644 --- a/wallet/wallet-cli-lib/src/lib.rs +++ b/wallet/wallet-cli-lib/src/lib.rs @@ -36,9 +36,10 @@ use node_comm::{make_cold_wallet_rpc_client, make_rpc_client, rpc_client::ColdWa use rpc::RpcAuthData; use tokio::sync::mpsc; use utils::{cookie::COOKIE_FILENAME, default_data_dir::default_data_dir_for_chain, ensure}; -use wallet_cli_commands::{ManageableWalletCommand, WalletCommand, WalletManagementCommand}; -use wallet_rpc_lib::types::NodeInterface; -use wallet_rpc_lib::{cmdline::make_wallet_config, config::WalletRpcConfig}; +use wallet_cli_commands::{ + CliHardwareWalletType, ManageableWalletCommand, WalletCommand, WalletManagementCommand, +}; +use wallet_rpc_lib::{cmdline::make_wallet_config, config::WalletRpcConfig, types::NodeInterface}; enum Mode { Interactive { @@ -284,6 +285,12 @@ fn setup_events_and_repl( wallet_path, encryption_password: args.wallet_password, force_change_wallet_type: args.force_change_wallet_type, + hardware_wallet: args.hardware_wallet.map(|hw| match hw { + #[cfg(feature = "trezor")] + wallet_rpc_lib::cmdline::CliHardwareWalletType::Trezor => { + CliHardwareWalletType::Trezor + } + }), }, ), res_tx, diff --git a/wallet/wallet-cli-lib/tests/cli_test_framework.rs b/wallet/wallet-cli-lib/tests/cli_test_framework.rs index 13af81d10a..f9eb8dffa2 100644 --- a/wallet/wallet-cli-lib/tests/cli_test_framework.rs +++ b/wallet/wallet-cli-lib/tests/cli_test_framework.rs @@ -98,6 +98,7 @@ impl CliTestFramework { wallet_file: None, wallet_password: None, force_change_wallet_type: false, + hardware_wallet: None, start_staking: false, start_staking_for_account: vec![], node_rpc_address: Some(rpc_address.into()), @@ -127,6 +128,7 @@ impl CliTestFramework { wallet_file: None, wallet_password: None, force_change_wallet_type: false, + hardware_wallet: None, start_staking: false, start_staking_for_account: vec![], node_rpc_address: Some(rpc_address.into()), @@ -198,10 +200,10 @@ impl CliTestFramework { .unwrap() .to_owned(); let cmd = format!( - "wallet-create \"{}\" store-seed-phrase \"{}\"", + "wallet-recover \"{}\" store-seed-phrase \"{}\"", file_name, MNEMONIC ); - assert_eq!(self.exec(&cmd), "New wallet created successfully"); + assert_eq!(self.exec(&cmd), "Wallet recovered successfully"); } #[allow(dead_code)] diff --git a/wallet/wallet-cli/Cargo.toml b/wallet/wallet-cli/Cargo.toml index 033027324e..bf68f34857 100644 --- a/wallet/wallet-cli/Cargo.toml +++ b/wallet/wallet-cli/Cargo.toml @@ -14,3 +14,6 @@ wallet-cli-lib = { path = "../wallet-cli-lib" } clap = { workspace = true, features = ["derive"] } tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } + +[features] +trezor = ["wallet-cli-lib/trezor"] diff --git a/wallet/wallet-controller/Cargo.toml b/wallet/wallet-controller/Cargo.toml index 4f1b001da1..66f3afcae9 100644 --- a/wallet/wallet-controller/Cargo.toml +++ b/wallet/wallet-controller/Cargo.toml @@ -20,6 +20,7 @@ node-comm = { path = "../wallet-node-client" } rpc-description = { path = "../../rpc/description" } randomness = { path = "../../randomness" } serialization = { path = "../../serialization" } +storage = { path = "../../storage" } utils = { path = "../../utils" } utils-networking = { path = "../../utils/networking" } wallet = { path = ".." } @@ -42,3 +43,6 @@ test-utils = { path = "../../test-utils" } anyhow.workspace = true rstest.workspace = true + +[features] +trezor = ["wallet-types/trezor"] diff --git a/wallet/wallet-controller/src/helpers.rs b/wallet/wallet-controller/src/helpers.rs new file mode 100644 index 0000000000..f0d3134b9c --- /dev/null +++ b/wallet/wallet-controller/src/helpers.rs @@ -0,0 +1,298 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Common code for read and synced controllers + +use std::collections::BTreeMap; + +use common::{ + address::RpcAddress, + chain::{ + output_value::OutputValue, + tokens::{RPCTokenInfo, TokenId}, + ChainConfig, Destination, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{amount::RpcAmountOut, Amount}, +}; +use futures::{ + stream::{FuturesOrdered, FuturesUnordered}, + TryStreamExt, +}; +use node_comm::node_traits::NodeInterface; +use wallet::{ + destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, + WalletError, +}; +use wallet_types::{ + partially_signed_transaction::{ + PartiallySignedTransaction, TokenAdditionalInfo, UtxoAdditionalInfo, UtxoWithAdditionalInfo, + }, + Currency, +}; + +use crate::{runtime_wallet::RuntimeWallet, types::Balances, ControllerError}; + +pub async fn fetch_token_info( + rpc_client: &T, + token_id: TokenId, +) -> Result> { + rpc_client + .get_token_info(token_id) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(ControllerError::WalletError(WalletError::UnknownTokenId( + token_id, + ))) +} + +pub async fn fetch_utxo( + rpc_client: &T, + input: &UtxoOutPoint, + wallet: &RuntimeWallet, +) -> Result> { + // search locally for the unspent utxo + if let Some(out) = match &wallet { + RuntimeWallet::Software(w) => w.find_unspent_utxo_with_destination(input), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_unspent_utxo_with_destination(input), + } { + return Ok(out.0); + } + + // check the chainstate + rpc_client + .get_utxo(input.clone()) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( + input.clone(), + ))) +} + +pub async fn fetch_utxo_with_destination( + rpc_client: &T, + input: &UtxoOutPoint, + wallet: &RuntimeWallet, +) -> Result<(TxOutput, Destination), ControllerError> { + // search locally for the unspent utxo + if let Some(out) = match &wallet { + RuntimeWallet::Software(w) => w.find_unspent_utxo_with_destination(input), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_unspent_utxo_with_destination(input), + } { + return Ok(out); + } + + // check the chainstate + let utxo = rpc_client + .get_utxo(input.clone()) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( + input.clone(), + )))?; + + let pool_id = pool_id_from_txo(&utxo); + let dest = if let Some(pool_id) = pool_id { + rpc_client + .get_pool_decommission_destination(pool_id) + .await + .map_err(ControllerError::NodeCallError)? + } else { + get_tx_output_destination(&utxo, &|_| None, HtlcSpendingCondition::Skip) + } + .ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( + input.clone(), + )))?; + + Ok((utxo, dest)) +} + +fn pool_id_from_txo(utxo: &TxOutput) -> Option { + match utxo { + TxOutput::CreateStakePool(pool_id, _) | TxOutput::ProduceBlockFromStake(_, pool_id) => { + Some(*pool_id) + } + TxOutput::Burn(_) + | TxOutput::Transfer(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::Htlc(_, _) + | TxOutput::CreateOrder(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::DelegateStaking(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DataDeposit(_) => None, + } +} + +async fn fetch_token_extra_info( + rpc_client: &T, + value: &OutputValue, +) -> Result, ControllerError> +where + T: NodeInterface, +{ + match value { + OutputValue::Coin(_) | OutputValue::TokenV0(_) => Ok(None), + OutputValue::TokenV1(token_id, _) => { + let info = fetch_token_info(rpc_client, *token_id).await?; + Ok(Some(TokenAdditionalInfo { + num_decimals: info.token_number_of_decimals(), + ticker: info.token_ticker().to_vec(), + })) + } + } +} + +pub async fn fetch_utxo_extra_info( + rpc_client: &T, + utxo: TxOutput, +) -> Result> +where + T: NodeInterface, +{ + match &utxo { + TxOutput::Burn(value) + | TxOutput::Transfer(value, _) + | TxOutput::LockThenTransfer(value, _, _) + | TxOutput::Htlc(value, _) => { + let additional_info = fetch_token_extra_info(rpc_client, value) + .await? + .map(UtxoAdditionalInfo::TokenInfo); + Ok(UtxoWithAdditionalInfo::new(utxo, additional_info)) + } + TxOutput::CreateOrder(order) => { + let ask = fetch_token_extra_info(rpc_client, order.ask()).await?; + let give = fetch_token_extra_info(rpc_client, order.ask()).await?; + let additional_info = Some(UtxoAdditionalInfo::CreateOrder { ask, give }); + Ok(UtxoWithAdditionalInfo::new(utxo, additional_info)) + } + TxOutput::ProduceBlockFromStake(_, pool_id) => { + let staker_balance = rpc_client + .get_staker_balance(*pool_id) + .await + .map_err(ControllerError::NodeCallError)? + .ok_or(WalletError::UnknownPoolId(*pool_id))?; + Ok(UtxoWithAdditionalInfo::new( + utxo, + Some(UtxoAdditionalInfo::PoolInfo { staker_balance }), + )) + } + TxOutput::IssueNft(_, _, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DataDeposit(_) => Ok(UtxoWithAdditionalInfo::new(utxo, None)), + } +} + +pub async fn into_balances( + rpc_client: &T, + chain_config: &ChainConfig, + mut balances: BTreeMap, +) -> Result> { + let coins = balances.remove(&Currency::Coin).unwrap_or(Amount::ZERO); + let coins = RpcAmountOut::from_amount_no_padding(coins, chain_config.coin_decimals()); + + let tasks: FuturesUnordered<_> = balances + .into_iter() + .map(|(currency, amount)| async move { + let token_id = match currency { + Currency::Coin => panic!("Removed just above"), + Currency::Token(token_id) => token_id, + }; + + fetch_token_info(rpc_client, token_id).await.map(|info| { + let decimals = info.token_number_of_decimals(); + let amount = RpcAmountOut::from_amount_no_padding(amount, decimals); + let token_id = RpcAddress::new(chain_config, token_id).expect("addressable"); + (token_id, amount) + }) + }) + .collect(); + + Ok(Balances::new(coins, tasks.try_collect().await?)) +} + +pub async fn tx_to_partially_signed_tx( + rpc_client: &T, + wallet: &RuntimeWallet, + tx: Transaction, +) -> Result> { + let tasks: FuturesOrdered<_> = tx + .inputs() + .iter() + .map(|inp| into_utxo_and_destination(rpc_client, wallet, inp)) + .collect(); + let (input_utxos, destinations) = tasks.try_collect::>().await?.into_iter().unzip(); + let num_inputs = tx.inputs().len(); + + let tasks: FuturesOrdered<_> = tx + .outputs() + .iter() + .map(|out| fetch_utxo_extra_info(rpc_client, out.clone())) + .collect(); + let output_additional_infos = tasks + .try_collect::>() + .await? + .into_iter() + .map(|x| x.additional_info) + .collect(); + + let ptx = PartiallySignedTransaction::new( + tx, + vec![None; num_inputs], + input_utxos, + destinations, + None, + output_additional_infos, + ) + .map_err(WalletError::PartiallySignedTransactionCreation)?; + Ok(ptx) +} + +async fn into_utxo_and_destination( + rpc_client: &T, + wallet: &RuntimeWallet, + tx_inp: &TxInput, +) -> Result<(Option, Option), ControllerError> { + Ok(match tx_inp { + TxInput::Utxo(outpoint) => { + let (utxo, dest) = fetch_utxo_with_destination(rpc_client, outpoint, wallet).await?; + let utxo_with_extra_info = fetch_utxo_extra_info(rpc_client, utxo).await?; + (Some(utxo_with_extra_info), Some(dest)) + } + TxInput::Account(acc_outpoint) => { + // find delegation destination + let dest = match &wallet { + RuntimeWallet::Software(w) => w.find_account_destination(acc_outpoint), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_account_destination(acc_outpoint), + }; + (None, dest) + } + TxInput::AccountCommand(_, cmd) => { + // find authority of the token + let dest = match &wallet { + RuntimeWallet::Software(w) => w.find_account_command_destination(cmd), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_account_command_destination(cmd), + }; + (None, dest) + } + }) +} diff --git a/wallet/wallet-controller/src/lib.rs b/wallet/wallet-controller/src/lib.rs index 1a365b82d1..3674aa21cb 100644 --- a/wallet/wallet-controller/src/lib.rs +++ b/wallet/wallet-controller/src/lib.rs @@ -15,8 +15,10 @@ //! Common code for wallet UI applications +mod helpers; pub mod mnemonic; pub mod read; +mod runtime_wallet; mod sync; pub mod synced_controller; pub mod types; @@ -28,12 +30,10 @@ use blockprod::BlockProductionError; use chainstate::tx_verifier::{ self, error::ScriptError, input_check::signature_only_check::SignatureOnlyVerifiable, }; -use futures::{ - never::Never, - stream::{FuturesOrdered, FuturesUnordered}, - TryStreamExt, -}; +use futures::{never::Never, stream::FuturesOrdered, TryStreamExt}; +use helpers::{fetch_token_info, fetch_utxo, fetch_utxo_extra_info, into_balances}; use node_comm::rpc_client::ColdWalletClient; +use runtime_wallet::RuntimeWallet; use std::{ collections::{BTreeMap, BTreeSet}, fs, @@ -45,25 +45,25 @@ use std::{ use types::{ Balances, GenericCurrencyTransferToTxOutputConversionError, InspectTransaction, SeedWithPassPhrase, SignatureStats, TransactionToInspect, ValidatedSignatures, WalletInfo, + WalletTypeArgsComputed, }; +use wallet_storage::DefaultBackend; use read::ReadOnlyController; use sync::InSync; use synced_controller::SyncedController; use common::{ - address::{AddressError, RpcAddress}, + address::AddressError, chain::{ block::timestamp::BlockTimestamp, htlc::HtlcSecret, - partially_signed_transaction::PartiallySignedTransaction, signature::{inputsig::InputWitness, DestinationSigError, Transactable}, tokens::{RPCTokenInfo, TokenId}, Block, ChainConfig, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{ - amount::RpcAmountOut, time::{get_time, Time}, Amount, BlockHeight, Id, Idable, }, @@ -78,23 +78,33 @@ pub use node_comm::{ rpc_client::NodeRpcClient, }; use randomness::{make_pseudo_rng, make_true_rng, Rng}; +#[cfg(feature = "trezor")] +use wallet::signer::trezor_signer::TrezorSignerProvider; +#[cfg(feature = "trezor")] +use wallet::signer::SignerError; + use wallet::{ account::{ currency_grouper::{self}, TransactionToSign, }, destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, + signer::software_signer::SoftwareSignerProvider, wallet::WalletPoolsFilter, wallet_events::WalletEvents, - DefaultWallet, WalletError, WalletResult, + WalletError, WalletResult, }; + pub use wallet_types::{ account_info::DEFAULT_ACCOUNT_INDEX, utxo_types::{UtxoState, UtxoStates, UtxoType, UtxoTypes}, }; use wallet_types::{ - seed_phrase::StoreSeedPhrase, signature_status::SignatureStatus, wallet_type::WalletType, - with_locked::WithLocked, Currency, + partially_signed_transaction::{PartiallySignedTransaction, UtxoWithAdditionalInfo}, + signature_status::SignatureStatus, + wallet_type::{WalletControllerMode, WalletType}, + with_locked::WithLocked, + Currency, }; #[derive(thiserror::Error, Debug)] @@ -155,33 +165,38 @@ pub struct ControllerConfig { pub broadcast_to_mempool: bool, } -pub struct Controller { +pub struct Controller { chain_config: Arc, rpc_client: T, - wallet: DefaultWallet, + wallet: RuntimeWallet, staking_started: BTreeSet, wallet_events: W, } -impl std::fmt::Debug for Controller { +impl std::fmt::Debug for Controller { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Controller").finish() } } -pub type RpcController = Controller; -pub type HandlesController = Controller; -pub type ColdController = Controller; +pub type RpcController = Controller; +pub type HandlesController = + Controller; +pub type ColdController = Controller; -impl Controller { +impl Controller +where + T: NodeInterface + Clone + Send + Sync + 'static, + W: WalletEvents, +{ pub async fn new( chain_config: Arc, rpc_client: T, - wallet: DefaultWallet, + wallet: RuntimeWallet, wallet_events: W, ) -> Result> { let mut controller = Self { @@ -201,7 +216,7 @@ impl Controll pub fn new_unsynced( chain_config: Arc, rpc_client: T, - wallet: DefaultWallet, + wallet: RuntimeWallet, wallet_events: W, ) -> Self { Self { @@ -216,12 +231,10 @@ impl Controll pub fn create_wallet( chain_config: Arc, file_path: impl AsRef, - mnemonic: mnemonic::Mnemonic, - passphrase: Option<&str>, - whether_to_store_seed_phrase: StoreSeedPhrase, + args: WalletTypeArgsComputed, best_block: (BlockHeight, Id), wallet_type: WalletType, - ) -> Result> { + ) -> Result, ControllerError> { utils::ensure!( !file_path.as_ref().exists(), ControllerError::WalletFileError( @@ -232,28 +245,53 @@ impl Controll let db = wallet::wallet::open_or_create_wallet_file(file_path) .map_err(ControllerError::WalletError)?; - let wallet = wallet::Wallet::create_new_wallet( - Arc::clone(&chain_config), - db, - &mnemonic.to_string(), - passphrase, - whether_to_store_seed_phrase, - best_block, - wallet_type, - ) - .map_err(ControllerError::WalletError)?; - - Ok(wallet) + match args { + WalletTypeArgsComputed::Software { + mnemonic, + passphrase, + store_seed_phrase, + } => { + let passphrase_ref = passphrase.as_ref().map(|x| x.as_ref()); + + let wallet = wallet::Wallet::create_new_wallet( + Arc::clone(&chain_config), + db, + best_block, + wallet_type, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + &mnemonic.to_string(), + passphrase_ref, + store_seed_phrase, + )?) + }, + ) + .map_err(ControllerError::WalletError)?; + Ok(RuntimeWallet::Software(wallet)) + } + #[cfg(feature = "trezor")] + WalletTypeArgsComputed::Trezor => { + let wallet = wallet::Wallet::create_new_wallet( + Arc::clone(&chain_config), + db, + best_block, + wallet_type, + |_db_tx| Ok(TrezorSignerProvider::new().map_err(SignerError::TrezorError)?), + ) + .map_err(ControllerError::WalletError)?; + Ok(RuntimeWallet::Trezor(wallet)) + } + } } pub fn recover_wallet( chain_config: Arc, file_path: impl AsRef, - mnemonic: mnemonic::Mnemonic, - passphrase: Option<&str>, - whether_to_store_seed_phrase: StoreSeedPhrase, + args: WalletTypeArgsComputed, wallet_type: WalletType, - ) -> Result> { + ) -> Result, ControllerError> { utils::ensure!( !file_path.as_ref().exists(), ControllerError::WalletFileError( @@ -264,17 +302,44 @@ impl Controll let db = wallet::wallet::open_or_create_wallet_file(file_path) .map_err(ControllerError::WalletError)?; - let wallet = wallet::Wallet::recover_wallet( - Arc::clone(&chain_config), - db, - &mnemonic.to_string(), - passphrase, - whether_to_store_seed_phrase, - wallet_type, - ) - .map_err(ControllerError::WalletError)?; - Ok(wallet) + match args { + WalletTypeArgsComputed::Software { + mnemonic, + passphrase, + store_seed_phrase, + } => { + let passphrase_ref = passphrase.as_ref().map(|x| x.as_ref()); + + let wallet = wallet::Wallet::recover_wallet( + Arc::clone(&chain_config), + db, + wallet_type, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + &mnemonic.to_string(), + passphrase_ref, + store_seed_phrase, + )?) + }, + ) + .map_err(ControllerError::WalletError)?; + Ok(RuntimeWallet::Software(wallet)) + } + #[cfg(feature = "trezor")] + WalletTypeArgsComputed::Trezor => { + let wallet = wallet::Wallet::recover_wallet( + Arc::clone(&chain_config), + db, + wallet_type, + |_db_tx| Ok(TrezorSignerProvider::new().map_err(SignerError::TrezorError)?), + ) + .map_err(ControllerError::WalletError)?; + Ok(RuntimeWallet::Trezor(wallet)) + } + } } fn make_backup_wallet_file(file_path: impl AsRef, version: u32) -> WalletResult<()> { @@ -308,9 +373,10 @@ impl Controll chain_config: Arc, file_path: impl AsRef, password: Option, - wallet_type: WalletType, + current_controller_mode: WalletControllerMode, force_change_wallet_type: bool, - ) -> Result> { + open_as_wallet_type: WalletType, + ) -> Result, ControllerError> { utils::ensure!( file_path.as_ref().exists(), ControllerError::WalletFileError( @@ -322,17 +388,35 @@ impl Controll let db = wallet::wallet::open_or_create_wallet_file(&file_path) .map_err(ControllerError::WalletError)?; - let wallet = wallet::Wallet::load_wallet( - Arc::clone(&chain_config), - db, - password, - |version| Self::make_backup_wallet_file(file_path.as_ref(), version), - wallet_type, - force_change_wallet_type, - ) - .map_err(ControllerError::WalletError)?; - - Ok(wallet) + match open_as_wallet_type { + WalletType::Cold | WalletType::Hot => { + let wallet = wallet::Wallet::load_wallet( + Arc::clone(&chain_config), + db, + password, + |version| Self::make_backup_wallet_file(file_path.as_ref(), version), + current_controller_mode, + force_change_wallet_type, + |db_tx| SoftwareSignerProvider::load_from_database(chain_config.clone(), db_tx), + ) + .map_err(ControllerError::WalletError)?; + Ok(RuntimeWallet::Software(wallet)) + } + #[cfg(feature = "trezor")] + WalletType::Trezor => { + let wallet = wallet::Wallet::load_wallet( + Arc::clone(&chain_config), + db, + password, + |version| Self::make_backup_wallet_file(file_path.as_ref(), version), + current_controller_mode, + force_change_wallet_type, + |db_tx| TrezorSignerProvider::load_from_database(chain_config.clone(), db_tx), + ) + .map_err(ControllerError::WalletError)?; + Ok(RuntimeWallet::Trezor(wallet)) + } + } } pub fn seed_phrase(&self) -> Result, ControllerError> { @@ -638,7 +722,7 @@ impl Controll WithLocked::Unlocked, ) .map_err(ControllerError::WalletError)?; - let pool_ids = stake_pool_utxos.into_iter().filter_map(|(_, utxo, _)| match utxo { + let pool_ids = stake_pool_utxos.into_iter().filter_map(|(_, utxo)| match utxo { TxOutput::ProduceBlockFromStake(_, pool_id) | TxOutput::CreateStakePool(pool_id, _) => { Some(pool_id) } @@ -669,13 +753,15 @@ impl Controll /// Synchronize the wallet to the current node tip height and return pub async fn sync_once(&mut self) -> Result<(), ControllerError> { - let res = sync::sync_once( - &self.chain_config, - &self.rpc_client, - &mut self.wallet, - &self.wallet_events, - ) - .await?; + let res = match &mut self.wallet { + RuntimeWallet::Software(w) => { + sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events).await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events).await + } + }?; match res { InSync::Synced => Ok(()), @@ -684,13 +770,17 @@ impl Controll } pub async fn try_sync_once(&mut self) -> Result<(), ControllerError> { - sync::sync_once( - &self.chain_config, - &self.rpc_client, - &mut self.wallet, - &self.wallet_events, - ) - .await?; + match &mut self.wallet { + RuntimeWallet::Software(w) => { + sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events) + .await?; + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + sync::sync_once(&self.chain_config, &self.rpc_client, w, &self.wallet_events) + .await?; + } + } Ok(()) } @@ -699,7 +789,7 @@ impl Controll &mut self, account_index: U31, config: ControllerConfig, - ) -> Result, ControllerError> { + ) -> Result, ControllerError> { self.sync_once().await?; Ok(SyncedController::new( &mut self.wallet, @@ -712,7 +802,10 @@ impl Controll )) } - pub fn readonly_controller(&self, account_index: U31) -> ReadOnlyController { + pub fn readonly_controller( + &self, + account_index: U31, + ) -> ReadOnlyController<'_, T, DefaultBackend> { ReadOnlyController::new( &self.wallet, self.rpc_client.clone(), @@ -819,9 +912,14 @@ impl Controll &self, ptx: PartiallySignedTransaction, ) -> Result> { - let input_utxos: Vec<_> = ptx.input_utxos().iter().flatten().cloned().collect(); + let input_utxos: Vec<_> = + ptx.input_utxos().iter().flatten().map(|utxo| &utxo.utxo).cloned().collect(); let fees = self.get_fees(&input_utxos, ptx.tx().outputs()).await?; - let inputs_utxos_refs: Vec<_> = ptx.input_utxos().iter().map(|out| out.as_ref()).collect(); + let inputs_utxos_refs: Vec<_> = ptx + .input_utxos() + .iter() + .map(|out| out.as_ref().map(|utxo| &utxo.utxo)) + .collect(); let signature_statuses: Vec<_> = ptx .witnesses() .iter() @@ -958,14 +1056,22 @@ impl Controll .collect::, WalletError>>() .map_err(ControllerError::WalletError)?; + let input_utxos = self.fetch_utxos_extra_info(input_utxos).await?; + let output_additional_infos = self + .fetch_utxos_extra_info(tx.outputs().to_vec()) + .await? + .into_iter() + .map(|x| x.additional_info) + .collect(); let tx = PartiallySignedTransaction::new( tx, vec![None; num_inputs], input_utxos.into_iter().map(Option::Some).collect(), destinations.into_iter().map(Option::Some).collect(), htlc_secrets, + output_additional_infos, ) - .map_err(WalletError::TransactionCreation)?; + .map_err(WalletError::PartiallySignedTransactionCreation)?; TransactionToSign::Partial(tx) }; @@ -1038,25 +1144,24 @@ impl Controll &self, inputs: &[UtxoOutPoint], ) -> Result, ControllerError> { - let tasks: FuturesOrdered<_> = inputs.iter().map(|input| self.fetch_utxo(input)).collect(); + let tasks: FuturesOrdered<_> = inputs + .iter() + .map(|input| fetch_utxo(&self.rpc_client, input, &self.wallet)) + .collect(); let input_utxos: Vec = tasks.try_collect().await?; Ok(input_utxos) } - async fn fetch_utxo(&self, input: &UtxoOutPoint) -> Result> { - // search locally for the unspent utxo - if let Some(out) = self.wallet.find_unspent_utxo_with_destination(input) { - return Ok(out.0); - } - - // check the chainstate - self.rpc_client - .get_utxo(input.clone()) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( - input.clone(), - ))) + async fn fetch_utxos_extra_info( + &self, + inputs: Vec, + ) -> Result, ControllerError> { + let tasks: FuturesOrdered<_> = inputs + .into_iter() + .map(|input| fetch_utxo_extra_info(&self.rpc_client, input)) + .collect(); + let input_utxos: Vec = tasks.try_collect().await?; + Ok(input_utxos) } async fn fetch_opt_utxo( @@ -1064,7 +1169,7 @@ impl Controll input: &TxInput, ) -> Result, ControllerError> { match input { - TxInput::Utxo(utxo) => self.fetch_utxo(utxo).await.map(Some), + TxInput::Utxo(utxo) => fetch_utxo(&self.rpc_client, utxo, &self.wallet).await.map(Some), TxInput::Account(_) => Ok(None), TxInput::AccountCommand(_, _) => Ok(None), } @@ -1143,44 +1248,3 @@ impl Controll } } } - -pub async fn fetch_token_info( - rpc_client: &T, - token_id: TokenId, -) -> Result> { - rpc_client - .get_token_info(token_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::UnknownTokenId( - token_id, - ))) -} - -pub async fn into_balances( - rpc_client: &T, - chain_config: &ChainConfig, - mut balances: BTreeMap, -) -> Result> { - let coins = balances.remove(&Currency::Coin).unwrap_or(Amount::ZERO); - let coins = RpcAmountOut::from_amount_no_padding(coins, chain_config.coin_decimals()); - - let tasks: FuturesUnordered<_> = balances - .into_iter() - .map(|(currency, amount)| async move { - let token_id = match currency { - Currency::Coin => panic!("Removed just above"), - Currency::Token(token_id) => token_id, - }; - - fetch_token_info(rpc_client, token_id).await.map(|info| { - let decimals = info.token_number_of_decimals(); - let amount = RpcAmountOut::from_amount_no_padding(amount, decimals); - let token_id = RpcAddress::new(chain_config, token_id).expect("addressable"); - (token_id, amount) - }) - }) - .collect(); - - Ok(Balances::new(coins, tasks.try_collect().await?)) -} diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index a5cc00d704..2e9d1b2ab2 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -32,7 +32,6 @@ use utils::tap_log::TapLog; use wallet::{ account::{transaction_list::TransactionList, DelegationData, PoolData, TxInfo}, wallet::WalletPoolsFilter, - DefaultWallet, }; use wallet_types::{ account_info::StandaloneAddresses, @@ -43,12 +42,13 @@ use wallet_types::{ }; use crate::{ + runtime_wallet::RuntimeWallet, types::{AccountStandaloneKeyDetails, Balances, CreatedBlockInfo}, ControllerError, }; -pub struct ReadOnlyController<'a, T> { - wallet: &'a DefaultWallet, +pub struct ReadOnlyController<'a, T, B: storage::Backend + 'static> { + wallet: &'a RuntimeWallet, rpc_client: T, chain_config: &'a ChainConfig, account_index: U31, @@ -57,9 +57,13 @@ pub struct ReadOnlyController<'a, T> { /// A Map between the derived child number and the Address with whether it is marked as used or not type MapAddressWithUsage = BTreeMap, bool)>; -impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { +impl<'a, T, B> ReadOnlyController<'a, T, B> +where + T: NodeInterface, + B: storage::Backend + 'static, +{ pub fn new( - wallet: &'a DefaultWallet, + wallet: &'a RuntimeWallet, rpc_client: T, chain_config: &'a ChainConfig, account_index: U31, @@ -103,9 +107,7 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { ) -> Result, ControllerError> { self.wallet .get_multisig_utxos(self.account_index, utxo_types, utxo_states, with_locked) - .map(|utxos| { - utxos.into_iter().map(|(outpoint, output, _)| (outpoint, output)).collect() - }) + .map(|utxos| utxos.into_iter().collect()) .map_err(ControllerError::WalletError) } @@ -117,9 +119,6 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { ) -> Result, ControllerError> { self.wallet .get_utxos(self.account_index, utxo_types, utxo_states, with_locked) - .map(|utxos| { - utxos.into_iter().map(|(outpoint, output, _)| (outpoint, output)).collect() - }) .map_err(ControllerError::WalletError) } @@ -309,13 +308,10 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { pub async fn get_delegations( &self, ) -> Result, ControllerError> { - let delegations = self + let tasks: FuturesUnordered<_> = self .wallet .get_delegations(self.account_index) - .map_err(ControllerError::WalletError)?; - - let tasks: FuturesUnordered<_> = delegations - .into_iter() + .map_err(ControllerError::WalletError)? .map(|(delegation_id, delegation_data)| { self.get_delegation_share(delegation_data, *delegation_id).map(|res| { res.map(|opt| { @@ -328,6 +324,7 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { .collect(); let delegations = tasks.try_collect::>().await?.into_iter().flatten().collect(); + Ok(delegations) } diff --git a/wallet/wallet-controller/src/runtime_wallet.rs b/wallet/wallet-controller/src/runtime_wallet.rs new file mode 100644 index 0000000000..83fa2dffd6 --- /dev/null +++ b/wallet/wallet-controller/src/runtime_wallet.rs @@ -0,0 +1,1419 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::{BTreeMap, BTreeSet}; + +use common::{ + address::{pubkeyhash::PublicKeyHash, Address}, + chain::{ + classic_multisig::ClassicMultisigChallenge, + htlc::HashedTimelockContract, + output_value::OutputValue, + signature::inputsig::arbitrary_message::ArbitraryMessageSignature, + tokens::{IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance}, + DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, + SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, + }, + primitives::{id::WithId, Amount, BlockHeight, Id, H256}, +}; +use crypto::key::hdkd::child_number::ChildNumber; +use crypto::key::hdkd::u31::U31; +use crypto::key::{PrivateKey, PublicKey}; +use crypto::vrf::VRFPublicKey; +use mempool::FeeRate; +use wallet::account::transaction_list::TransactionList; +use wallet::account::{CoinSelectionAlgo, DelegationData, PoolData, TxInfo, UnconfirmedTokenInfo}; +use wallet::send_request::{PoolOrTokenId, SelectedInputs, StakePoolDataArguments}; +use wallet::signer::software_signer::SoftwareSignerProvider; + +use wallet::wallet::WalletPoolsFilter; +use wallet::wallet_events::WalletEvents; +use wallet::{Wallet, WalletError, WalletResult}; +use wallet_types::account_info::{StandaloneAddressDetails, StandaloneAddresses}; +use wallet_types::partially_signed_transaction::{PartiallySignedTransaction, UtxoAdditionalInfo}; +use wallet_types::seed_phrase::SerializableSeedPhrase; +use wallet_types::signature_status::SignatureStatus; +use wallet_types::utxo_types::{UtxoStates, UtxoTypes}; +use wallet_types::wallet_tx::TxData; +use wallet_types::with_locked::WithLocked; +use wallet_types::{Currency, KeychainUsageState}; + +#[cfg(feature = "trezor")] +use wallet::signer::trezor_signer::TrezorSignerProvider; + +pub enum RuntimeWallet { + Software(Wallet), + #[cfg(feature = "trezor")] + Trezor(Wallet), +} + +impl RuntimeWallet { + pub fn seed_phrase(&self) -> Result, WalletError> { + match self { + RuntimeWallet::Software(w) => w.seed_phrase(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.seed_phrase(), + } + } + + pub fn delete_seed_phrase(&self) -> Result, WalletError> { + match self { + RuntimeWallet::Software(w) => w.delete_seed_phrase(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.delete_seed_phrase(), + } + } + + pub fn reset_wallet_to_genesis(&mut self) -> Result<(), WalletError> { + match self { + RuntimeWallet::Software(w) => w.reset_wallet_to_genesis(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.reset_wallet_to_genesis(), + } + } + + pub fn encrypt_wallet(&mut self, password: &Option) -> Result<(), WalletError> { + match self { + RuntimeWallet::Software(w) => w.encrypt_wallet(password), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.encrypt_wallet(password), + } + } + + pub fn unlock_wallet(&mut self, password: &String) -> Result<(), WalletError> { + match self { + RuntimeWallet::Software(w) => w.unlock_wallet(password), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.unlock_wallet(password), + } + } + + pub fn lock_wallet(&mut self) -> Result<(), WalletError> { + match self { + RuntimeWallet::Software(w) => w.lock_wallet(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.lock_wallet(), + } + } + + pub fn set_lookahead_size( + &mut self, + lookahead_size: u32, + force_reduce: bool, + ) -> Result<(), WalletError> { + match self { + RuntimeWallet::Software(w) => w.set_lookahead_size(lookahead_size, force_reduce), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.set_lookahead_size(lookahead_size, force_reduce), + } + } + + pub fn wallet_info(&self) -> (H256, Vec>) { + match self { + RuntimeWallet::Software(w) => w.wallet_info(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.wallet_info(), + } + } + + pub fn create_next_account( + &mut self, + name: Option, + ) -> Result<(U31, Option), WalletError> { + match self { + RuntimeWallet::Software(w) => w.create_next_account(name), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.create_next_account(name), + } + } + + pub fn set_account_name( + &mut self, + account_index: U31, + name: Option, + ) -> Result<(U31, Option), WalletError> { + match self { + RuntimeWallet::Software(w) => w.set_account_name(account_index, name), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.set_account_name(account_index, name), + } + } + + pub fn get_pos_gen_block_data( + &self, + account_index: U31, + pool_id: PoolId, + ) -> Result { + match self { + RuntimeWallet::Software(w) => w.get_pos_gen_block_data(account_index, pool_id), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + } + } + + pub fn get_pos_gen_block_data_by_pool_id( + &self, + pool_id: PoolId, + ) -> Result { + match self { + RuntimeWallet::Software(w) => w.get_pos_gen_block_data_by_pool_id(pool_id), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + } + } + + pub fn get_pool_ids( + &self, + account_index: U31, + filter: WalletPoolsFilter, + ) -> WalletResult> { + match self { + RuntimeWallet::Software(w) => w.get_pool_ids(account_index, filter), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_pool_ids(account_index, filter), + } + } + + pub fn get_best_block(&self) -> BTreeMap, BlockHeight)> { + match self { + RuntimeWallet::Software(w) => w.get_best_block(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_best_block(), + } + } + + pub fn get_best_block_for_account( + &self, + account_index: U31, + ) -> WalletResult<(Id, BlockHeight)> { + match self { + RuntimeWallet::Software(w) => w.get_best_block_for_account(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_best_block_for_account(account_index), + } + } + + pub fn is_locked(&self) -> bool { + match self { + RuntimeWallet::Software(w) => w.is_locked(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.is_locked(), + } + } + + pub fn get_utxos( + &self, + account_index: U31, + utxo_types: UtxoTypes, + utxo_states: UtxoStates, + with_locked: WithLocked, + ) -> Result, WalletError> { + match self { + RuntimeWallet::Software(w) => { + w.get_utxos(account_index, utxo_types, utxo_states, with_locked) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.get_utxos(account_index, utxo_types, utxo_states, with_locked) + } + } + } + + pub fn get_transactions_to_be_broadcast( + &mut self, + ) -> Result, WalletError> { + match self { + RuntimeWallet::Software(w) => w.get_transactions_to_be_broadcast(), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_transactions_to_be_broadcast(), + } + } + + pub fn get_balance( + &self, + account_index: U31, + utxo_states: UtxoStates, + with_locked: WithLocked, + ) -> WalletResult> { + match self { + RuntimeWallet::Software(w) => w.get_balance(account_index, utxo_states, with_locked), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_balance(account_index, utxo_states, with_locked), + } + } + + pub fn get_multisig_utxos( + &self, + account_index: U31, + utxo_types: UtxoTypes, + utxo_states: UtxoStates, + with_locked: WithLocked, + ) -> WalletResult> { + match self { + RuntimeWallet::Software(w) => { + w.get_multisig_utxos(account_index, utxo_types, utxo_states, with_locked) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.get_multisig_utxos(account_index, utxo_types, utxo_states, with_locked) + } + } + } + + pub fn pending_transactions( + &self, + account_index: U31, + ) -> WalletResult>> { + match self { + RuntimeWallet::Software(w) => w.pending_transactions(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.pending_transactions(account_index), + } + } + + pub fn mainchain_transactions( + &self, + account_index: U31, + destination: Option, + limit: usize, + ) -> WalletResult> { + match self { + RuntimeWallet::Software(w) => { + w.mainchain_transactions(account_index, destination, limit) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.mainchain_transactions(account_index, destination, limit), + } + } + + pub fn get_transaction_list( + &self, + account_index: U31, + skip: usize, + count: usize, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => w.get_transaction_list(account_index, skip, count), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_transaction_list(account_index, skip, count), + } + } + + pub fn get_transaction( + &self, + account_index: U31, + transaction_id: Id, + ) -> WalletResult<&TxData> { + match self { + RuntimeWallet::Software(w) => w.get_transaction(account_index, transaction_id), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_transaction(account_index, transaction_id), + } + } + + pub fn get_all_issued_addresses( + &self, + account_index: U31, + ) -> WalletResult>> { + match self { + RuntimeWallet::Software(w) => w.get_all_issued_addresses(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_all_issued_addresses(account_index), + } + } + + pub fn get_all_issued_vrf_public_keys( + &self, + account_index: U31, + ) -> WalletResult, bool)>> { + match self { + RuntimeWallet::Software(w) => w.get_all_issued_vrf_public_keys(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + } + } + + pub fn get_legacy_vrf_public_key( + &self, + account_index: U31, + ) -> WalletResult> { + match self { + RuntimeWallet::Software(w) => w.get_legacy_vrf_public_key(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + } + } + + pub fn get_addresses_usage(&self, account_index: U31) -> WalletResult<&KeychainUsageState> { + match self { + RuntimeWallet::Software(w) => w.get_addresses_usage(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_addresses_usage(account_index), + } + } + + pub fn get_all_standalone_addresses( + &self, + account_index: U31, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => w.get_all_standalone_addresses(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_all_standalone_addresses(account_index), + } + } + + pub fn get_all_standalone_address_details( + &self, + account_index: U31, + address: Destination, + ) -> WalletResult<( + Destination, + BTreeMap, + StandaloneAddressDetails, + )> { + match self { + RuntimeWallet::Software(w) => { + w.get_all_standalone_address_details(account_index, address) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.get_all_standalone_address_details(account_index, address) + } + } + } + + pub fn get_created_blocks( + &self, + account_index: U31, + ) -> WalletResult, PoolId)>> { + match self { + RuntimeWallet::Software(w) => w.get_created_blocks(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_created_blocks(account_index), + } + } + + pub fn find_used_tokens( + &self, + account_index: U31, + input_utxos: &[UtxoOutPoint], + ) -> WalletResult> { + match self { + RuntimeWallet::Software(w) => w.find_used_tokens(account_index, input_utxos), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_used_tokens(account_index, input_utxos), + } + } + + pub fn get_token_unconfirmed_info( + &self, + account_index: U31, + token_info: RPCFungibleTokenInfo, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => w.get_token_unconfirmed_info(account_index, token_info), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_token_unconfirmed_info(account_index, token_info), + } + } + + pub fn abandon_transaction( + &mut self, + account_index: U31, + tx_id: Id, + ) -> WalletResult<()> { + match self { + RuntimeWallet::Software(w) => w.abandon_transaction(account_index, tx_id), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.abandon_transaction(account_index, tx_id), + } + } + + pub fn standalone_address_label_rename( + &mut self, + account_index: U31, + address: Destination, + label: Option, + ) -> WalletResult<()> { + match self { + RuntimeWallet::Software(w) => { + w.standalone_address_label_rename(account_index, address, label) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.standalone_address_label_rename(account_index, address, label) + } + } + } + + pub fn add_standalone_address( + &mut self, + account_index: U31, + address: PublicKeyHash, + label: Option, + ) -> WalletResult<()> { + match self { + RuntimeWallet::Software(w) => w.add_standalone_address(account_index, address, label), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.add_standalone_address(account_index, address, label), + } + } + + pub fn add_standalone_private_key( + &mut self, + account_index: U31, + private_key: PrivateKey, + label: Option, + ) -> WalletResult<()> { + match self { + RuntimeWallet::Software(w) => { + w.add_standalone_private_key(account_index, private_key, label) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.add_standalone_private_key(account_index, private_key, label) + } + } + } + + pub fn add_standalone_multisig( + &mut self, + account_index: U31, + challenge: ClassicMultisigChallenge, + label: Option, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.add_standalone_multisig(account_index, challenge, label) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.add_standalone_multisig(account_index, challenge, label), + } + } + + pub fn get_new_address( + &mut self, + account_index: U31, + ) -> WalletResult<(ChildNumber, Address)> { + match self { + RuntimeWallet::Software(w) => w.get_new_address(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_new_address(account_index), + } + } + + pub fn find_public_key( + &mut self, + account_index: U31, + address: Destination, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => w.find_public_key(account_index, address), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.find_public_key(account_index, address), + } + } + + pub fn get_vrf_key( + &mut self, + account_index: U31, + ) -> WalletResult<(ChildNumber, Address)> { + match self { + RuntimeWallet::Software(w) => w.get_vrf_key(account_index), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + } + } + + pub async fn issue_new_token( + &mut self, + account_index: U31, + token_issuance: TokenIssuance, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult<(TokenId, SignedTransaction)> { + match self { + RuntimeWallet::Software(w) => { + w.issue_new_token( + account_index, + token_issuance, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.issue_new_token( + account_index, + token_issuance, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn issue_new_nft( + &mut self, + account_index: U31, + address: Address, + metadata: Metadata, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult<(TokenId, SignedTransaction)> { + match self { + RuntimeWallet::Software(w) => { + w.issue_new_nft( + account_index, + address, + metadata, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.issue_new_nft( + account_index, + address, + metadata, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn mint_tokens( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + amount: Amount, + address: Address, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.mint_tokens( + account_index, + &token_info, + amount, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.mint_tokens( + account_index, + &token_info, + amount, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn unmint_tokens( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + amount: Amount, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.unmint_tokens( + account_index, + &token_info, + amount, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.unmint_tokens( + account_index, + &token_info, + amount, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn lock_token_supply( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.lock_token_supply( + account_index, + &token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.lock_token_supply( + account_index, + &token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn freeze_token( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + is_token_unfreezable: IsTokenUnfreezable, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.freeze_token( + account_index, + &token_info, + is_token_unfreezable, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.freeze_token( + account_index, + &token_info, + is_token_unfreezable, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn unfreeze_token( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.unfreeze_token( + account_index, + &token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.unfreeze_token( + account_index, + &token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn change_token_authority( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + address: Address, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.change_token_authority( + account_index, + &token_info, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.change_token_authority( + account_index, + &token_info, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn change_token_metadata_uri( + &mut self, + account_index: U31, + token_info: UnconfirmedTokenInfo, + metadata_uri: Vec, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> Result { + match self { + RuntimeWallet::Software(w) => { + w.change_token_metadata_uri( + account_index, + &token_info, + metadata_uri, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.change_token_metadata_uri( + account_index, + &token_info, + metadata_uri, + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn create_transaction_to_addresses( + &mut self, + account_index: U31, + outputs: impl IntoIterator, + inputs: SelectedInputs, + change_addresses: BTreeMap>, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_transaction_to_addresses( + account_index, + outputs, + inputs, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_transaction_to_addresses( + account_index, + outputs, + inputs, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ) + .await + } + } + } + + pub async fn create_sweep_transaction( + &mut self, + account_index: U31, + destination_address: Destination, + filtered_inputs: Vec<(UtxoOutPoint, TxOutput)>, + current_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_sweep_transaction( + account_index, + destination_address, + filtered_inputs, + current_fee_rate, + &additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_sweep_transaction( + account_index, + destination_address, + filtered_inputs, + current_fee_rate, + &additional_utxo_infos, + ) + .await + } + } + } + + pub fn get_delegation( + &self, + account_index: U31, + delegation_id: DelegationId, + ) -> WalletResult<&DelegationData> { + match self { + RuntimeWallet::Software(w) => w.get_delegation(account_index, delegation_id), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.get_delegation(account_index, delegation_id), + } + } + + pub async fn create_sweep_from_delegation_transaction( + &mut self, + account_index: U31, + destination_address: Address, + delegation_id: DelegationId, + delegation_share: Amount, + current_fee_rate: FeeRate, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_sweep_from_delegation_transaction( + account_index, + destination_address, + delegation_id, + delegation_share, + current_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_sweep_from_delegation_transaction( + account_index, + destination_address, + delegation_id, + delegation_share, + current_fee_rate, + ) + .await + } + } + } + + #[allow(clippy::too_many_arguments)] + pub fn create_unsigned_transaction_to_addresses( + &mut self, + account_index: U31, + outputs: impl IntoIterator, + selected_inputs: SelectedInputs, + selection_algo: Option, + change_addresses: BTreeMap>, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, + ) -> WalletResult<(PartiallySignedTransaction, BTreeMap)> { + match self { + RuntimeWallet::Software(w) => w.create_unsigned_transaction_to_addresses( + account_index, + outputs, + selected_inputs, + selection_algo, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.create_unsigned_transaction_to_addresses( + account_index, + outputs, + selected_inputs, + selection_algo, + change_addresses, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ), + } + } + + pub async fn create_delegation( + &mut self, + account_index: U31, + output: TxOutput, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult<(DelegationId, SignedTransaction)> { + match self { + RuntimeWallet::Software(w) => { + w.create_delegation( + account_index, + vec![output], + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_delegation( + account_index, + vec![output], + current_fee_rate, + consolidate_fee_rate, + ) + .await + } + } + } + + pub async fn create_transaction_to_addresses_from_delegation( + &mut self, + account_index: U31, + address: Address, + amount: Amount, + delegation_id: DelegationId, + delegation_share: Amount, + current_fee_rate: FeeRate, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_transaction_to_addresses_from_delegation( + account_index, + address, + amount, + delegation_id, + delegation_share, + current_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_transaction_to_addresses_from_delegation( + account_index, + address, + amount, + delegation_id, + delegation_share, + current_fee_rate, + ) + .await + } + } + } + + pub async fn create_stake_pool_tx( + &mut self, + account_index: U31, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + stake_pool_arguments: StakePoolDataArguments, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_stake_pool_tx( + account_index, + current_fee_rate, + consolidate_fee_rate, + stake_pool_arguments, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(_) => Err(WalletError::UnsupportedHardwareWalletOperation), + } + } + + pub async fn decommission_stake_pool( + &mut self, + account_index: U31, + pool_id: PoolId, + staker_balance: Amount, + output_address: Option, + current_fee_rate: FeeRate, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.decommission_stake_pool( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.decommission_stake_pool( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .await + } + } + } + + pub async fn decommission_stake_pool_request( + &mut self, + account_index: U31, + pool_id: PoolId, + staker_balance: Amount, + output_address: Option, + current_fee_rate: FeeRate, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.decommission_stake_pool_request( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.decommission_stake_pool_request( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .await + } + } + } + + pub async fn create_htlc_tx( + &mut self, + account_index: U31, + output_value: OutputValue, + htlc: HashedTimelockContract, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: &BTreeMap, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_htlc_tx( + account_index, + output_value, + htlc, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_htlc_tx( + account_index, + output_value, + htlc, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ) + .await + } + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn create_order_tx( + &mut self, + account_index: U31, + ask_value: OutputValue, + give_value: OutputValue, + conclude_key: Address, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, + ) -> WalletResult<(OrderId, SignedTransaction)> { + match self { + RuntimeWallet::Software(w) => { + w.create_order_tx( + account_index, + ask_value, + give_value, + conclude_key, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_order_tx( + account_index, + ask_value, + give_value, + conclude_key, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn create_conclude_order_tx( + &mut self, + account_index: U31, + order_id: OrderId, + order_info: RpcOrderInfo, + output_address: Option, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_conclude_order_tx( + account_index, + order_id, + order_info, + output_address, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_conclude_order_tx( + account_index, + order_id, + order_info, + output_address, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn create_fill_order_tx( + &mut self, + account_index: U31, + order_id: OrderId, + order_info: RpcOrderInfo, + fill_amount_in_ask_currency: Amount, + output_address: Option, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.create_fill_order_tx( + account_index, + order_id, + order_info, + fill_amount_in_ask_currency, + output_address, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_fill_order_tx( + account_index, + order_id, + order_info, + fill_amount_in_ask_currency, + output_address, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + } + } + + pub async fn sign_raw_transaction( + &mut self, + account_index: U31, + ptx: PartiallySignedTransaction, + ) -> WalletResult<( + PartiallySignedTransaction, + Vec, + Vec, + )> { + match self { + RuntimeWallet::Software(w) => w.sign_raw_transaction(account_index, ptx).await, + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.sign_raw_transaction(account_index, ptx).await, + } + } + + pub async fn sign_challenge( + &mut self, + account_index: U31, + challenge: &[u8], + destination: &Destination, + ) -> WalletResult { + match self { + RuntimeWallet::Software(w) => { + w.sign_challenge(account_index, challenge, destination).await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.sign_challenge(account_index, challenge, destination).await + } + } + } + + #[allow(clippy::too_many_arguments)] + pub async fn create_transaction_to_addresses_with_intent( + &mut self, + account_index: U31, + outputs: impl IntoIterator, + inputs: SelectedInputs, + change_addresses: BTreeMap>, + intent: String, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + additional_utxo_infos: BTreeMap, + ) -> WalletResult<(SignedTransaction, SignedTransactionIntent)> { + match self { + RuntimeWallet::Software(w) => { + w.create_transaction_to_addresses_with_intent( + account_index, + outputs, + inputs, + change_addresses, + intent, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.create_transaction_to_addresses_with_intent( + account_index, + outputs, + inputs, + change_addresses, + intent, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + ) + .await + } + } + } + + pub fn add_unconfirmed_tx( + &mut self, + tx: SignedTransaction, + wallet_events: &impl WalletEvents, + ) -> WalletResult<()> { + match self { + RuntimeWallet::Software(w) => w.add_unconfirmed_tx(tx, wallet_events), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w.add_unconfirmed_tx(tx, wallet_events), + } + } + + pub fn add_account_unconfirmed_tx( + &mut self, + account_index: U31, + tx: &SignedTransaction, + wallet_events: &impl WalletEvents, + ) -> WalletResult<()> { + match self { + RuntimeWallet::Software(w) => { + w.add_account_unconfirmed_tx(account_index, tx.clone(), wallet_events) + } + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => { + w.add_account_unconfirmed_tx(account_index, tx.clone(), wallet_events) + } + } + } + + pub fn get_delegations( + &self, + account_index: U31, + ) -> WalletResult + '_>> { + match self { + RuntimeWallet::Software(w) => w + .get_delegations(account_index) + .map(|it| -> Box> { Box::new(it) }), + #[cfg(feature = "trezor")] + RuntimeWallet::Trezor(w) => w + .get_delegations(account_index) + .map(|it| -> Box> { Box::new(it) }), + } + } +} diff --git a/wallet/wallet-controller/src/sync/mod.rs b/wallet/wallet-controller/src/sync/mod.rs index 8e638f4e16..453f425209 100644 --- a/wallet/wallet-controller/src/sync/mod.rs +++ b/wallet/wallet-controller/src/sync/mod.rs @@ -24,7 +24,8 @@ use logging::log; use node_comm::node_traits::NodeInterface; use utils::{once_destructor::OnceDestructor, set_flag::SetFlag}; use wallet::{ - wallet::WalletSyncingState, wallet_events::WalletEvents, DefaultWallet, WalletResult, + signer::SignerProvider, wallet::WalletSyncingState, wallet_events::WalletEvents, Wallet, + WalletResult, }; use crate::ControllerError; @@ -52,7 +53,11 @@ pub trait SyncingWallet { fn update_median_time(&mut self, median_time: BlockTimestamp) -> WalletResult<()>; } -impl SyncingWallet for DefaultWallet { +impl SyncingWallet for Wallet +where + B: storage::Backend + 'static, + P: SignerProvider, +{ fn syncing_state(&self) -> WalletSyncingState { self.get_syncing_state() } diff --git a/wallet/wallet-controller/src/sync/tests/mod.rs b/wallet/wallet-controller/src/sync/tests/mod.rs index f0069b514b..370fd2ee2d 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -25,7 +25,7 @@ use chainstate_test_framework::TestFramework; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - DelegationId, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, + DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, SignedTransaction, Transaction, }, primitives::{time::Time, Amount}, }; @@ -46,7 +46,7 @@ use test_utils::random::{make_seedable_rng, Seed}; use tokio::sync::mpsc; use utils_networking::IpOrSocketAddress; use wallet::wallet_events::WalletEventsNoOp; -use wallet_types::{account_info::DEFAULT_ACCOUNT_INDEX, wallet_type::WalletType}; +use wallet_types::{account_info::DEFAULT_ACCOUNT_INDEX, wallet_type::WalletControllerMode}; use super::*; @@ -202,8 +202,8 @@ impl MockNode { impl NodeInterface for MockNode { type Error = NodeRpcError; - fn is_cold_wallet_node(&self) -> WalletType { - WalletType::Hot + fn is_cold_wallet_node(&self) -> WalletControllerMode { + WalletControllerMode::Hot } async fn chainstate_info(&self) -> Result { @@ -275,6 +275,13 @@ impl NodeInterface for MockNode { unreachable!() } + async fn get_pool_decommission_destination( + &self, + _pool_id: PoolId, + ) -> Result, Self::Error> { + unreachable!() + } + async fn get_delegation_share( &self, _pool_id: PoolId, diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 181ee1dcdf..ad962e2078 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -21,11 +21,11 @@ use common::{ classic_multisig::ClassicMultisigChallenge, htlc::HashedTimelockContract, output_value::OutputValue, - partially_signed_transaction::PartiallySignedTransaction, signature::inputsig::arbitrary_message::ArbitraryMessageSignature, tokens::{ - IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, RPCTokenInfo, - TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, + get_referenced_token_ids, IsTokenFreezable, IsTokenUnfreezable, Metadata, + RPCFungibleTokenInfo, RPCTokenInfo, TokenId, TokenIssuance, TokenIssuanceV1, + TokenTotalSupply, }, ChainConfig, DelegationId, Destination, OrderId, PoolId, RpcOrderInfo, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, @@ -39,7 +39,12 @@ use crypto::{ }, vrf::VRFPublicKey, }; -use futures::{stream::FuturesUnordered, TryStreamExt}; +use futures::{ + future::{self, BoxFuture}, + stream::FuturesUnordered, + FutureExt, TryStreamExt, +}; +use itertools::Itertools; use logging::log; use mempool::FeeRate; use node_comm::node_traits::NodeInterface; @@ -49,13 +54,16 @@ use wallet::{ destination_getters::{get_tx_output_destination, HtlcSpendingCondition}, send_request::{ make_address_output, make_address_output_token, make_create_delegation_output, - make_data_deposit_output, SelectedInputs, StakePoolDataArguments, + make_data_deposit_output, PoolOrTokenId, SelectedInputs, StakePoolDataArguments, }, wallet::WalletPoolsFilter, wallet_events::WalletEvents, - DefaultWallet, WalletError, WalletResult, + WalletError, WalletResult, }; use wallet_types::{ + partially_signed_transaction::{ + PartiallySignedTransaction, TokenAdditionalInfo, UtxoAdditionalInfo, + }, signature_status::SignatureStatus, utxo_types::{UtxoState, UtxoType}, with_locked::WithLocked, @@ -63,13 +71,14 @@ use wallet_types::{ }; use crate::{ - into_balances, + helpers::{fetch_token_info, fetch_utxo, into_balances, tx_to_partially_signed_tx}, + runtime_wallet::RuntimeWallet, types::{Balances, GenericCurrencyTransfer}, ControllerConfig, ControllerError, }; -pub struct SyncedController<'a, T, W> { - wallet: &'a mut DefaultWallet, +pub struct SyncedController<'a, T, W, B: storage::Backend + 'static> { + wallet: &'a mut RuntimeWallet, rpc_client: T, chain_config: &'a ChainConfig, wallet_events: &'a W, @@ -78,9 +87,14 @@ pub struct SyncedController<'a, T, W> { config: ControllerConfig, } -impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { +impl<'a, T, W, B> SyncedController<'a, T, W, B> +where + B: storage::Backend + 'static, + T: NodeInterface, + W: WalletEvents, +{ pub fn new( - wallet: &'a mut DefaultWallet, + wallet: &'a mut RuntimeWallet, rpc_client: T, chain_config: &'a ChainConfig, wallet_events: &'a W, @@ -99,25 +113,14 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { } } - pub async fn get_token_info( - &self, - token_id: TokenId, - ) -> Result> { - self.rpc_client - .get_token_info(token_id) - .await - .map_err(ControllerError::NodeCallError)? - .ok_or(ControllerError::WalletError(WalletError::UnknownTokenId( - token_id, - ))) - } - async fn fetch_token_infos( &self, tokens: BTreeSet, ) -> Result, ControllerError> { - let tasks: FuturesUnordered<_> = - tokens.into_iter().map(|token_id| self.get_token_info(token_id)).collect(); + let tasks: FuturesUnordered<_> = tokens + .into_iter() + .map(|token_id| fetch_token_info(&self.rpc_client, token_id)) + .collect(); tasks.try_collect().await } @@ -134,7 +137,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { for token_info in self.fetch_token_infos(token_ids).await? { match token_info { RPCTokenInfo::FungibleToken(token_info) => { - self.check_fungible_token_is_usable(&token_info)? + self.check_fungible_token_is_usable(token_info)? } RPCTokenInfo::NonFungibleToken(_) => {} } @@ -144,7 +147,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { pub fn check_fungible_token_is_usable( &self, - token_info: &RPCFungibleTokenInfo, + token_info: RPCFungibleTokenInfo, ) -> Result<(), ControllerError> { self.wallet .get_token_unconfirmed_info(self.account_index, token_info) @@ -158,30 +161,58 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { /// Filter out utxos that contain tokens that are frozen and can't be used async fn filter_out_utxos_with_frozen_tokens( &self, - input_utxos: Vec<(UtxoOutPoint, TxOutput, Option)>, - ) -> Result)>, ControllerError> { + input_utxos: Vec<(UtxoOutPoint, TxOutput)>, + ) -> Result< + ( + Vec<(UtxoOutPoint, TxOutput)>, + BTreeMap, + ), + ControllerError, + > { let mut result = vec![]; + let mut additional_utxo_infos = BTreeMap::new(); for utxo in input_utxos { - if let Some(token_id) = utxo.2 { - let token_info = self.get_token_info(token_id).await?; - - let ok_to_use = match token_info { - RPCTokenInfo::FungibleToken(token_info) => self - .wallet - .get_token_unconfirmed_info(self.account_index, &token_info) - .map_err(ControllerError::WalletError)? - .check_can_be_used() - .is_ok(), - RPCTokenInfo::NonFungibleToken(_) => true, - }; + let token_ids = get_referenced_token_ids(&utxo.1); + if token_ids.is_empty() { + result.push(utxo); + } else { + let token_infos = self.fetch_token_infos(token_ids).await?; + let ok_to_use = token_infos.iter().try_fold( + true, + |all_ok, token_info| -> Result> { + let all_ok = all_ok + && match &token_info { + RPCTokenInfo::FungibleToken(token_info) => self + .wallet + .get_token_unconfirmed_info( + self.account_index, + token_info.clone(), + ) + .map_err(ControllerError::WalletError)? + .check_can_be_used() + .is_ok(), + RPCTokenInfo::NonFungibleToken(_) => true, + }; + Ok(all_ok) + }, + )?; + if ok_to_use { result.push(utxo); + for token_info in token_infos { + additional_utxo_infos.insert( + PoolOrTokenId::TokenId(token_info.token_id()), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.token_number_of_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + ); + } } - } else { - result.push(utxo); } } - Ok(result) + + Ok((result, additional_utxo_infos)) } pub fn abandon_transaction( @@ -270,21 +301,23 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx_with_id( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.issue_new_token( - account_index, - TokenIssuance::V1(TokenIssuanceV1 { - token_ticker, - number_of_decimals, - metadata_uri, - total_supply: token_total_supply, - authority: address.into_object(), - is_freezable, - }), - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .issue_new_token( + account_index, + TokenIssuance::V1(TokenIssuanceV1 { + token_ticker, + number_of_decimals, + metadata_uri, + total_supply: token_total_supply, + authority: address.into_object(), + is_freezable, + }), + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -298,15 +331,17 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx_with_id( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.issue_new_nft( - account_index, - address, - metadata, - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .issue_new_nft( + account_index, + address, + metadata, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -319,21 +354,25 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { address: Address, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - token_info.check_can_be_used()?; - wallet.mint_tokens( - account_index, - token_info, - amount, - address, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + if let Err(err) = token_info.check_can_be_used() { + return future::err(err).boxed(); + }; + wallet + .mint_tokens( + account_index, + token_info, + amount, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -344,20 +383,24 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { amount: Amount, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - token_info.check_can_be_used()?; - wallet.unmint_tokens( - account_index, - token_info, - amount, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + if let Err(err) = token_info.check_can_be_used() { + return future::err(err).boxed(); + }; + wallet + .unmint_tokens( + account_index, + token_info, + amount, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -368,19 +411,23 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { token_info: RPCTokenInfo, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - token_info.check_can_be_used()?; - wallet.lock_token_supply( - account_index, - token_info, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + if let Err(err) = token_info.check_can_be_used() { + return future::err(err).boxed(); + }; + wallet + .lock_token_supply( + account_index, + token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -394,19 +441,21 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { is_token_unfreezable: IsTokenUnfreezable, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - wallet.freeze_token( - account_index, - token_info, - is_token_unfreezable, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + wallet + .freeze_token( + account_index, + token_info, + is_token_unfreezable, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -418,18 +467,20 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { token_info: RPCTokenInfo, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - wallet.unfreeze_token( - account_index, - token_info, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + wallet + .unfreeze_token( + account_index, + token_info, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -443,19 +494,24 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { address: Address, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - wallet.change_token_authority( - account_index, - token_info, - address, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + if let Err(err) = token_info.check_can_be_used() { + return future::err(err).boxed(); + }; + wallet + .change_token_authority( + account_index, + token_info, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -467,19 +523,21 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { metadata_uri: Vec, ) -> Result> { self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - wallet.change_token_metadata_uri( - account_index, - token_info, - metadata_uri, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + wallet + .change_token_metadata_uri( + account_index, + token_info, + metadata_uri, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -495,16 +553,19 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_transaction_to_addresses( - account_index, - outputs, - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_transaction_to_addresses( + account_index, + outputs, + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + current_fee_rate, + consolidate_fee_rate, + BTreeMap::new(), + ) + .boxed() }, ) .await @@ -526,16 +587,19 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_transaction_to_addresses( - account_index, - [output], - SelectedInputs::Utxos(selected_utxos), - BTreeMap::new(), - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_transaction_to_addresses( + account_index, + [output], + SelectedInputs::Utxos(selected_utxos), + BTreeMap::new(), + current_fee_rate, + consolidate_fee_rate, + BTreeMap::new(), + ) + .boxed() }, ) .await @@ -548,21 +612,19 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { destination_address: Destination, from_addresses: BTreeSet, ) -> Result> { - let selected_utxos = self - .wallet - .get_utxos( - self.account_index, - UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::IssueNft, - UtxoState::Confirmed | UtxoState::Inactive, - WithLocked::Unlocked, - ) - .map_err(ControllerError::WalletError)?; + let selected_utxos = self.wallet.get_utxos( + self.account_index, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::IssueNft, + UtxoState::Confirmed | UtxoState::Inactive, + WithLocked::Unlocked, + )?; + + let (inputs, additional_utxo_infos) = + self.filter_out_utxos_with_frozen_tokens(selected_utxos).await?; - let filtered_inputs = self - .filter_out_utxos_with_frozen_tokens(selected_utxos) - .await? + let filtered_inputs = inputs .into_iter() - .filter(|(_, output, _)| { + .filter(|(_, output)| { get_tx_output_destination(output, &|_| None, HtlcSpendingCondition::Skip) .map_or(false, |dest| from_addresses.contains(&dest)) }) @@ -571,14 +633,17 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, _consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_sweep_transaction( - account_index, - destination_address, - filtered_inputs, - current_fee_rate, - ) + wallet + .create_sweep_transaction( + account_index, + destination_address, + filtered_inputs, + current_fee_rate, + additional_utxo_infos, + ) + .boxed() }, ) .await @@ -609,15 +674,17 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, _consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_sweep_from_delegation_transaction( - account_index, - destination_address, - delegation_id, - delegation_share, - current_fee_rate, - ) + wallet + .create_sweep_from_delegation_transaction( + account_index, + destination_address, + delegation_id, + delegation_share, + current_fee_rate, + ) + .boxed() }, ) .await @@ -637,7 +704,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { ) -> Result<(PartiallySignedTransaction, Balances), ControllerError> { let output = make_address_output(address, amount); - let utxo_output = self.fetch_utxo(&selected_utxo).await?; + let utxo_output = fetch_utxo(&self.rpc_client, &selected_utxo, self.wallet).await?; let change_address = if let Some(change_address) = change_address { change_address } else { @@ -666,6 +733,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { [(Currency::Coin, change_address)].into(), current_fee_rate, consolidate_fee_rate, + &BTreeMap::new(), ) .map_err(ControllerError::WalletError)?; @@ -709,15 +777,23 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { ControllerError::::ExpectingNonEmptyOutputs ); - let outputs = { + let (outputs, additional_utxo_infos) = { let mut result = Vec::new(); + let mut additional_utxo_infos = BTreeMap::new(); for (token_id, outputs_vec) in outputs { - let token_info = self.get_token_info(token_id).await?; + let token_info = fetch_token_info(&self.rpc_client, token_id).await?; + additional_utxo_infos.insert( + PoolOrTokenId::TokenId(token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.token_number_of_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + ); match &token_info { RPCTokenInfo::FungibleToken(token_info) => { - self.check_fungible_token_is_usable(token_info)? + self.check_fungible_token_is_usable(token_info.clone())? } RPCTokenInfo::NonFungibleToken(_) => { return Err(ControllerError::::NotFungibleToken(token_id)); @@ -731,26 +807,23 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { .map_err(ControllerError::InvalidTxOutput)?; } - result + (result, additional_utxo_infos) }; let (inputs, change_addresses) = { let mut inputs = inputs; let mut change_addresses = change_addresses; - let all_utxos = self - .wallet - .get_utxos( - self.account_index, - UtxoType::Transfer | UtxoType::LockThenTransfer, - UtxoState::Confirmed | UtxoState::InMempool | UtxoState::Inactive, - WithLocked::Unlocked, - ) - .map_err(ControllerError::WalletError)?; + let all_utxos = self.wallet.get_utxos( + self.account_index, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed | UtxoState::InMempool | UtxoState::Inactive, + WithLocked::Unlocked, + )?; let all_coin_utxos = all_utxos .into_iter() - .filter_map(|(o, txo, _)| { + .filter_map(|(o, txo)| { let (val, dest) = match &txo { TxOutput::Transfer(val, dest) | TxOutput::LockThenTransfer(val, dest, _) => (val, dest), @@ -804,18 +877,16 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; - let (tx, fees) = self - .wallet - .create_unsigned_transaction_to_addresses( - self.account_index, - outputs, - selected_inputs, - Some(CoinSelectionAlgo::Randomize), - change_addresses, - current_fee_rate, - consolidate_fee_rate, - ) - .map_err(ControllerError::WalletError)?; + let (tx, fees) = self.wallet.create_unsigned_transaction_to_addresses( + self.account_index, + outputs, + selected_inputs, + Some(CoinSelectionAlgo::Randomize), + change_addresses, + current_fee_rate, + consolidate_fee_rate, + &additional_utxo_infos, + )?; let fees = into_balances(&self.rpc_client, self.chain_config, fees).await?; @@ -834,14 +905,16 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx_with_id( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_delegation( - account_index, - vec![output], - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_delegation( + account_index, + output, + current_fee_rate, + consolidate_fee_rate, + ) + .boxed() }, ) .await @@ -858,16 +931,19 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_transaction_to_addresses( - account_index, - [output], - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_transaction_to_addresses( + account_index, + [output], + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + current_fee_rate, + consolidate_fee_rate, + BTreeMap::new(), + ) + .boxed() }, ) .await @@ -881,11 +957,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { amount: Amount, delegation_id: DelegationId, ) -> Result> { - let pool_id = self - .wallet - .get_delegation(self.account_index, delegation_id) - .map_err(ControllerError::WalletError)? - .pool_id; + let pool_id = self.wallet.get_delegation(self.account_index, delegation_id)?.pool_id; let delegation_share = self .rpc_client @@ -899,16 +971,18 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, _consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_transaction_to_addresses_from_delegation( - account_index, - address, - amount, - delegation_id, - delegation_share, - current_fee_rate, - ) + wallet + .create_transaction_to_addresses_from_delegation( + account_index, + address, + amount, + delegation_id, + delegation_share, + current_fee_rate, + ) + .boxed() }, ) .await @@ -924,21 +998,33 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { ) -> Result> { let output = make_address_output_token(address, amount, token_info.token_id()); self.create_and_send_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - token_info.check_can_be_used()?; - wallet.create_transaction_to_addresses( - account_index, - [output], - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + if let Err(err) = token_info.check_can_be_used() { + return future::err(err).boxed(); + }; + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(token_info.token_id()), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.num_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + )]); + wallet + .create_transaction_to_addresses( + account_index, + [output], + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .boxed() }, ) .await @@ -954,22 +1040,34 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { ) -> Result<(SignedTransaction, SignedTransactionIntent), ControllerError> { let output = make_address_output_token(address, amount, token_info.token_id()); self.create_token_tx( - &token_info, + token_info, move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31, - token_info: &UnconfirmedTokenInfo| { - token_info.check_can_be_used()?; - wallet.create_transaction_to_addresses_with_intent( - account_index, - [output], - SelectedInputs::Utxos(vec![]), - BTreeMap::new(), - intent, - current_fee_rate, - consolidate_fee_rate, - ) + token_info: UnconfirmedTokenInfo| { + if let Err(err) = token_info.check_can_be_used() { + return future::err(err).boxed(); + }; + let additional_info = BTreeMap::from_iter([( + PoolOrTokenId::TokenId(token_info.token_id()), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.num_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + )]); + wallet + .create_transaction_to_addresses_with_intent( + account_index, + [output], + SelectedInputs::Utxos(vec![]), + BTreeMap::new(), + intent, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .boxed() }, ) .await @@ -986,19 +1084,21 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_stake_pool_tx( - account_index, - current_fee_rate, - consolidate_fee_rate, - StakePoolDataArguments { - amount, - margin_ratio_per_thousand, - cost_per_block, - decommission_key, - }, - ) + wallet + .create_stake_pool_tx( + account_index, + current_fee_rate, + consolidate_fee_rate, + StakePoolDataArguments { + amount, + margin_ratio_per_thousand, + cost_per_block, + decommission_key, + }, + ) + .boxed() }, ) .await @@ -1022,15 +1122,17 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.create_and_send_tx( move |current_fee_rate: FeeRate, _consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.decommission_stake_pool( - account_index, - pool_id, - staker_balance, - output_address, - current_fee_rate, - ) + wallet + .decommission_stake_pool( + account_index, + pool_id, + staker_balance, + output_address, + current_fee_rate, + ) + .boxed() }, ) .await @@ -1061,6 +1163,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { output_address, current_fee_rate, ) + .await .map_err(ControllerError::WalletError) } @@ -1068,17 +1171,22 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { &mut self, output_value: OutputValue, htlc: HashedTimelockContract, + additional_utxo_infos: &BTreeMap, ) -> Result> { let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; - let result = self.wallet.create_htlc_tx( - self.account_index, - output_value, - htlc, - current_fee_rate, - consolidate_fee_rate, - )?; + let result = self + .wallet + .create_htlc_tx( + self.account_index, + output_value, + htlc, + current_fee_rate, + consolidate_fee_rate, + additional_utxo_infos, + ) + .await?; Ok(result) } @@ -1087,20 +1195,26 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { ask_value: OutputValue, give_value: OutputValue, conclude_key: Address, + token_infos: Vec, ) -> Result<(SignedTransaction, OrderId), ControllerError> { + let additional_info = self.additional_token_infos(token_infos)?; + self.create_and_send_tx_with_id( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_order_tx( - account_index, - ask_value, - give_value, - conclude_key, - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_order_tx( + account_index, + ask_value, + give_value, + conclude_key, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .boxed() }, ) .await @@ -1111,20 +1225,25 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { order_id: OrderId, order_info: RpcOrderInfo, output_address: Option, + token_infos: Vec, ) -> Result> { + let additional_info = self.additional_token_infos(token_infos)?; self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_conclude_order_tx( - account_index, - order_id, - order_info, - output_address, - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_conclude_order_tx( + account_index, + order_id, + order_info, + output_address, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .boxed() }, ) .await @@ -1136,34 +1255,56 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { order_info: RpcOrderInfo, fill_amount_in_ask_currency: Amount, output_address: Option, + token_infos: Vec, ) -> Result> { + let additional_info = self.additional_token_infos(token_infos)?; self.create_and_send_tx( move |current_fee_rate: FeeRate, consolidate_fee_rate: FeeRate, - wallet: &mut DefaultWallet, + wallet: &mut RuntimeWallet, account_index: U31| { - wallet.create_fill_order_tx( - account_index, - order_id, - order_info, - fill_amount_in_ask_currency, - output_address, - current_fee_rate, - consolidate_fee_rate, - ) + wallet + .create_fill_order_tx( + account_index, + order_id, + order_info, + fill_amount_in_ask_currency, + output_address, + current_fee_rate, + consolidate_fee_rate, + additional_info, + ) + .boxed() }, ) .await } + fn additional_token_infos( + &mut self, + token_infos: Vec, + ) -> Result, ControllerError> { + token_infos + .into_iter() + .map(|token_info| { + let token_info = self.unconfiremd_token_info(token_info)?; + + Ok(( + PoolOrTokenId::TokenId(token_info.token_id()), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.num_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + )) + }) + .try_collect() + } + /// Checks if the wallet has stake pools and marks this account for staking. pub fn start_staking(&mut self) -> Result<(), ControllerError> { utils::ensure!(!self.wallet.is_locked(), ControllerError::WalletIsLocked); // Make sure that account_index is valid and that pools exist - let pool_ids = self - .wallet - .get_pool_ids(self.account_index, WalletPoolsFilter::Stake) - .map_err(ControllerError::WalletError)?; + let pool_ids = self.wallet.get_pool_ids(self.account_index, WalletPoolsFilter::Stake)?; utils::ensure!(!pool_ids.is_empty(), ControllerError::NoStakingPool); log::info!("Start staking, account_index: {}", self.account_index); self.staking_started.insert(self.account_index); @@ -1172,7 +1313,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { /// Tries to sign any unsigned inputs of a raw or partially signed transaction with the private /// keys in this wallet. - pub fn sign_raw_transaction( + pub async fn sign_raw_transaction( &mut self, tx: TransactionToSign, ) -> Result< @@ -1183,18 +1324,27 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { ), ControllerError, > { + let ptx = match tx { + TransactionToSign::Partial(ptx) => ptx, + TransactionToSign::Tx(tx) => { + tx_to_partially_signed_tx(&self.rpc_client, self.wallet, tx).await? + } + }; + self.wallet - .sign_raw_transaction(self.account_index, tx) + .sign_raw_transaction(self.account_index, ptx) + .await .map_err(ControllerError::WalletError) } - pub fn sign_challenge( + pub async fn sign_challenge( &mut self, challenge: &[u8], destination: &Destination, ) -> Result> { self.wallet .sign_challenge(self.account_index, challenge, destination) + .await .map_err(ControllerError::WalletError) } @@ -1223,7 +1373,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { tx: SignedTransaction, ) -> Result> { self.wallet - .add_account_unconfirmed_tx(self.account_index, tx.clone(), self.wallet_events) + .add_account_unconfirmed_tx(self.account_index, &tx, self.wallet_events) .map_err(ControllerError::WalletError)?; self.rpc_client @@ -1248,12 +1398,21 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { } /// Create a transaction and broadcast it - async fn create_and_send_tx< - F: FnOnce(FeeRate, FeeRate, &mut DefaultWallet, U31) -> WalletResult, - >( + async fn create_and_send_tx( &mut self, - tx_maker: F, - ) -> Result> { + tx_maker: Fun, + ) -> Result> + where + Fun: FnOnce( + FeeRate, + FeeRate, + &mut RuntimeWallet, + U31, + ) -> BoxFuture> + + Send + + 'static, + ControllerError: From, + { let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; @@ -1263,7 +1422,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.wallet, self.account_index, ) - .map_err(ControllerError::WalletError)?; + .await?; self.broadcast_to_mempool_if_needed(tx).await } @@ -1271,28 +1430,21 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { /// Create a transaction that uses a token, check if that token can be used i.e. not frozen. async fn create_token_tx( &mut self, - token_info: &RPCTokenInfo, + token_info: RPCTokenInfo, tx_maker: F, ) -> Result> where F: FnOnce( - FeeRate, - FeeRate, - &mut DefaultWallet, - U31, - &UnconfirmedTokenInfo, - ) -> WalletResult, + FeeRate, + FeeRate, + &mut RuntimeWallet, + U31, + UnconfirmedTokenInfo, + ) -> BoxFuture> + + Send + + 'static, { - // make sure we can use the token before create an tx using it - let token_freezable_info = match token_info { - RPCTokenInfo::FungibleToken(token_info) => self - .wallet - .get_token_unconfirmed_info(self.account_index, token_info) - .map_err(ControllerError::WalletError)?, - RPCTokenInfo::NonFungibleToken(info) => { - UnconfirmedTokenInfo::NonFungibleToken(info.token_id) - } - }; + let token_freezable_info = self.unconfiremd_token_info(token_info)?; let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; @@ -1302,8 +1454,9 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { consolidate_fee_rate, self.wallet, self.account_index, - &token_freezable_info, + token_freezable_info, ) + .await .map_err(ControllerError::WalletError)?; Ok(tx) @@ -1311,28 +1464,53 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { /// Create and broadcast a transaction that uses a token, /// check if that token can be used i.e. not frozen. - async fn create_and_send_token_tx< - F: FnOnce( - FeeRate, - FeeRate, - &mut DefaultWallet, - U31, - &UnconfirmedTokenInfo, - ) -> WalletResult, - >( + async fn create_and_send_token_tx( &mut self, - token_info: &RPCTokenInfo, + token_info: RPCTokenInfo, tx_maker: F, - ) -> Result> { + ) -> Result> + where + F: FnOnce( + FeeRate, + FeeRate, + &mut RuntimeWallet, + U31, + UnconfirmedTokenInfo, + ) -> BoxFuture> + + Send + + 'static, + { let tx = self.create_token_tx(token_info, tx_maker).await?; self.broadcast_to_mempool_if_needed(tx).await } + fn unconfiremd_token_info( + &mut self, + token_info: RPCTokenInfo, + ) -> Result> { + let token_freezable_info = match token_info { + RPCTokenInfo::FungibleToken(token_info) => { + self.wallet.get_token_unconfirmed_info(self.account_index, token_info)? + } + RPCTokenInfo::NonFungibleToken(info) => { + UnconfirmedTokenInfo::NonFungibleToken(info.token_id, info.as_ref().into()) + } + }; + Ok(token_freezable_info) + } + /// Similar to create_and_send_tx but some transactions also create an ID /// e.g. newly issued token, nft or delegation id async fn create_and_send_tx_with_id< ID, - F: FnOnce(FeeRate, FeeRate, &mut DefaultWallet, U31) -> WalletResult<(ID, SignedTransaction)>, + F: FnOnce( + FeeRate, + FeeRate, + &mut RuntimeWallet, + U31, + ) -> BoxFuture> + + Send + + 'static, >( &mut self, tx_maker: F, @@ -1346,21 +1524,10 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.wallet, self.account_index, ) + .await .map_err(ControllerError::WalletError)?; let tx = self.broadcast_to_mempool_if_needed(tx).await?; Ok((tx, id)) } - - async fn fetch_utxo(&self, input: &UtxoOutPoint) -> Result> { - let utxo = self - .rpc_client - .get_utxo(input.clone()) - .await - .map_err(ControllerError::NodeCallError)?; - - utxo.ok_or(ControllerError::WalletError(WalletError::CannotFindUtxo( - input.clone(), - ))) - } } diff --git a/wallet/wallet-controller/src/types/mod.rs b/wallet/wallet-controller/src/types/mod.rs index 646af702b0..d7f5ecadbd 100644 --- a/wallet/wallet-controller/src/types/mod.rs +++ b/wallet/wallet-controller/src/types/mod.rs @@ -22,6 +22,7 @@ mod standalone_key; mod transaction; pub use balances::Balances; +use bip39::{Language, Mnemonic}; pub use block_info::{BlockInfo, CreatedBlockInfo}; pub use common::primitives::amount::RpcAmountOut; use common::{ @@ -38,6 +39,12 @@ pub use transaction::{ InspectTransaction, SignatureStats, TransactionToInspect, ValidatedSignatures, }; use utils::ensure; +use wallet_types::{ + seed_phrase::StoreSeedPhrase, + wallet_type::{WalletControllerMode, WalletType}, +}; + +use crate::mnemonic; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rpc_description::HasValueHint)] pub struct WalletInfo { @@ -140,3 +147,96 @@ impl rpc_description::HasValueHint for GenericCurrencyTransfer { impl rpc_description::HasValueHint for GenericTokenTransfer { const HINT_SER: rpc_description::ValueHint = rpc_description::ValueHint::GENERIC_OBJECT; } + +pub enum CreatedWallet { + UserProvidedMnemonic, + NewlyGeneratedMnemonic(Mnemonic, Option), +} + +#[derive(Debug, Clone)] +pub enum WalletTypeArgs { + Software { + mnemonic: Option, + passphrase: Option, + store_seed_phrase: StoreSeedPhrase, + }, + #[cfg(feature = "trezor")] + Trezor, +} + +impl WalletTypeArgs { + pub fn wallet_type(&self, controller_mode: WalletControllerMode) -> WalletType { + match self { + Self::Software { + mnemonic: _, + passphrase: _, + store_seed_phrase: _, + } => controller_mode.into(), + #[cfg(feature = "trezor")] + Self::Trezor => WalletType::Trezor, + } + } + pub fn user_supplied_mnemonic(&self) -> bool { + match self { + Self::Software { + mnemonic, + passphrase: _, + store_seed_phrase: _, + } => mnemonic.is_some(), + #[cfg(feature = "trezor")] + Self::Trezor => true, + } + } + + pub fn parse_or_generate_mnemonic_if_needed( + self, + ) -> Result<(WalletTypeArgsComputed, CreatedWallet), mnemonic::Error> { + match self { + Self::Software { + mnemonic, + passphrase, + store_seed_phrase, + } => { + let language = Language::English; + let (mnemonic, created_wallet) = match &mnemonic { + Some(mnemonic) => { + let mnemonic = mnemonic::parse_mnemonic(language, mnemonic)?; + (mnemonic, CreatedWallet::UserProvidedMnemonic) + } + None => { + let mnemonic = mnemonic::generate_new_mnemonic(language); + ( + mnemonic.clone(), + CreatedWallet::NewlyGeneratedMnemonic(mnemonic, passphrase.clone()), + ) + } + }; + + Ok(( + WalletTypeArgsComputed::Software { + mnemonic, + passphrase, + store_seed_phrase, + }, + created_wallet, + )) + } + + #[cfg(feature = "trezor")] + Self::Trezor => Ok(( + WalletTypeArgsComputed::Trezor, + CreatedWallet::UserProvidedMnemonic, + )), + } + } +} + +pub enum WalletTypeArgsComputed { + Software { + mnemonic: Mnemonic, + passphrase: Option, + store_seed_phrase: StoreSeedPhrase, + }, + #[cfg(feature = "trezor")] + Trezor, +} diff --git a/wallet/wallet-controller/src/types/transaction.rs b/wallet/wallet-controller/src/types/transaction.rs index a1aab80247..df7d953ec1 100644 --- a/wallet/wallet-controller/src/types/transaction.rs +++ b/wallet/wallet-controller/src/types/transaction.rs @@ -13,11 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::chain::{ - partially_signed_transaction::PartiallySignedTransaction, SignedTransaction, Transaction, -}; +use common::chain::{SignedTransaction, Transaction}; use serialization::hex_encoded::HexEncoded; -use wallet_types::signature_status::SignatureStatus; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, signature_status::SignatureStatus, +}; use super::Balances; diff --git a/wallet/wallet-node-client/Cargo.toml b/wallet/wallet-node-client/Cargo.toml index a4155e04f7..8970753a39 100644 --- a/wallet/wallet-node-client/Cargo.toml +++ b/wallet/wallet-node-client/Cargo.toml @@ -33,3 +33,6 @@ tower.workspace = true chainstate-storage = { path = "../../chainstate/storage" } tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } + +[features] +trezor = ["wallet-types/trezor"] diff --git a/wallet/wallet-node-client/src/handles_client/mod.rs b/wallet/wallet-node-client/src/handles_client/mod.rs index f2f12d6b6a..16388ddf44 100644 --- a/wallet/wallet-node-client/src/handles_client/mod.rs +++ b/wallet/wallet-node-client/src/handles_client/mod.rs @@ -20,8 +20,8 @@ use chainstate::{BlockSource, ChainInfo, ChainstateError, ChainstateHandle}; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, - Transaction, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + SignedTransaction, Transaction, }, primitives::{time::Time, Amount, BlockHeight, Id}, }; @@ -38,7 +38,7 @@ use p2p::{ }; use serialization::hex::HexError; use utils_networking::IpOrSocketAddress; -use wallet_types::wallet_type::WalletType; +use wallet_types::wallet_type::WalletControllerMode; use crate::node_traits::NodeInterface; @@ -103,8 +103,8 @@ impl WalletHandlesClient { impl NodeInterface for WalletHandlesClient { type Error = WalletHandlesClientError; - fn is_cold_wallet_node(&self) -> WalletType { - WalletType::Hot + fn is_cold_wallet_node(&self) -> WalletControllerMode { + WalletControllerMode::Hot } async fn chainstate_info(&self) -> Result { @@ -196,6 +196,18 @@ impl NodeInterface for WalletHandlesClient { Ok(result) } + async fn get_pool_decommission_destination( + &self, + pool_id: PoolId, + ) -> Result, Self::Error> { + let result = self + .chainstate + .call(move |this| this.get_stake_pool_data(pool_id)) + .await?? + .map(|data| data.decommission_destination().clone()); + Ok(result) + } + async fn get_delegation_share( &self, pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/node_traits.rs b/wallet/wallet-node-client/src/node_traits.rs index afc22cd9d6..bf90fcb426 100644 --- a/wallet/wallet-node-client/src/node_traits.rs +++ b/wallet/wallet-node-client/src/node_traits.rs @@ -19,8 +19,8 @@ use chainstate::ChainInfo; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, - Transaction, TxOutput, UtxoOutPoint, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, primitives::{time::Time, Amount, BlockHeight, Id}, }; @@ -31,13 +31,13 @@ use mempool::{tx_accumulator::PackingStrategy, tx_options::TxOptionsOverrides, F use p2p::types::{bannable_address::BannableAddress, socket_address::SocketAddress}; pub use p2p::{interface::types::ConnectedPeer, types::peer_id::PeerId}; use utils_networking::IpOrSocketAddress; -use wallet_types::wallet_type::WalletType; +use wallet_types::wallet_type::WalletControllerMode; #[async_trait::async_trait] pub trait NodeInterface { type Error: std::error::Error + Send + Sync + 'static; - fn is_cold_wallet_node(&self) -> WalletType; + fn is_cold_wallet_node(&self) -> WalletControllerMode; async fn chainstate_info(&self) -> Result; async fn get_best_block_id(&self) -> Result, Self::Error>; @@ -65,6 +65,10 @@ pub trait NodeInterface { ) -> Result, BlockHeight)>, Self::Error>; async fn get_stake_pool_balance(&self, pool_id: PoolId) -> Result, Self::Error>; async fn get_staker_balance(&self, pool_id: PoolId) -> Result, Self::Error>; + async fn get_pool_decommission_destination( + &self, + pool_id: PoolId, + ) -> Result, Self::Error>; async fn get_delegation_share( &self, pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/rpc_client/client_impl.rs b/wallet/wallet-node-client/src/rpc_client/client_impl.rs index 29e992cc08..0140653d27 100644 --- a/wallet/wallet-node-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-node-client/src/rpc_client/client_impl.rs @@ -21,8 +21,8 @@ use common::{ address::Address, chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, - Transaction, TxOutput, UtxoOutPoint, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + SignedTransaction, Transaction, TxOutput, UtxoOutPoint, }, primitives::{time::Time, Amount, BlockHeight, Id}, }; @@ -38,7 +38,7 @@ use p2p::{ }; use serialization::hex_encoded::HexEncoded; use utils_networking::IpOrSocketAddress; -use wallet_types::wallet_type::WalletType; +use wallet_types::wallet_type::WalletControllerMode; use crate::node_traits::NodeInterface; @@ -48,8 +48,8 @@ use super::{NodeRpcClient, NodeRpcError}; impl NodeInterface for NodeRpcClient { type Error = NodeRpcError; - fn is_cold_wallet_node(&self) -> WalletType { - WalletType::Hot + fn is_cold_wallet_node(&self) -> WalletControllerMode { + WalletControllerMode::Hot } async fn chainstate_info(&self) -> Result { @@ -141,6 +141,19 @@ impl NodeInterface for NodeRpcClient { .map_err(NodeRpcError::ResponseError) } + async fn get_pool_decommission_destination( + &self, + pool_id: PoolId, + ) -> Result, Self::Error> { + let pool_address = Address::new(&self.chain_config, pool_id)?; + ChainstateRpcClient::pool_decommission_destination( + &self.http_client, + pool_address.into_string(), + ) + .await + .map_err(NodeRpcError::ResponseError) + } + async fn get_delegation_share( &self, pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs index 7b2c035392..0ace8356a8 100644 --- a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs +++ b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs @@ -20,8 +20,8 @@ use chainstate::ChainInfo; use common::{ chain::{ tokens::{RPCTokenInfo, TokenId}, - Block, DelegationId, GenBlock, OrderId, PoolId, RpcOrderInfo, SignedTransaction, - Transaction, + Block, DelegationId, Destination, GenBlock, OrderId, PoolId, RpcOrderInfo, + SignedTransaction, Transaction, }, primitives::{time::Time, Amount, BlockHeight, Id}, }; @@ -33,7 +33,7 @@ use p2p::{ types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}, }; use utils_networking::IpOrSocketAddress; -use wallet_types::wallet_type::WalletType; +use wallet_types::wallet_type::WalletControllerMode; use crate::node_traits::NodeInterface; @@ -49,8 +49,8 @@ pub enum ColdWalletRpcError { impl NodeInterface for ColdWalletClient { type Error = ColdWalletRpcError; - fn is_cold_wallet_node(&self) -> WalletType { - WalletType::Cold + fn is_cold_wallet_node(&self) -> WalletControllerMode { + WalletControllerMode::Cold } async fn chainstate_info(&self) -> Result { @@ -119,6 +119,13 @@ impl NodeInterface for ColdWalletClient { Err(ColdWalletRpcError::NotAvailable) } + async fn get_pool_decommission_destination( + &self, + _pool_id: PoolId, + ) -> Result, Self::Error> { + Err(ColdWalletRpcError::NotAvailable) + } + async fn get_delegation_share( &self, _pool_id: PoolId, diff --git a/wallet/wallet-node-client/src/rpc_client/mod.rs b/wallet/wallet-node-client/src/rpc_client/mod.rs index 3e646cc750..c24021e9b5 100644 --- a/wallet/wallet-node-client/src/rpc_client/mod.rs +++ b/wallet/wallet-node-client/src/rpc_client/mod.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use common::address::AddressError; use common::chain::ChainConfig; +use common::primitives::per_thousand::PerThousandParseError; use rpc::new_http_client; use rpc::ClientError; use rpc::RpcAuthData; @@ -39,6 +40,8 @@ pub enum NodeRpcError { ResponseError(ClientError), #[error("Address error: {0}")] AddressError(#[from] AddressError), + #[error("PerThousand parse error: {0}")] + PerThousandParseError(#[from] PerThousandParseError), } #[derive(Clone, Debug)] diff --git a/wallet/wallet-rpc-client/Cargo.toml b/wallet/wallet-rpc-client/Cargo.toml index c62e8133fd..8fa3b30a5c 100644 --- a/wallet/wallet-rpc-client/Cargo.toml +++ b/wallet/wallet-rpc-client/Cargo.toml @@ -18,6 +18,7 @@ p2p-types = { path = "../../p2p/types" } rpc = { path = "../../rpc" } serialization = { path = "../../serialization" } subsystem = { path = "../../subsystem" } +utils = { path = "../../utils" } utils-networking = { path = "../../utils/networking" } wallet = { path = ".." } wallet-controller = { path = "../wallet-controller" } @@ -35,3 +36,6 @@ tower.workspace = true chainstate-storage = { path = "../../chainstate/storage" } tokio = { workspace = true, default-features = false, features = ["io-util", "macros", "net", "rt", "sync"] } + +[features] +trezor = ["wallet/trezor", "wallet-types/trezor", "wallet-rpc-lib/trezor", "wallet-controller/trezor"] diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index b30051c49a..7558687889 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -19,9 +19,8 @@ use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ address::{dehexify::dehexify_all_addresses, AddressError}, chain::{ - block::timestamp::BlockTimestamp, partially_signed_transaction::PartiallySignedTransaction, - tokens::IsTokenUnfreezable, Block, GenBlock, SignedTransaction, SignedTransactionIntent, - Transaction, TxOutput, UtxoOutPoint, + block::timestamp::BlockTimestamp, tokens::IsTokenUnfreezable, Block, GenBlock, + SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id, Idable, H256}, }; @@ -30,17 +29,21 @@ use node_comm::node_traits::NodeInterface; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}; use rpc::types::RpcHexString; use serialization::{hex::HexEncode, hex_encoded::HexEncoded, json_encoded::JsonEncoded}; +#[cfg(feature = "trezor")] +use utils::ensure; use utils_networking::IpOrSocketAddress; use wallet::{account::TxInfo, version::get_version}; use wallet_controller::{ - types::{CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo}, + types::{ + CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo, WalletTypeArgs, + }, ConnectedPeer, ControllerConfig, UtxoState, UtxoType, }; use wallet_rpc_lib::{ types::{ AddressInfo, AddressWithUsageInfo, Balances, BlockInfo, ComposedTransaction, CreatedWallet, - DelegationInfo, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegation, NewOrder, - NewTransaction, NftMetadata, NodeVersion, PoolInfo, PublicKeyInfo, + DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegation, + NewOrder, NewTransaction, NftMetadata, NodeVersion, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, RpcStandaloneAddresses, RpcTokenId, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TxOptionsOverrides, UtxoInfo, @@ -49,8 +52,8 @@ use wallet_rpc_lib::{ RpcError, WalletRpc, }; use wallet_types::{ - seed_phrase::StoreSeedPhrase, signature_status::SignatureStatus, utxo_types::UtxoTypes, - with_locked::WithLocked, + partially_signed_transaction::PartiallySignedTransaction, seed_phrase::StoreSeedPhrase, + signature_status::SignatureStatus, utxo_types::UtxoTypes, with_locked::WithLocked, }; use crate::wallet_rpc_traits::{ @@ -77,7 +80,10 @@ pub enum WalletRpcHandlesClientError { AddressError(#[from] AddressError), } -impl WalletRpcHandlesClient { +impl WalletRpcHandlesClient +where + N: NodeInterface + Clone + Send + Sync + 'static + Debug, +{ pub fn new(wallet_rpc: WalletRpc, server_rpc: Option) -> Self { Self { wallet_rpc, @@ -87,8 +93,9 @@ impl WalletRpcHandlesC } #[async_trait::async_trait] -impl WalletInterface - for WalletRpcHandlesClient +impl WalletInterface for WalletRpcHandlesClient +where + N: NodeInterface + Clone + Send + Sync + 'static + Debug, { type Error = WalletRpcHandlesClientError; @@ -115,25 +122,57 @@ impl WalletInterface } async fn create_wallet( + &self, + path: PathBuf, + wallet_args: WalletTypeArgs, + ) -> Result { + let scan_blockchain = false; + self.wallet_rpc + .create_wallet(path, wallet_args, false, scan_blockchain) + .await + .map(Into::into) + .map_err(WalletRpcHandlesClientError::WalletRpcError) + } + + async fn recover_wallet( &self, path: PathBuf, store_seed_phrase: bool, mnemonic: Option, passphrase: Option, + hardware_wallet: Option, ) -> Result { - let whether_to_store_seed_phrase = if store_seed_phrase { + let store_seed_phrase = if store_seed_phrase { StoreSeedPhrase::Store } else { StoreSeedPhrase::DoNotStore }; - self.wallet_rpc - .create_wallet( - path, - whether_to_store_seed_phrase, + + let args = match hardware_wallet { + None => WalletTypeArgs::Software { mnemonic, passphrase, - false, - ) + store_seed_phrase, + }, + #[cfg(feature = "trezor")] + Some(HardwareWalletType::Trezor) => { + ensure!( + mnemonic.is_none() + && passphrase.is_none() + && store_seed_phrase == StoreSeedPhrase::DoNotStore, + RpcError::HardwareWalletWithMnemonicOrPassphrase + ); + WalletTypeArgs::Trezor + } + #[cfg(not(feature = "trezor"))] + Some(_) => { + return Err(RpcError::::InvalidHardwareWallet)?; + } + }; + + let scan_blockchain = true; + self.wallet_rpc + .create_wallet(path, args, false, scan_blockchain) .await .map(Into::into) .map_err(WalletRpcHandlesClientError::WalletRpcError) @@ -144,9 +183,15 @@ impl WalletInterface path: PathBuf, password: Option, force_migrate_wallet_type: Option, + hardware_wallet: Option, ) -> Result<(), Self::Error> { self.wallet_rpc - .open_wallet(path, password, force_migrate_wallet_type.unwrap_or(false)) + .open_wallet( + path, + password, + force_migrate_wallet_type.unwrap_or(false), + hardware_wallet, + ) .await .map_err(WalletRpcHandlesClientError::WalletRpcError) } diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index 57543bea0f..08a3829079 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -24,9 +24,8 @@ use super::{ClientWalletRpc, WalletRpcError}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, partially_signed_transaction::PartiallySignedTransaction, - Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, - UtxoOutPoint, + block::timestamp::BlockTimestamp, Block, GenBlock, SignedTransaction, + SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, }; @@ -38,14 +37,17 @@ use serialization::DecodeAll; use utils_networking::IpOrSocketAddress; use wallet::account::TxInfo; use wallet_controller::{ - types::{Balances, CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo}, + types::{ + Balances, CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo, + WalletTypeArgs, + }, ConnectedPeer, ControllerConfig, UtxoState, UtxoType, }; use wallet_rpc_lib::{ types::{ AddressInfo, AddressWithUsageInfo, BlockInfo, ComposedTransaction, CreatedWallet, - DelegationInfo, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegation, NewOrder, - NewTransaction, NftMetadata, NodeVersion, PoolInfo, PublicKeyInfo, + DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegation, + NewOrder, NewTransaction, NftMetadata, NodeVersion, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, RpcInspectTransaction, RpcStandaloneAddresses, RpcTokenId, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TxOptionsOverrides, @@ -53,7 +55,9 @@ use wallet_rpc_lib::{ }, ColdWalletRpcClient, WalletRpcClient, }; -use wallet_types::with_locked::WithLocked; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, with_locked::WithLocked, +}; #[async_trait::async_trait] impl WalletInterface for ClientWalletRpc { @@ -80,18 +84,47 @@ impl WalletInterface for ClientWalletRpc { } async fn create_wallet( + &self, + path: PathBuf, + wallet_args: WalletTypeArgs, + ) -> Result { + let (mnemonic, passphrase, store_seed_phrase, hardware_wallet) = match wallet_args { + WalletTypeArgs::Software { + mnemonic, + passphrase, + store_seed_phrase, + } => (mnemonic, passphrase, store_seed_phrase.should_save(), None), + #[cfg(feature = "trezor")] + WalletTypeArgs::Trezor => (None, None, false, Some(HardwareWalletType::Trezor)), + }; + + ColdWalletRpcClient::create_wallet( + &self.http_client, + path.to_string_lossy().to_string(), + store_seed_phrase, + mnemonic, + passphrase, + hardware_wallet, + ) + .await + .map_err(WalletRpcError::ResponseError) + } + + async fn recover_wallet( &self, path: PathBuf, store_seed_phrase: bool, mnemonic: Option, passphrase: Option, + hardware_wallet: Option, ) -> Result { - ColdWalletRpcClient::create_wallet( + ColdWalletRpcClient::recover_wallet( &self.http_client, path.to_string_lossy().to_string(), store_seed_phrase, mnemonic, passphrase, + hardware_wallet, ) .await .map_err(WalletRpcError::ResponseError) @@ -102,12 +135,14 @@ impl WalletInterface for ClientWalletRpc { path: PathBuf, password: Option, force_migrate_wallet_type: Option, + hardware_wallet: Option, ) -> Result<(), Self::Error> { ColdWalletRpcClient::open_wallet( &self.http_client, path.to_string_lossy().to_string(), password, force_migrate_wallet_type, + hardware_wallet, ) .await .map_err(WalletRpcError::ResponseError) diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index 8dd0be2fe6..2ee41679a2 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -18,9 +18,8 @@ use std::{collections::BTreeMap, num::NonZeroUsize, path::PathBuf}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, partially_signed_transaction::PartiallySignedTransaction, - Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, - UtxoOutPoint, + block::timestamp::BlockTimestamp, Block, GenBlock, SignedTransaction, + SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, }; @@ -30,18 +29,22 @@ use serialization::hex_encoded::HexEncoded; use utils_networking::IpOrSocketAddress; use wallet::account::TxInfo; use wallet_controller::{ - types::{CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo}, + types::{ + CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo, WalletTypeArgs, + }, ConnectedPeer, ControllerConfig, UtxoState, UtxoType, }; use wallet_rpc_lib::types::{ AddressInfo, AddressWithUsageInfo, Balances, BlockInfo, ComposedTransaction, CreatedWallet, - DelegationInfo, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegation, NewOrder, - NewTransaction, NftMetadata, NodeVersion, PoolInfo, PublicKeyInfo, RpcHashedTimelockContract, - RpcInspectTransaction, RpcSignatureStatus, RpcStandaloneAddresses, RpcTokenId, - SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, + DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, NewAccountInfo, NewDelegation, + NewOrder, NewTransaction, NftMetadata, NodeVersion, PoolInfo, PublicKeyInfo, + RpcHashedTimelockContract, RpcInspectTransaction, RpcSignatureStatus, RpcStandaloneAddresses, + RpcTokenId, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }; -use wallet_types::with_locked::WithLocked; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, with_locked::WithLocked, +}; pub enum PartialOrSignedTx { Partial(PartiallySignedTransaction), @@ -67,11 +70,18 @@ pub trait WalletInterface { async fn rpc_completed(&self); async fn create_wallet( + &self, + path: PathBuf, + wallet_args: WalletTypeArgs, + ) -> Result; + + async fn recover_wallet( &self, path: PathBuf, store_seed_phrase: bool, mnemonic: Option, passphrase: Option, + hardware_wallet: Option, ) -> Result; async fn open_wallet( @@ -79,6 +89,7 @@ pub trait WalletInterface { path: PathBuf, password: Option, force_migrate_wallet_type: Option, + hardware_wallet: Option, ) -> Result<(), Self::Error>; async fn close_wallet(&self) -> Result<(), Self::Error>; diff --git a/wallet/wallet-rpc-daemon/Cargo.toml b/wallet/wallet-rpc-daemon/Cargo.toml index 332fda36d2..6270884661 100644 --- a/wallet/wallet-rpc-daemon/Cargo.toml +++ b/wallet/wallet-rpc-daemon/Cargo.toml @@ -23,3 +23,6 @@ tokio.workspace = true rpc-description = { path = "../../rpc/description" } expect-test.workspace = true + +[features] +trezor = ["wallet-rpc-lib/trezor"] diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index d7862a7dbb..859101fe45 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -2693,7 +2693,7 @@ string ### Method `wallet_create` -Create new wallet +Create a new wallet, this will skip scanning the blockchain Parameters: @@ -2707,6 +2707,46 @@ Parameters: "passphrase": EITHER OF 1) string 2) null, + "hardware_wallet": EITHER OF + 1) "Trezor" + 2) null, +} +``` + +Returns: +``` +{ "mnemonic": EITHER OF + 1) { "type": "UserProvided" } + 2) { + "type": "NewlyGenerated", + "content": { + "mnemonic": string, + "passphrase": EITHER OF + 1) string + 2) null, + }, + } } +``` + +### Method `wallet_recover` + +Recover new wallet, this will rescan the blockchain upon creation + + +Parameters: +``` +{ + "path": string, + "store_seed_phrase": bool, + "mnemonic": EITHER OF + 1) string + 2) null, + "passphrase": EITHER OF + 1) string + 2) null, + "hardware_wallet": EITHER OF + 1) "Trezor" + 2) null, } ``` @@ -2740,6 +2780,9 @@ Parameters: "force_migrate_wallet_type": EITHER OF 1) bool 2) null, + "hardware_wallet": EITHER OF + 1) "Trezor" + 2) null, } ``` diff --git a/wallet/wallet-rpc-lib/Cargo.toml b/wallet/wallet-rpc-lib/Cargo.toml index 27376d9c9b..bc20422508 100644 --- a/wallet/wallet-rpc-lib/Cargo.toml +++ b/wallet/wallet-rpc-lib/Cargo.toml @@ -22,6 +22,7 @@ utils = { path = "../../utils" } utils-networking = { path = "../../utils/networking" } wallet = { path = ".." } wallet-controller = { path = "../wallet-controller" } +wallet-storage = { path = "../storage" } wallet-types = { path = "../types" } p2p-types = { path = "../../p2p/types" } @@ -48,3 +49,6 @@ wallet-test-node = { path = "../wallet-test-node" } wallet-types = { path = "../types" } rstest.workspace = true + +[features] +trezor = ["wallet-types/trezor", "wallet/trezor", "wallet-controller/trezor"] diff --git a/wallet/wallet-rpc-lib/src/cmdline.rs b/wallet/wallet-rpc-lib/src/cmdline.rs index c5b677720c..0893d2d310 100644 --- a/wallet/wallet-rpc-lib/src/cmdline.rs +++ b/wallet/wallet-rpc-lib/src/cmdline.rs @@ -15,6 +15,7 @@ use std::path::PathBuf; +use clap::ValueEnum; use common::chain::config::{regtest_options::ChainConfigOptions, ChainType}; use crypto::key::hdkd::u31::U31; use rpc::{ @@ -26,7 +27,10 @@ use utils::{ }; use utils_networking::NetworkAddressWithPort; -use crate::config::{WalletRpcConfig, WalletServiceConfig}; +use crate::{ + config::{WalletRpcConfig, WalletServiceConfig}, + types::HardwareWalletType, +}; /// Service providing an RPC interface to a wallet #[derive(clap::Parser)] @@ -78,6 +82,21 @@ impl WalletRpcDaemonCommand { } } +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum CliHardwareWalletType { + #[cfg(feature = "trezor")] + Trezor, +} + +impl From for HardwareWalletType { + fn from(value: CliHardwareWalletType) -> Self { + match value { + #[cfg(feature = "trezor")] + CliHardwareWalletType::Trezor => Self::Trezor, + } + } +} + #[derive(clap::Args)] #[command( version, @@ -98,6 +117,10 @@ pub struct WalletRpcDaemonChainArgs { #[arg(long, requires("wallet_file"))] force_change_wallet_type: bool, + /// Specified if the wallet file is of a hardware wallet type e.g. Trezor + #[arg(long, requires("wallet_file"))] + hardware_wallet: Option, + /// Start staking for the specified account after starting the wallet #[arg(long, value_name("ACC_NUMBER"), requires("wallet_file"))] start_staking_for_account: Vec, @@ -161,6 +184,7 @@ impl WalletRpcDaemonChainArgs { let Self { wallet_file, force_change_wallet_type, + hardware_wallet, rpc_bind_address, start_staking_for_account, node_rpc_address, @@ -185,6 +209,7 @@ impl WalletRpcDaemonChainArgs { wallet_file, force_change_wallet_type, start_staking_for_account, + hardware_wallet.map(Into::into), ); if cold_wallet { diff --git a/wallet/wallet-rpc-lib/src/config.rs b/wallet/wallet-rpc-lib/src/config.rs index c2029d2673..6bf0e55d07 100644 --- a/wallet/wallet-rpc-lib/src/config.rs +++ b/wallet/wallet-rpc-lib/src/config.rs @@ -22,6 +22,8 @@ use common::chain::config::{ use crypto::key::hdkd::u31::U31; use rpc::{rpc_creds::RpcCreds, RpcAuthData}; +use crate::types::HardwareWalletType; + #[derive(Clone)] pub enum NodeRpc { ColdWallet, @@ -45,6 +47,9 @@ pub struct WalletServiceConfig { /// Force change the wallet type from hot to cold or from cold to hot pub force_change_wallet_type: bool, + /// Specified if the wallet file is of a hardware wallet type e.g. Trezor + pub hardware_wallet_type: Option, + /// Start staking for account after starting the wallet pub start_staking_for_account: Vec, @@ -58,6 +63,7 @@ impl WalletServiceConfig { wallet_file: Option, force_change_wallet_type: bool, start_staking_for_account: Vec, + hardware_wallet_type: Option, ) -> Self { Self { chain_config: Arc::new(common::chain::config::Builder::new(chain_type).build()), @@ -65,6 +71,7 @@ impl WalletServiceConfig { force_change_wallet_type, start_staking_for_account, node_rpc: NodeRpc::ColdWallet, + hardware_wallet_type, } } diff --git a/wallet/wallet-rpc-lib/src/lib.rs b/wallet/wallet-rpc-lib/src/lib.rs index ed94f27c3e..98affde076 100644 --- a/wallet/wallet-rpc-lib/src/lib.rs +++ b/wallet/wallet-rpc-lib/src/lib.rs @@ -18,15 +18,16 @@ pub mod config; mod rpc; mod service; +#[cfg(feature = "trezor")] +use rpc::types::HardwareWalletType; pub use rpc::{ types, ColdWalletRpcClient, ColdWalletRpcDescription, ColdWalletRpcServer, RpcCreds, RpcError, WalletEventsRpcServer, WalletRpc, WalletRpcClient, WalletRpcDescription, WalletRpcServer, }; -pub use service::{ - CreatedWallet, Event, EventStream, TxState, WalletHandle, - /* WalletResult, */ WalletService, -}; +pub use service::{Event, EventStream, TxState, WalletHandle, /* WalletResult, */ WalletService,}; use wallet_controller::{NodeInterface, NodeRpcClient}; +#[cfg(feature = "trezor")] +use wallet_types::wallet_type::WalletType; use std::{fmt::Debug, time::Duration}; @@ -99,12 +100,19 @@ pub async fn start_services( cold_wallet: bool, ) -> Result<(WalletService, rpc::Rpc), StartupError> where - N: NodeInterface + Clone + Sync + Send + Debug + 'static, + N: NodeInterface + Clone + Sync + Send + 'static + Debug, { + let wallet_type = wallet_config.hardware_wallet_type.map_or_else( + || node_rpc.is_cold_wallet_node().into(), + |hw| match hw { + #[cfg(feature = "trezor")] + HardwareWalletType::Trezor => WalletType::Trezor, + }, + ); // Start the wallet service let wallet_service = WalletService::start( wallet_config.chain_config, - wallet_config.wallet_file, + wallet_config.wallet_file.map(|file| (file, wallet_type)), wallet_config.force_change_wallet_type, wallet_config.start_staking_for_account, node_rpc, diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 058514e7c9..3a05f146b4 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -19,10 +19,9 @@ use chainstate::rpc::RpcOutputValueIn; use common::{ address::RpcAddress, chain::{ - block::timestamp::BlockTimestamp, tokens::TokenId, - transaction::partially_signed_transaction::PartiallySignedTransaction, Block, DelegationId, - Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, - Transaction, TxOutput, + block::timestamp::BlockTimestamp, tokens::TokenId, Block, DelegationId, Destination, + GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, + TxOutput, }, primitives::{BlockHeight, Id}, }; @@ -34,15 +33,17 @@ use wallet_controller::{ types::{BlockInfo, CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo}, ConnectedPeer, }; -use wallet_types::with_locked::WithLocked; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, with_locked::WithLocked, +}; use crate::types::{ AccountArg, AddressInfo, AddressWithUsageInfo, Balances, ChainInfo, ComposedTransaction, - CreatedWallet, DelegationInfo, HexEncoded, LegacyVrfPublicKeyInfo, MaybeSignedTransaction, - NewAccountInfo, NewDelegation, NewOrder, NewTransaction, NftMetadata, NodeVersion, PoolInfo, - PublicKeyInfo, RpcAmountIn, RpcHashedTimelockContract, RpcInspectTransaction, - RpcStandaloneAddresses, RpcTokenId, RpcUtxoOutpoint, RpcUtxoState, RpcUtxoType, - SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, + CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, LegacyVrfPublicKeyInfo, + MaybeSignedTransaction, NewAccountInfo, NewDelegation, NewOrder, NewTransaction, NftMetadata, + NodeVersion, PoolInfo, PublicKeyInfo, RpcAmountIn, RpcHashedTimelockContract, + RpcInspectTransaction, RpcStandaloneAddresses, RpcTokenId, RpcUtxoOutpoint, RpcUtxoState, + RpcUtxoType, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TxOptionsOverrides, VrfPublicKeyInfo, }; @@ -66,7 +67,7 @@ trait ColdWalletRpc { #[method(name = "version")] async fn version(&self) -> rpc::RpcResult; - /// Create new wallet + /// Create a new wallet, this will skip scanning the blockchain #[method(name = "wallet_create")] async fn create_wallet( &self, @@ -74,6 +75,18 @@ trait ColdWalletRpc { store_seed_phrase: bool, mnemonic: Option, passphrase: Option, + hardware_wallet: Option, + ) -> rpc::RpcResult; + + /// Recover new wallet, this will rescan the blockchain upon creation + #[method(name = "wallet_recover")] + async fn recover_wallet( + &self, + path: String, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, ) -> rpc::RpcResult; /// Open an exiting wallet by specifying the file location of the wallet file @@ -83,6 +96,7 @@ trait ColdWalletRpc { path: String, password: Option, force_migrate_wallet_type: Option, + hardware_wallet: Option, ) -> rpc::RpcResult<()>; /// Close the currently open wallet file diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index b0000b1853..4bb11e35be 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -39,6 +39,7 @@ use utils::{ensure, shallow_clone::ShallowClone}; use utils_networking::IpOrSocketAddress; use wallet::{ account::{transaction_list::TransactionList, PoolData, TransactionToSign, TxInfo}, + send_request::PoolOrTokenId, WalletError, }; @@ -49,11 +50,12 @@ use common::{ classic_multisig::ClassicMultisigChallenge, htlc::{HashedTimelockContract, HtlcSecret, HtlcSecretHash}, output_value::{OutputValue, RpcOutputValue}, - partially_signed_transaction::PartiallySignedTransaction, signature::inputsig::arbitrary_message::{ produce_message_challenge, ArbitraryMessageSignature, }, - tokens::{IsTokenFreezable, IsTokenUnfreezable, Metadata, TokenId, TokenTotalSupply}, + tokens::{ + IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCTokenInfo, TokenId, TokenTotalSupply, + }, Block, ChainConfig, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, @@ -68,26 +70,32 @@ pub use interface::{ pub use rpc::{rpc_creds::RpcCreds, Rpc}; use wallet_controller::{ types::{ - Balances, BlockInfo, CreatedBlockInfo, GenericTokenTransfer, InspectTransaction, - SeedWithPassPhrase, TransactionToInspect, WalletInfo, + Balances, BlockInfo, CreatedBlockInfo, CreatedWallet, GenericTokenTransfer, + InspectTransaction, SeedWithPassPhrase, TransactionToInspect, WalletInfo, WalletTypeArgs, }, ConnectedPeer, ControllerConfig, ControllerError, NodeInterface, UtxoState, UtxoStates, UtxoType, UtxoTypes, DEFAULT_ACCOUNT_INDEX, }; use wallet_types::{ - account_info::StandaloneAddressDetails, seed_phrase::StoreSeedPhrase, - signature_status::SignatureStatus, wallet_tx::TxData, with_locked::WithLocked, Currency, + account_info::StandaloneAddressDetails, + partially_signed_transaction::{ + PartiallySignedTransaction, TokenAdditionalInfo, UtxoAdditionalInfo, + }, + signature_status::SignatureStatus, + wallet_tx::TxData, + with_locked::WithLocked, + Currency, }; -use crate::{ - service::{CreatedWallet, WalletController}, - WalletHandle, WalletRpcConfig, -}; +use crate::{service::WalletController, WalletHandle, WalletRpcConfig}; + +#[cfg(feature = "trezor")] +use wallet_types::wallet_type::WalletType; pub use self::types::RpcError; use self::types::{ - AddressInfo, AddressWithUsageInfo, DelegationInfo, LegacyVrfPublicKeyInfo, NewAccountInfo, - NewTransaction, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, + AddressInfo, AddressWithUsageInfo, DelegationInfo, HardwareWalletType, LegacyVrfPublicKeyInfo, + NewAccountInfo, NewTransaction, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, RpcStandaloneAddress, RpcStandaloneAddressDetails, RpcStandaloneAddresses, RpcStandalonePrivateKeyAddress, RpcTokenId, RpcUtxoOutpoint, StakingStatus, StandaloneAddressWithDetails, VrfPublicKeyInfo, @@ -102,7 +110,10 @@ pub struct WalletRpc { type WRpcResult = Result>; -impl WalletRpc { +impl WalletRpc +where + N: NodeInterface + Clone + Send + Sync + 'static, +{ pub fn new(wallet: WalletHandle, node: N, chain_config: Arc) -> Self { Self { wallet, @@ -126,17 +137,14 @@ impl WalletRpc { pub async fn create_wallet( &self, path: PathBuf, - store_seed_phrase: StoreSeedPhrase, - mnemonic: Option, - passphrase: Option, + args: WalletTypeArgs, skip_syncing: bool, + scan_blockchain: bool, ) -> WRpcResult { self.wallet .manage_async(move |wallet_manager| { Box::pin(async move { - wallet_manager - .create_wallet(path, store_seed_phrase, mnemonic, passphrase, skip_syncing) - .await + wallet_manager.create_wallet(path, args, skip_syncing, scan_blockchain).await }) }) .await? @@ -147,13 +155,24 @@ impl WalletRpc { wallet_path: PathBuf, password: Option, force_migrate_wallet_type: bool, + open_as_hw_wallet: Option, ) -> WRpcResult<(), N> { + let open_as_wallet_type = + open_as_hw_wallet.map_or(self.node.is_cold_wallet_node().into(), |hw| match hw { + #[cfg(feature = "trezor")] + HardwareWalletType::Trezor => WalletType::Trezor, + }); Ok(self .wallet .manage_async(move |wallet_manager| { Box::pin(async move { wallet_manager - .open_wallet(wallet_path, password, force_migrate_wallet_type) + .open_wallet( + wallet_path, + password, + force_migrate_wallet_type, + open_as_wallet_type, + ) .await }) }) @@ -827,6 +846,7 @@ impl WalletRpc { .synced_controller(account_index, config) .await? .sign_raw_transaction(tx_to_sign) + .await .map_err(RpcError::Controller) }) }) @@ -854,6 +874,7 @@ impl WalletRpc { .synced_controller(account_index, config) .await? .sign_challenge(&challenge, &destination) + .await .map_err(RpcError::Controller) }) }) @@ -1214,6 +1235,7 @@ impl WalletRpc { let (tx, _, cur_signatures) = synced_controller .sign_raw_transaction(TransactionToSign::Partial(tx)) + .await .map_err(RpcError::Controller)?; Ok::<_, RpcError>((tx, cur_signatures, fees)) @@ -1480,12 +1502,20 @@ impl WalletRpc { self.wallet .call_async(move |controller| { Box::pin(async move { + let mut additional_utxo_infos = BTreeMap::new(); let value = match token_id { Some(token_id) => { let token_info = controller.get_token_info(token_id).await?; let amount = amount .to_amount(token_info.token_number_of_decimals()) .ok_or(RpcError::InvalidCoinAmount)?; + additional_utxo_infos.insert( + PoolOrTokenId::TokenId(token_id), + UtxoAdditionalInfo::TokenInfo(TokenAdditionalInfo { + num_decimals: token_info.token_number_of_decimals(), + ticker: token_info.token_ticker().to_vec(), + }), + ); OutputValue::TokenV1(token_id, amount) } None => { @@ -1499,7 +1529,7 @@ impl WalletRpc { controller .synced_controller(account_index, config) .await? - .create_htlc_tx(value, htlc) + .create_htlc_tx(value, htlc, &additional_utxo_infos) .await .map_err(RpcError::Controller) }) @@ -1512,19 +1542,19 @@ impl WalletRpc { currency: Currency, amount: RpcAmountIn, coin_decimals: u8, - ) -> Result> { + ) -> Result<(OutputValue, Option), RpcError> { match currency { Currency::Coin => { let amount = amount.to_amount(coin_decimals).ok_or(RpcError::::InvalidCoinAmount)?; - Ok::<_, RpcError>(OutputValue::Coin(amount)) + Ok::<_, RpcError>((OutputValue::Coin(amount), None)) } Currency::Token(token_id) => { let token_info = controller.get_token_info(token_id).await?; let amount = amount .to_amount(token_info.token_number_of_decimals()) .ok_or(RpcError::InvalidCoinAmount)?; - Ok(OutputValue::TokenV1(token_id, amount)) + Ok((OutputValue::TokenV1(token_id, amount), Some(token_info))) } } } @@ -1561,14 +1591,14 @@ impl WalletRpc { self.wallet .call_async(move |controller| { Box::pin(async move { - let ask_value = Self::convert_currency_to_output_value( + let (ask_value, ask_token_info) = Self::convert_currency_to_output_value( controller, ask_currency, ask_amount, coin_decimals, ) .await?; - let give_value = Self::convert_currency_to_output_value( + let (give_value, give_token_info) = Self::convert_currency_to_output_value( controller, give_currency, give_amount, @@ -1576,10 +1606,12 @@ impl WalletRpc { ) .await?; + let token_infos = ask_token_info.into_iter().chain(give_token_info).collect(); + controller .synced_controller(account_index, config) .await? - .create_order(ask_value, give_value, conclude_dest) + .create_order(ask_value, give_value, conclude_dest, token_infos) .await .map_err(RpcError::Controller) }) @@ -1611,9 +1643,23 @@ impl WalletRpc { Box::pin(async move { let order_info = w.get_order_info(order_id).await?; + let ask_token_info = + if let Some(token_id) = order_info.initially_asked.token_id() { + Some(w.get_token_info(token_id).await?) + } else { + None + }; + let give_token_info = + if let Some(token_id) = order_info.initially_given.token_id() { + Some(w.get_token_info(token_id).await?) + } else { + None + }; + let token_infos = ask_token_info.into_iter().chain(give_token_info).collect(); + w.synced_controller(account_index, config) .await? - .conclude_order(order_id, order_info, output_address) + .conclude_order(order_id, order_info, output_address, token_infos) .await .map_err(RpcError::Controller) .map(NewTransaction::new) @@ -1642,17 +1688,32 @@ impl WalletRpc { .call_async(move |w| { Box::pin(async move { let order_info = w.get_order_info(order_id).await?; - let fill_amount_in_ask_currency = match order_info.initially_asked { - RpcOutputValue::Coin { .. } => fill_amount_in_ask_currency - .to_amount(coin_decimals) - .ok_or(RpcError::::InvalidCoinAmount)?, - RpcOutputValue::Token { id, amount: _ } => { - let token_info = w.get_token_info(id).await?; - fill_amount_in_ask_currency - .to_amount(token_info.token_number_of_decimals()) - .ok_or(RpcError::InvalidCoinAmount)? - } - }; + let (fill_amount_in_ask_currency, ask_token_info) = + match order_info.initially_asked { + RpcOutputValue::Coin { .. } => ( + fill_amount_in_ask_currency + .to_amount(coin_decimals) + .ok_or(RpcError::::InvalidCoinAmount)?, + None, + ), + RpcOutputValue::Token { id, amount: _ } => { + let token_info = w.get_token_info(id).await?; + ( + fill_amount_in_ask_currency + .to_amount(token_info.token_number_of_decimals()) + .ok_or(RpcError::InvalidCoinAmount)?, + Some(token_info), + ) + } + }; + let give_token_info = + if let Some(token_id) = order_info.initially_given.token_id() { + Some(w.get_token_info(token_id).await?) + } else { + None + }; + + let token_infos = ask_token_info.into_iter().chain(give_token_info).collect(); w.synced_controller(account_index, config) .await? @@ -1661,6 +1722,7 @@ impl WalletRpc { order_info, fill_amount_in_ask_currency, output_address, + token_infos, ) .await .map_err(RpcError::Controller) @@ -2236,13 +2298,16 @@ impl WalletRpc { } } -pub async fn start( +pub async fn start( wallet_handle: WalletHandle, node_rpc: N, config: WalletRpcConfig, chain_config: Arc, cold_wallet: bool, -) -> anyhow::Result { +) -> anyhow::Result +where + N: NodeInterface + Clone + Send + Sync + 'static + Debug, +{ let WalletRpcConfig { bind_addr, auth_credentials, diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index 90a3525bb8..509ccb8300 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -20,7 +20,6 @@ use common::{ address::dehexify::dehexify_all_addresses, chain::{ block::timestamp::BlockTimestamp, - partially_signed_transaction::PartiallySignedTransaction, tokens::{IsTokenUnfreezable, TokenId}, Block, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, @@ -30,25 +29,30 @@ use common::{ use crypto::key::PrivateKey; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}; use serialization::{hex::HexEncode, json_encoded::JsonEncoded}; +use utils::ensure; use utils_networking::IpOrSocketAddress; use wallet::{account::TxInfo, version::get_version}; use wallet_controller::{ - types::{BlockInfo, CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo}, + types::{ + BlockInfo, CreatedBlockInfo, GenericTokenTransfer, SeedWithPassPhrase, WalletInfo, + WalletTypeArgs, + }, ConnectedPeer, ControllerConfig, NodeInterface, UtxoState, UtxoStates, UtxoType, UtxoTypes, }; use wallet_types::{ - seed_phrase::StoreSeedPhrase, signature_status::SignatureStatus, with_locked::WithLocked, + partially_signed_transaction::PartiallySignedTransaction, seed_phrase::StoreSeedPhrase, + signature_status::SignatureStatus, with_locked::WithLocked, }; use crate::{ rpc::{ColdWalletRpcServer, WalletEventsRpcServer, WalletRpc, WalletRpcServer}, types::{ AccountArg, AddressInfo, AddressWithUsageInfo, Balances, ChainInfo, ComposedTransaction, - CreatedWallet, DelegationInfo, HexEncoded, LegacyVrfPublicKeyInfo, MaybeSignedTransaction, - NewAccountInfo, NewDelegation, NewTransaction, NftMetadata, NodeVersion, PoolInfo, - PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, RpcInspectTransaction, - RpcStandaloneAddresses, RpcTokenId, RpcUtxoOutpoint, RpcUtxoState, RpcUtxoType, - SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, + CreatedWallet, DelegationInfo, HardwareWalletType, HexEncoded, LegacyVrfPublicKeyInfo, + MaybeSignedTransaction, NewAccountInfo, NewDelegation, NewTransaction, NftMetadata, + NodeVersion, PoolInfo, PublicKeyInfo, RpcAddress, RpcAmountIn, RpcHexString, + RpcInspectTransaction, RpcStandaloneAddresses, RpcTokenId, RpcUtxoOutpoint, RpcUtxoState, + RpcUtxoType, SendTokensFromMultisigAddressResult, StakePoolBalance, StakingStatus, StandaloneAddressWithDetails, TokenMetadata, TransactionOptions, TxOptionsOverrides, UtxoInfo, VrfPublicKeyInfo, }, @@ -58,8 +62,9 @@ use crate::{ use super::types::{NewOrder, RpcHashedTimelockContract}; #[async_trait::async_trait] -impl WalletEventsRpcServer - for WalletRpc +impl WalletEventsRpcServer for WalletRpc +where + N: NodeInterface + Clone + Send + Sync + 'static + Debug, { async fn subscribe_wallet_events( &self, @@ -71,8 +76,9 @@ impl WalletEventsRpcSe } #[async_trait::async_trait] -impl ColdWalletRpcServer - for WalletRpc +impl ColdWalletRpcServer for WalletRpc +where + N: NodeInterface + Clone + Send + Sync + 'static + Debug, { async fn shutdown(&self) -> rpc::RpcResult<()> { rpc::handle_result(self.shutdown()) @@ -88,22 +94,88 @@ impl ColdWalletRpcServ store_seed_phrase: bool, mnemonic: Option, passphrase: Option, + hardware_wallet: Option, ) -> rpc::RpcResult { - let whether_to_store_seed_phrase = if store_seed_phrase { + let store_seed_phrase = if store_seed_phrase { StoreSeedPhrase::Store } else { StoreSeedPhrase::DoNotStore }; - rpc::handle_result( - self.create_wallet( - path.into(), - whether_to_store_seed_phrase, + + let args = match hardware_wallet { + None => WalletTypeArgs::Software { mnemonic, passphrase, - false, - ) - .await - .map(Into::::into), + store_seed_phrase, + }, + #[cfg(feature = "trezor")] + Some(HardwareWalletType::Trezor) => { + ensure!( + mnemonic.is_none() + && passphrase.is_none() + && store_seed_phrase == StoreSeedPhrase::DoNotStore, + RpcError::::HardwareWalletWithMnemonicOrPassphrase + ); + WalletTypeArgs::Trezor + } + #[cfg(not(feature = "trezor"))] + Some(_) => { + return Err(RpcError::::InvalidHardwareWallet)?; + } + }; + + let scan_blockchain = false; + rpc::handle_result( + self.create_wallet(path.into(), args, false, scan_blockchain) + .await + .map(Into::::into), + ) + } + + async fn recover_wallet( + &self, + path: String, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, + ) -> rpc::RpcResult { + let store_seed_phrase = if store_seed_phrase { + StoreSeedPhrase::Store + } else { + StoreSeedPhrase::DoNotStore + }; + + let args = match hardware_wallet { + None => { + ensure!(mnemonic.is_some(), RpcError::::EmptyMnemonic); + WalletTypeArgs::Software { + mnemonic, + passphrase, + store_seed_phrase, + } + } + #[cfg(feature = "trezor")] + Some(HardwareWalletType::Trezor) => { + ensure!( + mnemonic.is_none() + && passphrase.is_none() + && store_seed_phrase == StoreSeedPhrase::DoNotStore, + RpcError::::HardwareWalletWithMnemonicOrPassphrase + ); + WalletTypeArgs::Trezor + } + #[cfg(not(feature = "trezor"))] + Some(_) => { + return Err(RpcError::::InvalidHardwareWallet)?; + } + }; + + let scan_blockchain = true; + rpc::handle_result( + self.create_wallet(path.into(), args, false, scan_blockchain) + .await + .map(Into::::into), ) } @@ -112,12 +184,14 @@ impl ColdWalletRpcServ path: String, password: Option, force_migrate_wallet_type: Option, + open_as_hw_wallet: Option, ) -> rpc::RpcResult<()> { rpc::handle_result( self.open_wallet( path.into(), password, force_migrate_wallet_type.unwrap_or(false), + open_as_hw_wallet, ) .await, ) @@ -304,7 +378,10 @@ impl ColdWalletRpcServ } #[async_trait::async_trait] -impl WalletRpcServer for WalletRpc { +impl WalletRpcServer for WalletRpc +where + N: NodeInterface + Clone + Send + Sync + 'static + Debug, +{ async fn rescan(&self) -> rpc::RpcResult<()> { rpc::handle_result(self.rescan().await) } diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index e0a079c546..00c75f6614 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -20,7 +20,6 @@ use common::{ chain::{ block::timestamp::BlockTimestamp, classic_multisig::ClassicMultisigChallengeError, - partially_signed_transaction::PartiallySignedTransaction, signature::DestinationSigError, timelock::OutputTimeLock, tokens::{self, IsTokenFreezable, Metadata, TokenCreator, TokenId}, @@ -58,7 +57,9 @@ pub use wallet_controller::types::{ }; pub use wallet_controller::{ControllerConfig, NodeInterface}; use wallet_controller::{UtxoState, UtxoType}; -use wallet_types::signature_status::SignatureStatus; +use wallet_types::{ + partially_signed_transaction::PartiallySignedTransaction, signature_status::SignatureStatus, +}; use crate::service::SubmitError; @@ -92,6 +93,15 @@ pub enum RpcError { #[error("Invalid mnemonic: {0}")] InvalidMnemonic(wallet_controller::mnemonic::Error), + #[error("Cannot recover a software wallet without providing a mnemonic")] + EmptyMnemonic, + + #[error("Cannot specify a mnemonic or passphrase when using a hardware wallet")] + HardwareWalletWithMnemonicOrPassphrase, + + #[error("Invalid hardware wallet selection")] + InvalidHardwareWallet, + #[error("Invalid ip address")] InvalidIpAddress, @@ -647,16 +657,19 @@ pub struct CreatedWallet { pub mnemonic: MnemonicInfo, } -impl From for CreatedWallet { - fn from(value: crate::CreatedWallet) -> Self { +impl From for CreatedWallet { + fn from(value: wallet_controller::types::CreatedWallet) -> Self { let mnemonic = match value { - crate::CreatedWallet::UserProvidedMnemonic => MnemonicInfo::UserProvided, - crate::CreatedWallet::NewlyGeneratedMnemonic(mnemonic, passphrase) => { - MnemonicInfo::NewlyGenerated { - mnemonic: mnemonic.to_string(), - passphrase, - } + wallet_controller::types::CreatedWallet::UserProvidedMnemonic => { + MnemonicInfo::UserProvided } + wallet_controller::types::CreatedWallet::NewlyGeneratedMnemonic( + mnemonic, + passphrase, + ) => MnemonicInfo::NewlyGenerated { + mnemonic: mnemonic.to_string(), + passphrase, + }, }; Self { mnemonic } } @@ -786,6 +799,12 @@ pub enum RpcCurrency { Token { token_id: RpcAddress }, } +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, HasValueHint)] +pub enum HardwareWalletType { + #[cfg(feature = "trezor")] + Trezor, +} + #[cfg(test)] mod test { use super::*; diff --git a/wallet/wallet-rpc-lib/src/service/handle.rs b/wallet/wallet-rpc-lib/src/service/handle.rs index 7caaa8ebd1..d90278326d 100644 --- a/wallet/wallet-rpc-lib/src/service/handle.rs +++ b/wallet/wallet-rpc-lib/src/service/handle.rs @@ -31,7 +31,10 @@ pub use crate::service::worker::EventStream; #[derive(Clone)] pub struct WalletHandle(worker::CommandSender); -impl WalletHandle { +impl WalletHandle +where + N: NodeInterface + Clone + Send + Sync + 'static, +{ /// Asynchronous wallet service call pub fn call_async> + Send + 'static>( &self, diff --git a/wallet/wallet-rpc-lib/src/service/mod.rs b/wallet/wallet-rpc-lib/src/service/mod.rs index 5eb343b4bc..e14b2b9774 100644 --- a/wallet/wallet-rpc-lib/src/service/mod.rs +++ b/wallet/wallet-rpc-lib/src/service/mod.rs @@ -26,7 +26,8 @@ use utils::shallow_clone::ShallowClone; pub use events::{Event, TxState}; pub use handle::{EventStream, SubmitError, WalletHandle}; use wallet_controller::{ControllerConfig, NodeInterface}; -pub use worker::{CreatedWallet, WalletController, WalletControllerError}; +use wallet_types::wallet_type::WalletType; +pub use worker::{WalletController, WalletControllerError}; use events::WalletServiceEvents; @@ -50,10 +51,13 @@ pub enum InitError { Controller(#[from] WalletControllerError), } -impl WalletService { +impl WalletService +where + N: NodeInterface + Clone + Send + Sync + 'static, +{ pub async fn start( chain_config: Arc, - wallet_file: Option, + wallet_file: Option<(PathBuf, WalletType)>, force_change_wallet_type: bool, start_staking_for_account: Vec, node_rpc: N, @@ -61,7 +65,7 @@ impl WalletService { let (wallet_events, events_rx) = WalletServiceEvents::new(); let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel(); - let controller = if let Some(wallet_file) = &wallet_file { + let controller = if let Some((wallet_file, open_as_wallet_type)) = &wallet_file { let wallet = { // TODO: Allow user to set password (config file only) let wallet_password = None; @@ -71,6 +75,7 @@ impl WalletService { wallet_password, node_rpc.is_cold_wallet_node(), force_change_wallet_type, + *open_as_wallet_type, )? }; diff --git a/wallet/wallet-rpc-lib/src/service/worker.rs b/wallet/wallet-rpc-lib/src/service/worker.rs index ec31ed6389..cc6597e541 100644 --- a/wallet/wallet-rpc-lib/src/service/worker.rs +++ b/wallet/wallet-rpc-lib/src/service/worker.rs @@ -21,9 +21,9 @@ use tokio::{sync::mpsc, task::JoinHandle}; use logging::log; use utils_networking::broadcaster::Broadcaster; -use wallet::wallet::Mnemonic; +use wallet_controller::types::{CreatedWallet, WalletTypeArgs}; use wallet_controller::{ControllerError, NodeInterface}; -use wallet_types::seed_phrase::StoreSeedPhrase; +use wallet_types::wallet_type::WalletType; use crate::types::RpcError; @@ -52,11 +52,6 @@ pub enum WalletCommand { Stop, } -pub enum CreatedWallet { - UserProvidedMnemonic, - NewlyGeneratedMnemonic(Mnemonic, Option), -} - /// Represents the wallet worker task. It handles external commands and keeps the wallet in sync. pub struct WalletWorker { controller: Option>, @@ -68,7 +63,10 @@ pub struct WalletWorker { wallet_events: WalletServiceEvents, } -impl WalletWorker { +impl WalletWorker +where + N: NodeInterface + Clone + Send + Sync + 'static, +{ fn new( controller: Option>, chain_config: Arc, @@ -89,25 +87,6 @@ impl WalletWorker { } } - pub fn spawn( - controller: Option>, - chain_config: Arc, - node_rpc: N, - command_rx: CommandReceiver, - events_rx: mpsc::UnboundedReceiver, - wallet_events: WalletServiceEvents, - ) -> JoinHandle<()> { - let worker = Self::new( - controller, - chain_config, - node_rpc, - command_rx, - events_rx, - wallet_events, - ); - tokio::spawn(worker.event_loop()) - } - async fn event_loop(mut self) { loop { tokio::select! { @@ -173,6 +152,7 @@ impl WalletWorker { wallet_path: PathBuf, password: Option, force_migrate_wallet_type: bool, + open_as_wallet_type: WalletType, ) -> Result<(), ControllerError> { utils::ensure!( self.controller.is_none(), @@ -185,6 +165,7 @@ impl WalletWorker { password, self.node_rpc.is_cold_wallet_node(), force_migrate_wallet_type, + open_as_wallet_type, )?; let controller = WalletController::new( @@ -202,64 +183,58 @@ impl WalletWorker { pub async fn create_wallet( &mut self, wallet_path: PathBuf, - whether_to_store_seed_phrase: StoreSeedPhrase, - mnemonic: Option, - passphrase: Option, + args: WalletTypeArgs, skip_syncing: bool, + scan_blockchain: bool, ) -> Result> { utils::ensure!( self.controller.is_none(), ControllerError::WalletFileAlreadyOpen ); - // TODO: Support other languages - let language = wallet::wallet::Language::English; - let newly_generated_mnemonic = mnemonic.is_none(); - let mnemonic = match &mnemonic { - Some(mnemonic) => wallet_controller::mnemonic::parse_mnemonic(language, mnemonic) - .map_err(RpcError::InvalidMnemonic)?, - None => wallet_controller::mnemonic::generate_new_mnemonic(language), - }; - let passphrase_ref = passphrase.as_ref().map(|x| x.as_ref()); + let wallet_type = args.wallet_type(self.node_rpc.is_cold_wallet_node()); + let (computed_args, wallet_created) = + args.parse_or_generate_mnemonic_if_needed().map_err(RpcError::InvalidMnemonic)?; - let wallet = if newly_generated_mnemonic || skip_syncing { - let info = self.node_rpc.chainstate_info().await.map_err(RpcError::RpcError)?; - WalletController::create_wallet( + let wallet = if scan_blockchain { + WalletController::recover_wallet( self.chain_config.clone(), wallet_path, - mnemonic.clone(), - passphrase_ref, - whether_to_store_seed_phrase, - (info.best_block_height, info.best_block_id), - self.node_rpc.is_cold_wallet_node(), + computed_args, + wallet_type, ) } else { - WalletController::recover_wallet( + let info = self.node_rpc.chainstate_info().await.map_err(RpcError::RpcError)?; + WalletController::create_wallet( self.chain_config.clone(), wallet_path, - mnemonic.clone(), - passphrase_ref, - whether_to_store_seed_phrase, - self.node_rpc.is_cold_wallet_node(), + computed_args, + (info.best_block_height, info.best_block_id), + wallet_type, ) } .map_err(RpcError::Controller)?; - let controller = WalletController::new( - self.chain_config.clone(), - self.node_rpc.clone(), - wallet, - self.wallet_events.clone(), - ) - .await - .map_err(RpcError::Controller)?; + let controller = if skip_syncing { + WalletController::new_unsynced( + self.chain_config.clone(), + self.node_rpc.clone(), + wallet, + self.wallet_events.clone(), + ) + } else { + WalletController::new( + self.chain_config.clone(), + self.node_rpc.clone(), + wallet, + self.wallet_events.clone(), + ) + .await + .map_err(RpcError::Controller)? + }; self.controller.replace(controller); - let result = match newly_generated_mnemonic { - true => CreatedWallet::NewlyGeneratedMnemonic(mnemonic, passphrase), - false => CreatedWallet::UserProvidedMnemonic, - }; - Ok(result) + Ok(wallet_created) } pub fn subscribe(&mut self) -> EventStream { @@ -275,3 +250,28 @@ impl WalletWorker { } } } + +impl WalletWorker +where + N: NodeInterface + Clone + Send + Sync + 'static, +{ + pub fn spawn( + controller: Option>, + chain_config: Arc, + node_rpc: N, + command_rx: CommandReceiver, + events_rx: mpsc::UnboundedReceiver, + wallet_events: WalletServiceEvents, + ) -> JoinHandle<()> { + let worker = WalletWorker::new( + controller, + chain_config, + node_rpc, + command_rx, + events_rx, + wallet_events, + ); + + tokio::spawn(worker.event_loop()) + } +} diff --git a/wallet/wallet-rpc-lib/tests/utils.rs b/wallet/wallet-rpc-lib/tests/utils.rs index 6a8cafe418..d631f9c852 100644 --- a/wallet/wallet-rpc-lib/tests/utils.rs +++ b/wallet/wallet-rpc-lib/tests/utils.rs @@ -25,6 +25,7 @@ use common::{ }; use rpc::RpcAuthData; use test_utils::{test_dir::TestRoot, test_root}; +use wallet::signer::software_signer::SoftwareSignerProvider; use wallet_controller::NodeRpcClient; use wallet_rpc_lib::{config::WalletServiceConfig, types::AccountArg, WalletHandle, WalletService}; use wallet_test_node::{RPC_PASSWORD, RPC_USERNAME}; @@ -33,7 +34,7 @@ pub use randomness::Rng; pub use rpc::test_support::{ClientT, Subscription, SubscriptionClientT}; pub use serde_json::Value as JsonValue; pub use test_utils::random::{make_seedable_rng, Seed}; -use wallet_types::wallet_type::WalletType; +use wallet_types::{seed_phrase::StoreSeedPhrase, wallet_type::WalletType}; pub const ACCOUNT0_ARG: AccountArg = AccountArg(0); pub const ACCOUNT1_ARG: AccountArg = AccountArg(1); @@ -67,11 +68,17 @@ impl TestFramework { let _wallet = wallet::Wallet::create_new_wallet( Arc::clone(&chain_config), db, - wallet_test_node::MNEMONIC, - None, - wallet_types::seed_phrase::StoreSeedPhrase::DoNotStore, (BlockHeight::new(0), chain_config.genesis_block_id()), WalletType::Hot, + |db_tx| { + Ok(SoftwareSignerProvider::new_from_mnemonic( + chain_config.clone(), + db_tx, + wallet_test_node::MNEMONIC, + None, + StoreSeedPhrase::DoNotStore, + )?) + }, ) .unwrap(); @@ -103,10 +110,11 @@ impl TestFramework { // Start the wallet service let (wallet_service, rpc_server) = { - let ws_config = WalletServiceConfig::new(chain_type, Some(wallet_path), false, vec![]) - .with_regtest_options(chain_config_options) - .unwrap() - .with_custom_chain_config(chain_config.clone()); + let ws_config = + WalletServiceConfig::new(chain_type, Some(wallet_path), false, vec![], None) + .with_regtest_options(chain_config_options) + .unwrap() + .with_custom_chain_config(chain_config.clone()); let bind_addr = "127.0.0.1:0".parse().unwrap(); let rpc_config = wallet_rpc_lib::config::WalletRpcConfig { bind_addr,