diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf95062c..ec889b257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog +* Added swap transactions and example flows on integration tests. +* Flatten the CLI subcommand tree. +* Added a mechanism to retrieve MMR data whenever a note created on a past block is imported. +* Changed the way notes are added to the database based on `ExecutedTransaction`. * Added more feedback information to commands `info`, `notes list`, `notes show`, `account new`, `notes import`, `tx new` and `sync`. * Add `consumer_account_id` to `InputNoteRecord` with an implementation for sqlite store. -* Renamed the cli `input-notes` command to `notes`. Now we only export notes that were created on this client as the result of a transaction. +* Renamed the CLI `input-notes` command to `notes`. Now we only export notes that were created on this client as the result of a transaction. * Added validation using the `NoteScreener` to see if a block has relevant notes. * Added flags to `init` command for non-interactive environments * Added an option to verify note existence in the chain before importing. diff --git a/Cargo.toml b/Cargo.toml index e0f9c031a..c01a58be3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,10 @@ clap = { version = "4.3", features = ["derive"] } comfy-table = "7.1.0" figment = { version = "0.10", features = ["toml", "env"] } lazy_static = "1.4.0" -miden-lib = { version = "0.2", default-features = false } -miden-node-proto = { version = "0.2", default-features = false } -miden-tx = { version = "0.2", default-features = false } -miden-objects = { version = "0.2", features = ["serde"] } +miden-lib = { version = "0.3", default-features = false } +miden-node-proto = { version = "0.3", default-features = false } +miden-tx = { version = "0.3", default-features = false } +miden-objects = { version = "0.3", default-features = false, features = ["serde"] } rand = { version = "0.8.5" } rusqlite = { version = "0.30.0", features = ["vtab", "array", "bundled"] } rusqlite_migration = { version = "1.0" } diff --git a/Makefile.toml b/Makefile.toml index 4c88b8fed..e832fc42a 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -74,17 +74,17 @@ args = ["-rf", "miden-node"] description = "Clone or update miden-node repository and clean up files" script_runner = "bash" script = [ - 'if [ -d miden-node ]; then cd miden-node && git checkout main; else git clone https://github.com/0xPolygonMiden/miden-node.git && cd miden-node && git checkout main; fi', + 'if [ -d miden-node ]; then cd miden-node ; else git clone https://github.com/0xPolygonMiden/miden-node.git && cd miden-node; fi', + 'git checkout main && git pull origin main && cargo update', 'rm -rf miden-store.sqlite3 miden-store.sqlite3-wal miden-store.sqlite3-shm', - 'cd bin/node', - 'cargo run --features $NODE_FEATURES_TESTING -- make-genesis --force' + 'cargo run --bin miden-node --features $NODE_FEATURES_TESTING -- make-genesis --inputs-path ../tests/config/genesis.toml --force', ] [tasks.start-node] description = "Start the miden-node" script_runner = "bash" -cwd = "./miden-node/bin/node" -script = "cargo run --features ${NODE_FEATURES_TESTING} -- start node" +cwd = "./miden-node" +script = "cargo run --bin miden-node --features $NODE_FEATURES_TESTING -- start --config ../tests/config/miden-node.toml node" [tasks.docs-deps] description = "Install documentation dependencies" diff --git a/README.md b/README.md index eb03d54fa..7a96d2b76 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [](https://github.com/0xPolygonMiden/miden-client/blob/main/LICENSE) [](https://github.com/0xPolygonMiden/miden-clinet/actions/workflows/ci.yml) -[]() +[]() [](https://crates.io/crates/miden-client) This repository contains the Miden client, which provides a way to execute and prove transactions, facilitating the interaction with the Miden rollup. @@ -31,14 +31,14 @@ For more info check: Before you can use the Miden client, you'll need to make sure you have both [Rust](https://www.rust-lang.org/tools/install) and sqlite3 installed. Miden -client v0.2 requires rust version **1.77** or higher. +client requires rust version **1.78** or higher. ### Adding miden-client as a dependency In order to utilize the `miden-client` library, you can add the dependency to your project's `Cargo.toml` file: ````toml -miden-client = { version = "0.2" } +miden-client = { version = "0.3" } ```` #### Features diff --git a/docs/cli-config.md b/docs/cli-config.md index f4ba8d8b0..3f02f321c 100644 --- a/docs/cli-config.md +++ b/docs/cli-config.md @@ -44,8 +44,14 @@ transactions against it when the account flag is not provided. By default none is set, but you can set and unset it with: ```sh -miden account default set <ACCOUNT_ID>` -miden account default unset +miden account --default <ACCOUNT_ID> #Sets default account +miden account --default none #Unsets default account +``` + +You can also see the current default account ID with: + +```sh +miden account --default ``` ### Environment variables diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 949e73e67..bd96a4663 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -2,7 +2,7 @@ comments: true --- -The following document lists the commands that the CLI currently supports. +The following document lists the commands that the CLI currently supports. !!! note Use `--help` as a flag on any command for more information. @@ -12,13 +12,13 @@ The following document lists the commands that the CLI currently supports. Call a command on the `miden-client` like this: ```sh -miden <command> <sub-command> +miden <command> <flags> <arguments> ``` Optionally, you can include the `--debug` flag to run the command with debug mode, which enables debug output logs from scripts that were compiled in this mode: ```sh -miden --debug <command> <sub-command> +miden --debug <flags> <arguments> ``` Note that the debug flag overrides the `MIDEN_DEBUG` environment variable. @@ -51,131 +51,165 @@ miden init --rpc testnet.miden.io --store_path db/store.sqlite3 ### `account` -Create accounts and inspect account details. +Inspect account details. -#### Sub-commands +#### Action Flags -| Sub-command | Description | Aliases | -|---------|-----------------------------------------------------|---------| -| `list` | List all accounts monitored by this client | -l | -| `show` | Show details of the account for the specified ID | -s | -| `new <ACCOUNT TYPE>` | Create new account and store it locally | -n | -| `import` | Import accounts from binary files | -i | -| `default` | Manage the setting for the default account | -d | +| Flags | Description | Short Flag| +|-----------------|-----------------------------------------------------|-----------| +|`--list` | List all accounts monitored by this client | `-l` | +|`--show <ID>` | Show details of the account for the specified ID | `-s` | +|`--default <ID>` | Manage the setting for the default account | `-d` | -After creating an account with the `new` command, it is automatically stored and tracked by the client. This means the client can execute transactions that modify the state of accounts and track related changes by synchronizing with the Miden node. - -The `show` subcommand also accepts a partial ID instead of the full ID. For example, instead of: +The `--show` flag also accepts a partial ID instead of the full ID. For example, instead of: ```sh -miden account show 0x8fd4b86a6387f8d8 +miden account --show 0x8fd4b86a6387f8d8 ``` You can call: ```sh -miden account show 0x8fd4b86 +miden account --show 0x8fd4b86 ``` +For the `--default` flag, if `<ID>` is "none" then the previous default account is cleared. If no `<ID>` is specified then the default account is shown. + +### `new-wallet` + +Creates a new wallet account. + +This command has two optional flags: +- `--storage-type <TYPE>`: Used to select the storage type of the account (off-chain if not specified). It may receive "off-chain" or "on-chain". +- `--mutable`: Makes the account code mutable (it's immutable by default). + +After creating an account with the `new-wallet` command, it is automatically stored and tracked by the client. This means the client can execute transactions that modify the state of accounts and track related changes by synchronizing with the Miden node. + +### `new-faucet` + +Creates a new faucet account. + +This command has two optional flags: +- `--storage-type <type>`: Used to select the storage type of the account (off-chain if not specified). It may receive "off-chain" or "on-chain". +- `--non-fungible`: Makes the faucet asset non-fungible (it's fungible by default). + +After creating an account with the `new-faucet` command, it is automatically stored and tracked by the client. This means the client can execute transactions that modify the state of accounts and track related changes by synchronizing with the Miden node. + ### `info` View a summary of the current client state. -### `input-notes` +### `notes` -View and manage input notes. +View and manage notes. -#### Sub-commands +#### Action Flags -| Command | Description | Aliases | -|-------------------|-------------------------------------------------------------|---------| -| `list` | List input notes | -l | -| `show` | Show details of the input note for the specified note ID | -s | -| `export` | Export input note data to a binary file | -e | -| `import` | Import input note data from a binary file | -i | -| `list-consumables`| List consumable notes by tracked accounts | -c | +| Flags | Description | Short Flag | +|-------------------|-------------------------------------------------------------|------------| +|`--list [<filter>]`| List input notes | `-l` | +| `--show <ID>` | Show details of the input note for the specified note ID | `-s` | -The `show` subcommand also accepts a partial ID instead of the full ID. For example, instead of: +The `--list` flag receives an optional filter: + - pending: Only lists pending notes. + - commited: Only lists commited notes. + - consumed: Only lists consumed notes. + - consumable: Only lists consumable notes. An additional `--account-id <ID>` flag may be added to only show notes consumable by the specified account. +If no filter is specified then all notes are listed. + +The `--show` flag also accepts a partial ID instead of the full ID. For example, instead of: ```sh -miden input-notes show 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 +miden notes --show 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 ``` You can call: ```sh -miden input-notes show 0x70b7ec +miden notes --show 0x70b7ec ``` -The `import` subcommand verifies that the note that is about to be imported exists on chain. The user can add an optional flag `--no-verify` that skips this verification. - ### `sync` -Sync the client with the latest state of the Miden network. +Sync the client with the latest state of the Miden network. Shows a brief summary at the end. ### `tags` View and add tags. -#### Sub-commands +#### Action Flags -| Command | Description | Aliases | -|---------|----------------------------------------------------------|---------| -| `list` | List all tags monitored by this client | -l | -| `add` | Add a new tag to the list of tags monitored by this client | -a | -| `remove` | Remove a tag from the list of tags monitored by this client | -r | +| Flag | Description | Aliases | +|-----------------|-------------------------------------------------------------|---------| +| `--list` | List all tags monitored by this client | `-l` | +| `--add <tag>` | Add a new tag to the list of tags monitored by this client | `-a` | +| `--remove <tag>`| Remove a tag from the list of tags monitored by this client | `-r` | -### `tx` or `transaction` +### `tx` -Execute and view transactions. +View transactions. -#### Sub-commands +#### Action Flags | Command | Description | Aliases | |---------|----------------------------------------------------------|---------| -| `list` | List tracked transactions | -l | -| `new <TX TYPE>` | Execute a transaction, prove and submit it to the node. Once submitted, it gets tracked by the client. | -n | +| `--list`| List tracked transactions | -l | After a transaction gets executed, two entities start being tracked: - The transaction itself: It follows a lifecycle from `pending` (initial state) and `committed` (after the node receives it). - Output notes that might have been created as part of the transaction (for example, when executing a pay-to-id transaction). -#### Types of transaction +### Transaction creation commands +#### `mint` + +Creates a note that contains a specific amount tokens minted by a faucet, that the target Account ID can consume. -| Command | Explanation | -|-----------------|-------------------------------------------------------------------------------------------------------------------| -| `p2id --sender <SENDER ACCOUNT ID> --target <TARGET ACCOUNT ID> --faucet <FAUCET ID> <AMOUNT> --note-type <NOTE_TYPE>` | Pay-to-id transaction. Sender Account creates a note that a target Account ID can consume. The asset is identifed by the tuple `(FAUCET ID, AMOUNT)`. | -| `p2idr --sender <SENDER ACCOUNT ID> --target <TARGET ACCOUNT ID> --faucet <FAUCET ID> <AMOUNT> <RECALL_HEIGHT> --note-type <NOTE_TYPE>` | Pay-to-id With Recall transaction. Sender Account creates a note that a target Account ID can consume, but the Sender will also be able to consume it after `<RECALL_HEIGHT>` is reached. The asset is identifed by the tuple `(FAUCET ID, AMOUNT)`. | -| `mint --target <TARGET ACCOUNT ID> --faucet <FAUCET ID> <AMOUNT> --note-type <NOTE_TYPE>` | Creates a note that contains a specific amount tokens minted by a faucet, that the target Account ID can consume| -| `consume-notes --account <ACCOUNT ID> [NOTES]` | Account ID consumes a list of notes, specified by their Note ID | +Usage: `miden mint --target <TARGET ACCOUNT ID> --faucet <FAUCET ID> <AMOUNT> --note-type <NOTE_TYPE>` -`<NOTE_TYPE>` can be either `public` or `private`. +#### `consume-notes` -For `consume-notes` subcommand, you can also provide a partial ID instead of the full ID for each note. So instead of +Account ID consumes a list of notes, specified by their Note ID. + +Usage: `miden consume-notes --account <ACCOUNT ID> [NOTES]` + +For this command, you can also provide a partial ID instead of the full ID for each note. So instead of ```sh -miden tx new consume-notes --account <some-account-id> 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 0x80b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 -``` +miden consume-notes --account <some-account-id> 0x70b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 0x80b7ecba1db44c3aa75e87a3394de95463cc094d7794b706e02a9228342faeb0 +``` -You can do: +You can do: ```sh -miden tx new consume-notes --account <some-account-id> 0x70b7ecb 0x80b7ecb +miden consume-notes --account <some-account-id> 0x70b7ecb 0x80b7ecb ``` -Also, for `p2id`, `p2idr` and `consume-notes`, you can omit the `--sender` and `--account` flags to use the default account defined in the [config](./cli-config.md). If you omit the flag but have no default account defined in the config, you'll get an error instead. +#### `send` + +Sends assets to another account. Sender Account creates a note that a target Account ID can consume. The asset is identifed by the tuple `(FAUCET ID, AMOUNT)`. The note can be configured to be recallable making the sender able to consume it after a height is reached. + +Usage: `miden send --sender <SENDER ACCOUNT ID> --target <TARGET ACCOUNT ID> --faucet <FAUCET ID> --note-type <NOTE_TYPE> <AMOUNT> <RECALL_HEIGHT>` + +#### `swap` + +The source account creates a Swap note that offers some asset in exchange for some other asset. When another account consumes that note, it'll receive the offered amount and it'll have the requested amount removed from its assets (and put into a new note which the first account can then consume). Consuming the note will fail if the account doesn't have enough of the requested asset. + +Usage: `miden swap --source <SOURCE ACCOUNT ID> --offered_faucet <OFFERED FAUCET ID> --offered_amount <OFFERED AMOUNT> --requested_faucet <REQUESTED FAUCET ID> --requested_amount <REQUESTED AMOUNT> --note-type <NOTE_TYPE>` + +#### Tips +For `send` and `consume-notes`, you can omit the `--sender` and `--account` flags to use the default account defined in the [config](./cli-config.md). If you omit the flag but have no default account defined in the config, you'll get an error instead. For every command which needs an account ID (either wallet or faucet), you can also provide a partial ID instead of the full ID for each account. So instead of ```sh -miden tx new p2id --sender 0x80519a1c5e3680fc --target 0x8fd4b86a6387f8d8 --faucet 0xa99c5c8764d4e011 100 +miden send --sender 0x80519a1c5e3680fc --target 0x8fd4b86a6387f8d8 --faucet 0xa99c5c8764d4e011 100 ``` You can do: ```sh -miden tx new p2id --sender 0x80519 --target 0x8fd4b --faucet 0xa99c5 100 +miden send --sender 0x80519 --target 0x8fd4b --faucet 0xa99c5 100 ``` #### Transaction confirmation @@ -192,4 +226,14 @@ TX Summary: Continue with proving and submission? Changes will be irreversible once the proof is finalized on the rollup (Y/N) ``` -This confirmation can be skipped in non-interactive environments by providing the `--force` flag (`miden tx new --force ...`): +This confirmation can be skipped in non-interactive environments by providing the `--force` flag (`miden send --force ...`): + +### `import` + +Import entities managed by the client, such as accounts and notes. The type of entitie is inferred. + +When importing notes the CLI verifies that they exist on chain. The user can add an optional flag `--no-verify` that skips this verification. + +### `export` + +Export input note data to a binary file . diff --git a/docs/install-and-run.md b/docs/install-and-run.md index bd0a52cf7..77f63f6f7 100644 --- a/docs/install-and-run.md +++ b/docs/install-and-run.md @@ -4,7 +4,7 @@ comments: true ## Software prerequisites -- [Rust installation](https://www.rust-lang.org/tools/install) minimum version 1.77. +- [Rust installation](https://www.rust-lang.org/tools/install) minimum version 1.78. ## Install the client diff --git a/docs/library.md b/docs/library.md index 031048465..53bd1488f 100644 --- a/docs/library.md +++ b/docs/library.md @@ -27,15 +27,19 @@ The current supported store is the `SqliteDataStore`, which is a SQLite implemen ```rust let client: Client<TonicRpcClient, SqliteDataStore> = { - let store = Store::new((&client_config).into()).map_err(ClientError::StoreError)?; - - Client::new( - - client_config, - TonicRpcClient::new(&rpc_endpoint), - SqliteDataStore::new(store), - - )? + let store = SqliteStore::new((&client_config).into()).map_err(ClientError::StoreError)?; + let store = Rc::new(store); + + let rng = miden_client::get_random_coin(); + let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng); + + let client = Client::new( + TonicRpcClient::new(&client_config.rpc), + rng, + store, + authenticator, + false, // set to true if you want a client with debug mode + ) }; ``` diff --git a/rust-toolchain b/rust-toolchain index 3245dca3d..8e95c75da 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.77 +1.78 diff --git a/src/cli/account.rs b/src/cli/account.rs index 6d0853c89..c83703692 100644 --- a/src/cli/account.rs +++ b/src/cli/account.rs @@ -1,192 +1,103 @@ use std::path::PathBuf; -use clap::{Parser, ValueEnum}; +use clap::Parser; use comfy_table::{presets, Attribute, Cell, ContentArrangement, Table}; use miden_client::{ - client::{accounts, rpc::NodeRpcClient, Client}, + client::{rpc::NodeRpcClient, Client}, config::{CliConfig, ClientConfig}, store::Store, }; use miden_objects::{ - accounts::{AccountId, AccountStorage, AccountType, StorageSlotType}, - assets::{Asset, TokenSymbol}, + accounts::{AccountId, AccountStorage, AccountType, AuthSecretKey, StorageSlotType}, + assets::Asset, crypto::{dsa::rpo_falcon512::SK_LEN, rand::FeltRng}, ZERO, }; -use miden_tx::utils::{bytes_to_hex_string, Serializable}; +use miden_tx::{ + utils::{bytes_to_hex_string, Serializable}, + TransactionAuthenticator, +}; use super::{load_config, parse_account_id, update_config, CLIENT_CONFIG_FILE_NAME}; -use crate::cli::{create_dynamic_table, CLIENT_BINARY_NAME}; +use crate::cli::create_dynamic_table; // ACCOUNT COMMAND // ================================================================================================ #[derive(Default, Debug, Clone, Parser)] -pub enum AccountCmd { - /// List all accounts monitored by this client - #[default] - #[clap(short_flag = 'l', long_flag = "list")] - List, +/// View and manage accounts. Defaults to `list` command. +pub struct AccountCmd { + /// List all accounts monitored by this client (default action) + #[clap(short, long, group = "action")] + list: bool, /// Show details of the account for the specified ID or hex prefix - #[clap(short_flag = 's', long_flag = "show")] - Show { id: String }, - /// Create new account and store it locally - #[clap(short_flag = 'n', long_flag = "new")] - New { - #[clap(subcommand)] - template: AccountTemplate, - }, - /// Set/Unset default accounts for transaction execution - #[clap(short_flag = 'd', long_flag = "default")] - Default { - #[clap(subcommand)] - default_cmd: DefaultAccountCmd, - }, -} - -#[derive(Debug, Parser, Clone)] -#[clap()] -pub enum DefaultAccountCmd { - /// Turn an account into the default sender account - Set { - #[clap()] - id: String, - }, - /// Show current default account - Show, - /// Clear the default account setting - Unset, -} - -#[derive(Debug, Parser, Clone)] -#[clap()] -pub enum AccountTemplate { - /// Creates a basic account (Regular account with immutable code) - BasicImmutable { - #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] - storage_type: AccountStorageMode, - }, - /// Creates a basic account (Regular account with mutable code) - BasicMutable { - #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] - storage_type: AccountStorageMode, - }, - /// Creates a faucet for fungible tokens - FungibleFaucet { - #[clap(short, long)] - token_symbol: String, - #[clap(short, long)] - decimals: u8, - #[clap(short, long)] - max_supply: u64, - #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] - storage_type: AccountStorageMode, - }, - /// Creates a faucet for non-fungible tokens - NonFungibleFaucet { - #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] - storage_type: AccountStorageMode, - }, -} - -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum AccountStorageMode { - OffChain, - OnChain, -} - -impl From<AccountStorageMode> for accounts::AccountStorageMode { - fn from(value: AccountStorageMode) -> Self { - match value { - AccountStorageMode::OffChain => accounts::AccountStorageMode::Local, - AccountStorageMode::OnChain => accounts::AccountStorageMode::OnChain, - } - } -} - -impl From<&AccountStorageMode> for accounts::AccountStorageMode { - fn from(value: &AccountStorageMode) -> Self { - accounts::AccountStorageMode::from(*value) - } + #[clap(short, long, group = "action", value_name = "ID")] + show: Option<String>, + /// Manages default account for transaction execution + /// + /// If no ID is provided it will display the current default account ID. + /// If "none" is provided it will remove the default account else + /// it will set the default account to the provided ID + #[clap(short, long, group = "action", value_name = "ID")] + default: Option<Option<String>>, } impl AccountCmd { - pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store>( + pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( &self, - mut client: Client<N, R, S>, + client: Client<N, R, S, A>, ) -> Result<(), String> { match self { - AccountCmd::List => { - list_accounts(client)?; - }, - AccountCmd::New { template } => { - let client_template = match template { - AccountTemplate::BasicImmutable { storage_type: storage_mode } => { - accounts::AccountTemplate::BasicWallet { - mutable_code: false, - storage_mode: storage_mode.into(), - } - }, - AccountTemplate::BasicMutable { storage_type: storage_mode } => { - accounts::AccountTemplate::BasicWallet { - mutable_code: true, - storage_mode: storage_mode.into(), - } - }, - AccountTemplate::FungibleFaucet { - token_symbol, - decimals, - max_supply, - storage_type: storage_mode, - } => accounts::AccountTemplate::FungibleFaucet { - token_symbol: TokenSymbol::new(token_symbol) - .map_err(|err| format!("error: token symbol is invalid: {}", err))?, - decimals: *decimals, - max_supply: *max_supply, - storage_mode: storage_mode.into(), - }, - AccountTemplate::NonFungibleFaucet { storage_type: _ } => todo!(), - }; - let (new_account, _account_seed) = client.new_account(client_template)?; - println!("Succesfully created new account."); - println!( - "To view account details execute `{CLIENT_BINARY_NAME} account -s {}`", - new_account.id() - ); - }, - AccountCmd::Show { id } => { + AccountCmd { + list: false, + show: Some(id), + default: None, + } => { let account_id = parse_account_id(&client, id)?; show_account(client, account_id)?; }, - AccountCmd::Default { - default_cmd: DefaultAccountCmd::Set { id }, + AccountCmd { + list: false, + show: None, + default: Some(id), } => { - let account_id: AccountId = AccountId::from_hex(id) - .map_err(|_| "Input number was not a valid Account Id")?; - - // Check whether we're tracking that account - let (account, _) = client.get_account_stub_by_id(account_id)?; - - // load config - let (mut current_config, config_path) = load_config_file()?; - - // set default account - current_config.cli = Some(CliConfig { - default_account_id: Some(account.id().to_hex()), - }); - - update_config(&config_path, current_config)?; - }, - AccountCmd::Default { default_cmd: DefaultAccountCmd::Unset } => { - let (mut current_config, path) = load_config_file()?; - - // unset default account - current_config.cli.replace(CliConfig { default_account_id: None }); + match id { + None => { + display_default_account_id()?; + }, + Some(id) => { + let default_account = if id == "none" { + None + } else { + let account_id: AccountId = AccountId::from_hex(id) + .map_err(|_| "Input number was not a valid Account Id")?; + + // Check whether we're tracking that account + let (account, _) = client.get_account_stub_by_id(account_id)?; + + Some(account.id().to_hex()) + }; + + // load config + let (mut current_config, config_path) = load_config_file()?; + + // set default account + current_config.cli = Some(CliConfig { + default_account_id: default_account.clone(), + }); + + if let Some(id) = default_account { + println!("Setting default account to {id}..."); + } else { + println!("Removing default account..."); + } - update_config(&path, current_config)?; + update_config(&config_path, current_config)?; + }, + } }, - AccountCmd::Default { default_cmd: DefaultAccountCmd::Show } => { - display_default_account_id()?; + _ => { + list_accounts(client)?; }, } Ok(()) @@ -196,8 +107,8 @@ impl AccountCmd { // LIST ACCOUNTS // ================================================================================================ -fn list_accounts<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +fn list_accounts<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, ) -> Result<(), String> { let accounts = client.get_account_stubs()?; @@ -226,8 +137,8 @@ fn list_accounts<N: NodeRpcClient, R: FeltRng, S: Store>( Ok(()) } -pub fn show_account<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +pub fn show_account<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, account_id: AccountId, ) -> Result<(), String> { let (account, _) = client.get_account(account_id)?; @@ -317,7 +228,7 @@ pub fn show_account<N: NodeRpcClient, R: FeltRng, S: Store>( let auth_info = client.get_account_auth(account_id)?; match auth_info { - miden_client::store::AuthInfo::RpoFalcon512(key_pair) => { + AuthSecretKey::RpoFalcon512(key_pair) => { let auth_info: [u8; SK_LEN] = key_pair .to_bytes() .try_into() diff --git a/src/cli/export.rs b/src/cli/export.rs index d2588c5c3..ef51d7472 100644 --- a/src/cli/export.rs +++ b/src/cli/export.rs @@ -5,45 +5,37 @@ use miden_client::{ store::{InputNoteRecord, Store}, }; use miden_objects::{crypto::rand::FeltRng, Digest}; -use miden_tx::utils::Serializable; +use miden_tx::{utils::Serializable, TransactionAuthenticator}; use super::Parser; #[derive(Debug, Parser, Clone)] -#[clap(about = "Export client objects")] -pub enum ExportCmd { - /// Export note data into a binary file - #[clap(short_flag = 'n')] - Note { - /// ID of the output note to export - #[clap()] - id: String, +#[clap(about = "Export client notes")] +pub struct ExportCmd { + /// ID of the output note to export + #[clap()] + id: String, - /// Desired filename for the binary file. Defaults to the note ID if not provided - #[clap(short, long, default_value = "false")] - filename: Option<PathBuf>, - }, + /// Desired filename for the binary file. Defaults to the note ID if not provided + #[clap(short, long, default_value = "false")] + filename: Option<PathBuf>, } impl ExportCmd { - pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store>( + pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( &self, - client: Client<N, R, S>, + client: Client<N, R, S, A>, ) -> Result<(), String> { - match self { - ExportCmd::Note { id, filename } => { - export_note(&client, id, filename.clone())?; - println!("Succesfully exported note {}", id); - }, - } + export_note(&client, self.id.as_str(), self.filename.clone())?; + println!("Succesfully exported note {}", self.id.as_str()); Ok(()) } } // EXPORT NOTE // ================================================================================================ -pub fn export_note<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +pub fn export_note<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: &Client<N, R, S, A>, note_id: &str, filename: Option<PathBuf>, ) -> Result<File, String> { diff --git a/src/cli/import.rs b/src/cli/import.rs index fa1779367..799e96e03 100644 --- a/src/cli/import.rs +++ b/src/cli/import.rs @@ -9,52 +9,42 @@ use miden_client::{ store::{InputNoteRecord, Store}, }; use miden_objects::{ - accounts::AccountData, crypto::rand::FeltRng, notes::NoteId, utils::Deserializable, + accounts::{AccountData, AccountId}, + crypto::rand::FeltRng, + notes::NoteId, + utils::Deserializable, }; +use miden_tx::TransactionAuthenticator; use tracing::info; use super::Parser; #[derive(Debug, Parser, Clone)] #[clap(about = "Import client objects such as accounts and notes")] -pub enum ImportCmd { - /// Import accounts from binary files (with .mac extension) - #[clap(short_flag = 'a')] - Account { - /// Paths to the files that contains the account data - #[arg()] - filenames: Vec<PathBuf>, - }, - /// Import note data from a binary file - #[clap(short_flag = 'n')] - Note { - /// Path to the file that contains the input note data - #[clap()] - filename: PathBuf, - - /// Skip verification of note's existence in the chain - #[clap(short, long, default_value = "false")] - no_verify: bool, - }, +pub struct ImportCmd { + /// Paths to the files that contains the account/note data + #[arg()] + filenames: Vec<PathBuf>, + /// Skip verification of note's existence in the chain (Only when importing notes) + #[clap(short, long, default_value = "false")] + no_verify: bool, } impl ImportCmd { - pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store>( + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( &self, - mut client: Client<N, R, S>, + mut client: Client<N, R, S, A>, ) -> Result<(), String> { - match self { - ImportCmd::Account { filenames } => { - validate_paths(filenames, "mac")?; - for filename in filenames { - import_account(&mut client, filename)?; - } - println!("Imported {} accounts.", filenames.len()); - }, - ImportCmd::Note { filename, no_verify } => { - let note_id = import_note(&mut client, filename.clone(), !(*no_verify)).await?; - println!("Succesfully imported note {}", note_id.inner()); - }, + validate_paths(&self.filenames)?; + for filename in &self.filenames { + let note_id = import_note(&mut client, filename.clone(), !self.no_verify).await; + if note_id.is_ok() { + println!("Succesfully imported note {}", note_id.unwrap().inner()); + continue; + } + let account_id = import_account(&mut client, filename) + .map_err(|_| format!("Failed to parse file {}", filename.to_string_lossy()))?; + println!("Succesfully imported account {}", account_id); } Ok(()) } @@ -63,10 +53,10 @@ impl ImportCmd { // IMPORT ACCOUNT // ================================================================================================ -fn import_account<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &mut Client<N, R, S>, +fn import_account<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: &mut Client<N, R, S, A>, filename: &PathBuf, -) -> Result<(), String> { +) -> Result<AccountId, String> { info!( "Attempting to import account data from {}...", fs::canonicalize(filename).map_err(|err| err.to_string())?.as_path().display() @@ -77,16 +67,15 @@ fn import_account<N: NodeRpcClient, R: FeltRng, S: Store>( let account_id = account_data.account.id(); client.import_account(account_data)?; - println!("Imported account with ID: {}", account_id); - Ok(()) + Ok(account_id) } // IMPORT NOTE // ================================================================================================ -pub async fn import_note<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &mut Client<N, R, S>, +pub async fn import_note<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: &mut Client<N, R, S, A>, filename: PathBuf, verify: bool, ) -> Result<NoteId, String> { @@ -112,17 +101,11 @@ pub async fn import_note<N: NodeRpcClient, R: FeltRng, S: Store>( /// Checks that all files exist, otherwise returns an error. It also ensures that all files have a /// specific extension -fn validate_paths(paths: &[PathBuf], expected_extension: &str) -> Result<(), String> { - let invalid_path = paths.iter().find(|path| { - !path.exists() || path.extension().map_or(false, |ext| ext != expected_extension) - }); +fn validate_paths(paths: &[PathBuf]) -> Result<(), String> { + let invalid_path = paths.iter().find(|path| !path.exists()); if let Some(path) = invalid_path { - Err(format!( - "The path `{}` does not exist or does not have the appropiate extension", - path.to_string_lossy() - ) - .to_string()) + Err(format!("The path `{}` does not exist", path.to_string_lossy()).to_string()) } else { Ok(()) } @@ -136,23 +119,23 @@ mod tests { use std::env::temp_dir; use miden_client::{ - client::{get_random_coin, transactions::transaction_request::TransactionTemplate}, - config::{ClientConfig, Endpoint, RpcConfig}, + client::transactions::transaction_request::TransactionTemplate, errors::IdPrefixFetchError, mock::{ - mock_full_chain_mmr_and_notes, mock_fungible_faucet_account, mock_notes, MockClient, - MockRpcApi, + create_test_client, mock_full_chain_mmr_and_notes, mock_fungible_faucet_account, + mock_notes, }, - store::{sqlite_store::SqliteStore, AuthInfo, InputNoteRecord, NoteFilter}, + store::{InputNoteRecord, NoteFilter}, }; use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::{AccountId, ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN}, + accounts::{ + account_id::testing::ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, AccountId, AuthSecretKey, + }, assets::FungibleAsset, crypto::dsa::rpo_falcon512::SecretKey, notes::Note, }; - use uuid::Uuid; use super::import_note; use crate::cli::{export::export_note, get_input_note_with_id_prefix}; @@ -167,18 +150,7 @@ mod tests { // 3. One output note, one input note. Both representing the same note. // generate test client - let mut path = temp_dir(); - path.push(Uuid::new_v4().to_string()); - let client_config = ClientConfig::new( - path.into_os_string().into_string().unwrap().try_into().unwrap(), - RpcConfig::default(), - ); - - let rng = get_random_coin(); - let store = SqliteStore::new((&client_config).into()).unwrap(); - - let mut client = - MockClient::new(MockRpcApi::new(&Endpoint::default().to_string()), rng, store, true); + let mut client = create_test_client(); // Add a faucet account to run a mint tx against it const FAUCET_ID: u64 = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN; @@ -192,7 +164,9 @@ mod tests { ); client.sync_state().await.unwrap(); - client.insert_account(&faucet, None, &AuthInfo::RpoFalcon512(key_pair)).unwrap(); + client + .insert_account(&faucet, None, &AuthSecretKey::RpoFalcon512(key_pair)) + .unwrap(); // Ensure client has no notes assert!(client.get_input_notes(NoteFilter::All).unwrap().is_empty()); @@ -209,7 +183,7 @@ mod tests { let transaction_request = client.build_transaction_request(transaction_template).unwrap(); let transaction = client.new_transaction(transaction_request).unwrap(); - let created_note = transaction.created_notes()[0].clone(); + let created_note = transaction.created_notes().get_note(0).clone(); client.submit_transaction(transaction).await.unwrap(); // Ensure client has no input notes and one output note @@ -249,18 +223,7 @@ mod tests { #[tokio::test] async fn get_input_note_with_prefix() { // generate test client - let mut path = temp_dir(); - path.push(Uuid::new_v4().to_string()); - let client_config = ClientConfig::new( - path.into_os_string().into_string().unwrap().try_into().unwrap(), - RpcConfig::default(), - ); - - let rng = get_random_coin(); - let store = SqliteStore::new((&client_config).into()).unwrap(); - - let mut client = - MockClient::new(MockRpcApi::new(&Endpoint::default().to_string()), rng, store, true); + let mut client = create_test_client(); // Ensure we get an error if no note is found let non_existent_note_id = "0x123456"; diff --git a/src/cli/info.rs b/src/cli/info.rs index f7e4082a4..d6c951074 100644 --- a/src/cli/info.rs +++ b/src/cli/info.rs @@ -6,9 +6,10 @@ use miden_client::{ store::{NoteFilter, Store}, }; use miden_objects::crypto::rand::FeltRng; +use miden_tx::TransactionAuthenticator; -pub fn print_client_info<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +pub fn print_client_info<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: &Client<N, R, S, A>, config: &ClientConfig, ) -> Result<(), String> { println!("Client version: {}", env!("CARGO_PKG_VERSION")); @@ -18,8 +19,8 @@ pub fn print_client_info<N: NodeRpcClient, R: FeltRng, S: Store>( // HELPERS // ================================================================================================ -fn print_client_stats<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +fn print_client_stats<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: &Client<N, R, S, A>, ) -> Result<(), String> { println!("Block number: {}", client.get_sync_height().map_err(|e| e.to_string())?); println!( diff --git a/src/cli/init.rs b/src/cli/init.rs index f8041ca01..035c7f775 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -15,8 +15,7 @@ pub struct InitCmd { #[clap(long)] rpc: Option<String>, - /// Store file path. If not provided user will be - /// asked for input + /// Store file path #[clap(long)] store_path: Option<String>, } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 559f95181..6918c8bdf 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,4 @@ -use std::{env, fs::File, io::Write, path::Path}; +use std::{env, fs::File, io::Write, path::Path, rc::Rc}; use clap::Parser; use comfy_table::{presets, Attribute, Cell, ContentArrangement, Table}; @@ -10,6 +10,7 @@ use miden_client::{ client::{ get_random_coin, rpc::{NodeRpcClient, TonicRpcClient}, + store_authenticator::StoreAuthenticator, Client, }, config::ClientConfig, @@ -21,12 +22,21 @@ use miden_client::{ }; use miden_objects::{ accounts::{AccountId, AccountStub}, - crypto::rand::{FeltRng, RpoRandomCoin}, + crypto::rand::FeltRng, }; +use miden_tx::TransactionAuthenticator; use tracing::info; +use transactions::TransactionCmd; use self::{ - account::AccountCmd, export::ExportCmd, import::ImportCmd, init::InitCmd, tags::TagsCmd, + account::AccountCmd, + export::ExportCmd, + import::ImportCmd, + init::InitCmd, + new_account::{NewFaucetCmd, NewWalletCmd}, + new_transactions::{ConsumeNotesCmd, MintCmd, SendCmd, SwapCmd}, + notes::NotesCmd, + tags::TagsCmd, }; mod account; @@ -34,6 +44,8 @@ mod export; mod import; mod info; mod init; +mod new_account; +mod new_transactions; mod notes; mod sync; mod tags; @@ -61,33 +73,24 @@ pub struct Cli { /// CLI actions #[derive(Debug, Parser)] pub enum Command { - Account { - #[clap(subcommand)] - cmd: Option<AccountCmd>, - }, - #[clap(subcommand)] + Account(AccountCmd), + NewFaucet(NewFaucetCmd), + NewWallet(NewWalletCmd), Import(ImportCmd), - #[clap(subcommand)] Export(ExportCmd), Init(InitCmd), - Notes { - #[clap(subcommand)] - cmd: Option<notes::Notes>, - }, + Notes(NotesCmd), /// Sync this client with the latest state of the Miden network. Sync, /// View a summary of the current client state Info, - Tags { - #[clap(subcommand)] - cmd: Option<TagsCmd>, - }, + Tags(TagsCmd), #[clap(name = "tx")] - #[clap(visible_alias = "transaction")] - Transaction { - #[clap(subcommand)] - cmd: Option<transactions::Transaction>, - }, + Transaction(TransactionCmd), + Mint(MintCmd), + Send(SendCmd), + Swap(SwapCmd), + ConsumeNotes(ConsumeNotesCmd), } /// CLI entry point @@ -115,39 +118,41 @@ impl Cli { // Create the client let client_config = load_config(current_dir.as_path())?; let store = SqliteStore::new((&client_config).into()).map_err(ClientError::StoreError)?; + let store = Rc::new(store); + let rng = get_random_coin(); - let _executor_store = - miden_client::store::sqlite_store::SqliteStore::new((&client_config).into()) - .map_err(ClientError::StoreError)?; + let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng); + + let client = Client::new( + TonicRpcClient::new(&client_config.rpc), + rng, + store, + authenticator, + in_debug_mode, + ); - let client: Client<TonicRpcClient, RpoRandomCoin, SqliteStore> = - Client::new(TonicRpcClient::new(&client_config.rpc), rng, store, in_debug_mode); + let default_account_id = + client_config.cli.clone().and_then(|cli_conf| cli_conf.default_account_id); // Execute CLI command match &self.action { - Command::Account { cmd } => { - let account = cmd.clone().unwrap_or_default(); - account.execute(client) - }, + Command::Account(account) => account.execute(client), + Command::NewFaucet(new_faucet) => new_faucet.execute(client), + Command::NewWallet(new_wallet) => new_wallet.execute(client), Command::Import(import) => import.execute(client).await, Command::Init(_) => Ok(()), Command::Info => info::print_client_info(&client, &client_config), - Command::Notes { cmd: notes_cmd } => { - let notes_cmd = notes_cmd.clone().unwrap_or_default(); - notes_cmd.execute(client).await - }, + Command::Notes(notes) => notes.execute(client).await, Command::Sync => sync::sync_state(client).await, - Command::Tags { cmd: tags_cmd } => { - let tags_cmd = tags_cmd.clone().unwrap_or_default(); - tags_cmd.execute(client).await - }, - Command::Transaction { cmd: transaction_cmd } => { - let transaction_cmd = transaction_cmd.clone().unwrap_or_default(); - let default_account_id = - client_config.cli.and_then(|cli_conf| cli_conf.default_account_id); - transaction_cmd.execute(client, default_account_id).await - }, + Command::Tags(tags) => tags.execute(client).await, + Command::Transaction(transaction) => transaction.execute(client).await, Command::Export(cmd) => cmd.execute(client), + Command::Mint(mint) => mint.clone().execute(client, default_account_id).await, + Command::Send(send) => send.clone().execute(client, default_account_id).await, + Command::Swap(swap) => swap.clone().execute(client, default_account_id).await, + Command::ConsumeNotes(consume_notes) => { + consume_notes.clone().execute(client, default_account_id).await + }, } } } @@ -185,8 +190,13 @@ pub fn create_dynamic_table(headers: &[&str]) -> Table { /// `note_id_prefix` is a prefix of its id. /// - Returns [IdPrefixFetchError::MultipleMatches] if there were more than one note found /// where `note_id_prefix` is a prefix of its id. -pub(crate) fn get_input_note_with_id_prefix<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +pub(crate) fn get_input_note_with_id_prefix< + N: NodeRpcClient, + R: FeltRng, + S: Store, + A: TransactionAuthenticator, +>( + client: &Client<N, R, S, A>, note_id_prefix: &str, ) -> Result<InputNoteRecord, IdPrefixFetchError> { let mut input_note_records = client @@ -232,8 +242,13 @@ pub(crate) fn get_input_note_with_id_prefix<N: NodeRpcClient, R: FeltRng, S: Sto /// `note_id_prefix` is a prefix of its id. /// - Returns [IdPrefixFetchError::MultipleMatches] if there were more than one note found /// where `note_id_prefix` is a prefix of its id. -pub(crate) fn get_output_note_with_id_prefix<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +pub(crate) fn get_output_note_with_id_prefix< + N: NodeRpcClient, + R: FeltRng, + S: Store, + A: TransactionAuthenticator, +>( + client: &Client<N, R, S, A>, note_id_prefix: &str, ) -> Result<OutputNoteRecord, IdPrefixFetchError> { let mut output_note_records = client @@ -279,8 +294,13 @@ pub(crate) fn get_output_note_with_id_prefix<N: NodeRpcClient, R: FeltRng, S: St /// `account_id_prefix` is a prefix of its id. /// - Returns [IdPrefixFetchError::MultipleMatches] if there were more than one account found /// where `account_id_prefix` is a prefix of its id. -fn get_account_with_id_prefix<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +fn get_account_with_id_prefix< + N: NodeRpcClient, + R: FeltRng, + S: Store, + A: TransactionAuthenticator, +>( + client: &Client<N, R, S, A>, account_id_prefix: &str, ) -> Result<AccountStub, IdPrefixFetchError> { let mut accounts = client @@ -327,8 +347,13 @@ fn get_account_with_id_prefix<N: NodeRpcClient, R: FeltRng, S: Store>( /// /// - Will return a `IdPrefixFetchError` if the provided account id string can't be parsed as an /// `AccountId` and does not correspond to an account tracked by the client either. -pub(crate) fn parse_account_id<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, +pub(crate) fn parse_account_id< + N: NodeRpcClient, + R: FeltRng, + S: Store, + A: TransactionAuthenticator, +>( + client: &Client<N, R, S, A>, account_id: &str, ) -> Result<AccountId, String> { if let Ok(account_id) = AccountId::from_hex(account_id) { diff --git a/src/cli/new_account.rs b/src/cli/new_account.rs new file mode 100644 index 000000000..9b842497c --- /dev/null +++ b/src/cli/new_account.rs @@ -0,0 +1,122 @@ +use clap::{Parser, ValueEnum}; +use miden_client::{ + client::{ + accounts::{self, AccountTemplate}, + rpc::NodeRpcClient, + Client, + }, + store::Store, +}; +use miden_objects::{assets::TokenSymbol, crypto::rand::FeltRng}; +use miden_tx::TransactionAuthenticator; + +use crate::cli::CLIENT_BINARY_NAME; + +#[derive(Debug, Parser, Clone)] +/// Create a new faucet account +pub struct NewFaucetCmd { + #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] + /// Storage type of the account + storage_type: AccountStorageMode, + #[clap(short, long)] + /// Defines if the account assets are non-fungible (by default it is fungible) + non_fungible: bool, + #[clap(short, long)] + /// Token symbol of the faucet + token_symbol: Option<String>, + #[clap(short, long)] + /// Decimals of the faucet + decimals: Option<u8>, + #[clap(short, long)] + max_supply: Option<u64>, +} + +impl NewFaucetCmd { + pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + &self, + mut client: Client<N, R, S, A>, + ) -> Result<(), String> { + if self.non_fungible { + todo!("Non-fungible faucets are not supported yet"); + } + + if self.token_symbol.is_none() || self.decimals.is_none() || self.max_supply.is_none() { + return Err( + "`token-symbol`, `decimals` and `max-supply` flags must be provided for a fungible faucet" + .to_string(), + ); + } + + let client_template = AccountTemplate::FungibleFaucet { + token_symbol: TokenSymbol::new( + self.token_symbol.clone().expect("token symbol must be provided").as_str(), + ) + .map_err(|err| format!("error: token symbol is invalid: {}", err))?, + decimals: self.decimals.expect("decimals must be provided"), + max_supply: self.max_supply.expect("max supply must be provided"), + storage_mode: self.storage_type.into(), + }; + + let (new_account, _account_seed) = client.new_account(client_template)?; + println!("Succesfully created new faucet."); + println!( + "To view account details execute `{CLIENT_BINARY_NAME} account -s {}`", + new_account.id() + ); + + Ok(()) + } +} + +#[derive(Debug, Parser, Clone)] +/// Create a new wallet account +pub struct NewWalletCmd { + #[clap(short, long, value_enum, default_value_t = AccountStorageMode::OffChain)] + /// Storage type of the account + pub storage_type: AccountStorageMode, + #[clap(short, long)] + /// Defines if the account code is mutable (by default it is not mutable) + pub mutable: bool, +} + +impl NewWalletCmd { + pub fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + &self, + mut client: Client<N, R, S, A>, + ) -> Result<(), String> { + let client_template = AccountTemplate::BasicWallet { + mutable_code: self.mutable, + storage_mode: self.storage_type.into(), + }; + + let (new_account, _account_seed) = client.new_account(client_template)?; + println!("Succesfully created new wallet."); + println!( + "To view account details execute `{CLIENT_BINARY_NAME} account -s {}`", + new_account.id() + ); + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum AccountStorageMode { + OffChain, + OnChain, +} + +impl From<AccountStorageMode> for accounts::AccountStorageMode { + fn from(value: AccountStorageMode) -> Self { + match value { + AccountStorageMode::OffChain => accounts::AccountStorageMode::Local, + AccountStorageMode::OnChain => accounts::AccountStorageMode::OnChain, + } + } +} + +impl From<&AccountStorageMode> for accounts::AccountStorageMode { + fn from(value: &AccountStorageMode) -> Self { + accounts::AccountStorageMode::from(*value) + } +} diff --git a/src/cli/new_transactions.rs b/src/cli/new_transactions.rs new file mode 100644 index 000000000..f390ac897 --- /dev/null +++ b/src/cli/new_transactions.rs @@ -0,0 +1,445 @@ +use std::io; + +use clap::{Parser, ValueEnum}; +use miden_client::{ + client::{ + rpc::NodeRpcClient, + transactions::{ + transaction_request::{ + PaymentTransactionData, SwapTransactionData, TransactionTemplate, + }, + TransactionResult, + }, + }, + store::Store, +}; +use miden_objects::{ + accounts::AccountId, + assets::{Asset, FungibleAsset}, + crypto::rand::FeltRng, + notes::{NoteExecutionHint, NoteId, NoteTag, NoteType as MidenNoteType}, + Digest, NoteError, +}; +use miden_tx::TransactionAuthenticator; + +use super::{get_input_note_with_id_prefix, parse_account_id, Client}; +use crate::cli::create_dynamic_table; + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum NoteType { + Public, + Private, +} + +impl From<&NoteType> for MidenNoteType { + fn from(note_type: &NoteType) -> Self { + match note_type { + NoteType::Public => MidenNoteType::Public, + NoteType::Private => MidenNoteType::OffChain, + } + } +} + +#[derive(Debug, Parser, Clone)] +/// Mint tokens from a fungible faucet to a wallet. +pub struct MintCmd { + /// Target account ID or its hex prefix + #[clap(short = 't', long = "target")] + target_account_id: String, + /// Faucet account ID or its hex prefix + #[clap(short = 'f', long = "faucet")] + faucet_id: String, + /// Amount of tokens to mint + #[clap(short, long)] + amount: u64, + #[clap(short, long, value_enum)] + note_type: NoteType, + /// Flag to submit the executed transaction without asking for confirmation + #[clap(short, long, default_value_t = false)] + force: bool, +} + +impl MintCmd { + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + mut client: Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<(), String> { + let force = self.force; + let transaction_template = self.into_template(&client, default_account_id)?; + execute_transaction(&mut client, transaction_template, force).await + } + + fn into_template<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + client: &Client<N, R, S, A>, + _default_account_id: Option<String>, + ) -> Result<TransactionTemplate, String> { + let faucet_id = parse_account_id(client, self.faucet_id.as_str())?; + let fungible_asset = + FungibleAsset::new(faucet_id, self.amount).map_err(|err| err.to_string())?; + let target_account_id = parse_account_id(client, self.target_account_id.as_str())?; + + Ok(TransactionTemplate::MintFungibleAsset( + fungible_asset, + target_account_id, + (&self.note_type).into(), + )) + } +} + +#[derive(Debug, Parser, Clone)] +/// Create a pay-to-id transaction. +pub struct SendCmd { + /// Sender account ID or its hex prefix. If none is provided, the default account's ID is used instead + #[clap(short = 's', long = "sender")] + sender_account_id: Option<String>, + /// Target account ID or its hex prefix + #[clap(short = 't', long = "target")] + target_account_id: String, + /// Faucet account ID or its hex prefix + #[clap(short = 'f', long = "faucet")] + faucet_id: String, + #[clap(short, long, value_enum)] + note_type: NoteType, + /// Flag to submit the executed transaction without asking for confirmation + #[clap(long, default_value_t = false)] + force: bool, + /// Set the recall height for the transaction. If the note was not consumed by this height, the sender may consume it back. + /// + /// Setting this flag turns the transaction from a PayToId to a PayToIdWithRecall. + #[clap(short, long)] + recall_height: Option<u32>, + /// Amount of tokens to mint + amount: u64, +} + +impl SendCmd { + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + mut client: Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<(), String> { + let force = self.force; + let transaction_template = self.into_template(&client, default_account_id)?; + execute_transaction(&mut client, transaction_template, force).await + } + + fn into_template<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + client: &Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<TransactionTemplate, String> { + let faucet_id = parse_account_id(client, self.faucet_id.as_str())?; + let fungible_asset = FungibleAsset::new(faucet_id, self.amount) + .map_err(|err| err.to_string())? + .into(); + + // try to use either the provided argument or the default account + let sender_account_id = self + .sender_account_id + .clone() + .or(default_account_id) + .ok_or("Neither a sender nor a default account was provided".to_string())?; + let sender_account_id = parse_account_id(client, &sender_account_id)?; + let target_account_id = parse_account_id(client, self.target_account_id.as_str())?; + + let payment_transaction = + PaymentTransactionData::new(fungible_asset, sender_account_id, target_account_id); + if let Some(recall_height) = self.recall_height { + Ok(TransactionTemplate::PayToIdWithRecall( + payment_transaction, + recall_height, + (&self.note_type).into(), + )) + } else { + Ok(TransactionTemplate::PayToId(payment_transaction, (&self.note_type).into())) + } + } +} + +#[derive(Debug, Parser, Clone)] +/// Create a swap transaction. +pub struct SwapCmd { + /// Sender account ID or its hex prefix. If none is provided, the default account's ID is used instead + #[clap(short = 's', long = "source")] + sender_account_id: Option<String>, + /// Offered Faucet account ID or its hex prefix + #[clap(long = "offered-faucet")] + offered_asset_faucet_id: String, + /// Offered amount + #[clap(long = "offered-amount")] + offered_asset_amount: u64, + /// Requested Faucet account ID or its hex prefix + #[clap(long = "requested-faucet")] + requested_asset_faucet_id: String, + /// Requested amount + #[clap(long = "requested-amount")] + requested_asset_amount: u64, + #[clap(short, long, value_enum)] + note_type: NoteType, + /// Flag to submit the executed transaction without asking for confirmation + #[clap(long, default_value_t = false)] + force: bool, +} + +impl SwapCmd { + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + mut client: Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<(), String> { + let force = self.force; + let transaction_template = self.into_template(&client, default_account_id)?; + execute_transaction(&mut client, transaction_template, force).await + } + + fn into_template<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + client: &Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<TransactionTemplate, String> { + let offered_asset_faucet_id = parse_account_id(client, &self.offered_asset_faucet_id)?; + let offered_fungible_asset = + FungibleAsset::new(offered_asset_faucet_id, self.offered_asset_amount) + .map_err(|err| err.to_string())? + .into(); + + let requested_asset_faucet_id = parse_account_id(client, &self.requested_asset_faucet_id)?; + let requested_fungible_asset = + FungibleAsset::new(requested_asset_faucet_id, self.requested_asset_amount) + .map_err(|err| err.to_string())? + .into(); + + // try to use either the provided argument or the default account + let sender_account_id = self + .sender_account_id + .clone() + .or(default_account_id) + .ok_or("Neither a sender nor a default account was provided".to_string())?; + let sender_account_id = parse_account_id(client, &sender_account_id)?; + + let swap_transaction = SwapTransactionData::new( + sender_account_id, + offered_fungible_asset, + requested_fungible_asset, + ); + + Ok(TransactionTemplate::Swap(swap_transaction, (&self.note_type).into())) + } +} + +#[derive(Debug, Parser, Clone)] +/// Consume with the account corresponding to `account_id` all of the notes from `list_of_notes`. +pub struct ConsumeNotesCmd { + /// The account ID to be used to consume the note or its hex prefix. If none is provided, the default + /// account's ID is used instead + #[clap(short = 'a', long = "account")] + account_id: Option<String>, + /// A list of note IDs or the hex prefixes of their corresponding IDs + list_of_notes: Vec<String>, + /// Flag to submit the executed transaction without asking for confirmation + #[clap(short, long, default_value_t = false)] + force: bool, +} + +impl ConsumeNotesCmd { + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + mut client: Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<(), String> { + let force = self.force; + let transaction_template = self.into_template(&client, default_account_id)?; + execute_transaction(&mut client, transaction_template, force).await + } + + fn into_template<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + self, + client: &Client<N, R, S, A>, + default_account_id: Option<String>, + ) -> Result<TransactionTemplate, String> { + let list_of_notes = self + .list_of_notes + .iter() + .map(|note_id| { + get_input_note_with_id_prefix(client, note_id) + .map(|note_record| note_record.id()) + .map_err(|err| err.to_string()) + }) + .collect::<Result<Vec<NoteId>, _>>()?; + + let account_id = self + .account_id + .clone() + .or(default_account_id) + .ok_or("Neither a sender nor a default account was provided".to_string())?; + let account_id = parse_account_id(client, &account_id)?; + + Ok(TransactionTemplate::ConsumeNotes(account_id, list_of_notes)) + } +} + +// EXECUTE TRANSACTION +// ================================================================================================ +async fn execute_transaction< + N: NodeRpcClient, + R: FeltRng, + S: Store, + A: TransactionAuthenticator, +>( + client: &mut Client<N, R, S, A>, + transaction_template: TransactionTemplate, + force: bool, +) -> Result<(), String> { + let transaction_request = client.build_transaction_request(transaction_template.clone())?; + + println!("Executing transaction..."); + let transaction_execution_result = client.new_transaction(transaction_request)?; + + // Show delta and ask for confirmation + print_transaction_details(&transaction_execution_result); + if !force { + println!("Continue with proving and submission? Changes will be irreversible once the proof is finalized on the rollup (Y/N)"); + let mut proceed_str: String = String::new(); + io::stdin().read_line(&mut proceed_str).expect("Should read line"); + + if proceed_str.trim().to_lowercase() != "y" { + println!("Transaction was cancelled."); + return Ok(()); + } + } + + println!("Proving transaction and then submitting it to node..."); + + let transaction_id = transaction_execution_result.executed_transaction().id(); + let output_notes = transaction_execution_result + .created_notes() + .iter() + .map(|note| note.id()) + .collect::<Vec<_>>(); + client.submit_transaction(transaction_execution_result).await?; + + if let TransactionTemplate::Swap(swap_data, note_type) = transaction_template { + let payback_note_tag: u32 = build_swap_tag( + note_type, + swap_data.offered_asset().faucet_id(), + swap_data.requested_asset().faucet_id(), + ) + .map_err(|err| err.to_string())? + .into(); + println!( + "To receive updates about the payback Swap Note run `miden tags add {}`", + payback_note_tag + ); + } + + println!("Succesfully created transaction."); + println!("Transaction ID: {}", transaction_id); + println!("Output notes:"); + output_notes.iter().for_each(|note_id| println!("\t- {}", note_id)); + + Ok(()) +} + +fn print_transaction_details(transaction_result: &TransactionResult) { + println!( + "The transaction will have the following effects on the account with ID {}", + transaction_result.executed_transaction().account_id() + ); + + let account_delta = transaction_result.account_delta(); + let mut table = create_dynamic_table(&["Storage Slot", "Effect"]); + + for cleared_item_slot in account_delta.storage().cleared_items.iter() { + table.add_row(vec![cleared_item_slot.to_string(), "Cleared".to_string()]); + } + + for (updated_item_slot, new_value) in account_delta.storage().updated_items.iter() { + let value_digest: Digest = new_value.into(); + table.add_row(vec![ + updated_item_slot.to_string(), + format!("Updated ({})", value_digest.to_hex()), + ]); + } + + println!("Storage changes:"); + println!("{table}"); + + let mut table = create_dynamic_table(&["Asset Type", "Faucet ID", "Amount"]); + + for asset in account_delta.vault().added_assets.iter() { + let (asset_type, faucet_id, amount) = match asset { + Asset::Fungible(fungible_asset) => { + ("Fungible Asset", fungible_asset.faucet_id(), fungible_asset.amount()) + }, + Asset::NonFungible(non_fungible_asset) => { + ("Non Fungible Asset", non_fungible_asset.faucet_id(), 1) + }, + }; + table.add_row(vec![asset_type, &faucet_id.to_hex(), &format!("+{}", amount)]); + } + + for asset in account_delta.vault().removed_assets.iter() { + let (asset_type, faucet_id, amount) = match asset { + Asset::Fungible(fungible_asset) => { + ("Fungible Asset", fungible_asset.faucet_id(), fungible_asset.amount()) + }, + Asset::NonFungible(non_fungible_asset) => { + ("Non Fungible Asset", non_fungible_asset.faucet_id(), 1) + }, + }; + table.add_row(vec![asset_type, &faucet_id.to_hex(), &format!("-{}", amount)]); + } + + println!("Vault changes:"); + println!("{table}"); + + if let Some(new_nonce) = account_delta.nonce() { + println!("New nonce: {new_nonce}.") + } else { + println!("No nonce changes.") + } +} + +// HELPERS +// ================================================================================================ + +/// Returns a note tag for a swap note with the specified parameters. +/// +/// Use case ID for the returned tag is set to 0. +/// +/// Tag payload is constructed by taking asset tags (8 bits of faucet ID) and concatenating them +/// together as offered_asset_tag + requested_asset tag. +/// +/// Network execution hint for the returned tag is set to `Local`. +/// +/// Based on miden-base's implementation (<https://github.com/0xPolygonMiden/miden-base/blob/9e4de88031b55bcc3524cb0ccfb269821d97fb29/miden-lib/src/notes/mod.rs#L153>) +/// +/// TODO: we should make the function in base public and once that gets released use that one and +/// delete this implementation. +fn build_swap_tag( + note_type: MidenNoteType, + offered_asset_faucet_id: AccountId, + requested_asset_faucet_id: AccountId, +) -> Result<NoteTag, NoteError> { + const SWAP_USE_CASE_ID: u16 = 0; + + // get bits 4..12 from faucet IDs of both assets, these bits will form the tag payload; the + // reason we skip the 4 most significant bits is that these encode metadata of underlying + // faucets and are likely to be the same for many different faucets. + + let offered_asset_id: u64 = offered_asset_faucet_id.into(); + let offered_asset_tag = (offered_asset_id >> 52) as u8; + + let requested_asset_id: u64 = requested_asset_faucet_id.into(); + let requested_asset_tag = (requested_asset_id >> 52) as u8; + + let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); + + let execution = NoteExecutionHint::Local; + match note_type { + MidenNoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution), + _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), + } +} diff --git a/src/cli/notes.rs b/src/cli/notes.rs index c897ae5e6..9887c4acd 100644 --- a/src/cli/notes.rs +++ b/src/cli/notes.rs @@ -5,7 +5,7 @@ use comfy_table::{presets, Attribute, Cell, ContentArrangement}; use miden_client::{ client::{ rpc::NodeRpcClient, - transactions::transaction_request::known_script_hashs::{P2ID, P2IDR, SWAP}, + transactions::transaction_request::known_script_roots::{P2ID, P2IDR, SWAP}, ConsumableNote, }, errors::{ClientError, IdPrefixFetchError}, @@ -18,6 +18,7 @@ use miden_objects::{ notes::{NoteInputs, NoteMetadata}, Digest, }; +use miden_tx::TransactionAuthenticator; use super::{Client, Parser}; use crate::cli::{ @@ -26,66 +27,61 @@ use crate::cli::{ #[derive(Clone, Debug, ValueEnum)] pub enum NoteFilter { + All, Pending, Committed, Consumed, + Consumable, } -#[derive(Debug, Parser, Clone)] -#[clap(about = "View and manage notes")] -pub enum Notes { - /// List notes - #[clap(short_flag = 'l')] - List { - /// Filter the displayed note list - #[clap(short, long)] - filter: Option<NoteFilter>, - }, +impl TryInto<ClientNoteFilter<'_>> for NoteFilter { + type Error = String; - /// Show details of the note for the specified note ID - #[clap(short_flag = 's')] - Show { - /// Note ID of the note to show - #[clap()] - id: String, - }, - - /// List consumable notes - #[clap(short_flag = 'c')] - ListConsumable { - /// Account ID used to filter list. Only notes consumable by this account will be shown. - #[clap()] - account_id: Option<String>, - }, + fn try_into(self) -> Result<ClientNoteFilter<'static>, Self::Error> { + match self { + NoteFilter::All => Ok(ClientNoteFilter::All), + NoteFilter::Pending => Ok(ClientNoteFilter::Pending), + NoteFilter::Committed => Ok(ClientNoteFilter::Committed), + NoteFilter::Consumed => Ok(ClientNoteFilter::Consumed), + NoteFilter::Consumable => Err("Consumable filter is not supported".to_string()), + } + } } -impl Default for Notes { - fn default() -> Self { - Notes::List { filter: None } - } +#[derive(Debug, Parser, Clone)] +#[clap(about = "View and manage notes")] +pub struct NotesCmd { + /// List notes with the specified filter. If no filter is provided, all notes will be listed. + #[clap(short, long, group = "action", default_missing_value="all", num_args=0..=1, value_name = "filter")] + list: Option<NoteFilter>, + /// Show note with the specified ID. + #[clap(short, long, group = "action", value_name = "note_id")] + show: Option<String>, + /// (only has effect on `--list consumable`) Account ID used to filter list. Only notes consumable by this account will be shown. + #[clap(short, long, value_name = "account_id")] + account_id: Option<String>, } -impl Notes { - pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store>( +impl NotesCmd { + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( &self, - client: Client<N, R, S>, + client: Client<N, R, S, A>, ) -> Result<(), String> { match self { - Notes::List { filter } => { - let filter = match filter { - Some(NoteFilter::Committed) => ClientNoteFilter::Committed, - Some(NoteFilter::Consumed) => ClientNoteFilter::Consumed, - Some(NoteFilter::Pending) => ClientNoteFilter::Pending, - None => ClientNoteFilter::All, - }; - - list_notes(client, filter)?; + NotesCmd { list: Some(NoteFilter::Consumable), .. } => { + list_consumable_notes(client, &None)?; }, - Notes::Show { id } => { + NotesCmd { list: Some(filter), .. } => { + list_notes( + client, + filter.clone().try_into().expect("Filter shouldn't be consumable"), + )?; + }, + NotesCmd { show: Some(id), .. } => { show_note(client, id.to_owned())?; }, - Notes::ListConsumable { account_id } => { - list_consumable_notes(client, account_id)?; + _ => { + list_notes(client, ClientNoteFilter::All)?; }, } Ok(()) @@ -107,8 +103,8 @@ struct CliNoteSummary { // LIST NOTES // ================================================================================================ -fn list_notes<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +fn list_notes<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, filter: ClientNoteFilter, ) -> Result<(), String> { let input_notes = client.get_input_notes(filter.clone())?; @@ -138,8 +134,8 @@ fn list_notes<N: NodeRpcClient, R: FeltRng, S: Store>( // SHOW NOTE // ================================================================================================ -fn show_note<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +fn show_note<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, note_id: String, ) -> Result<(), String> { let input_note_record = get_input_note_with_id_prefix(&client, ¬e_id); @@ -293,8 +289,8 @@ fn show_note<N: NodeRpcClient, R: FeltRng, S: Store>( // LIST CONSUMABLE INPUT NOTES // ================================================================================================ -fn list_consumable_notes<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +fn list_consumable_notes<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, account_id: &Option<String>, ) -> Result<(), String> { let account_id = match account_id { @@ -513,41 +509,30 @@ mod tests { use std::env::temp_dir; use miden_client::{ - client::{get_random_coin, transactions::transaction_request::TransactionTemplate}, - config::{ClientConfig, Endpoint, RpcConfig}, + client::transactions::transaction_request::TransactionTemplate, errors::IdPrefixFetchError, mock::{ - mock_full_chain_mmr_and_notes, mock_fungible_faucet_account, mock_notes, MockClient, - MockRpcApi, + create_test_client, mock_full_chain_mmr_and_notes, mock_fungible_faucet_account, + mock_notes, }, - store::{sqlite_store::SqliteStore, AuthInfo, InputNoteRecord, NoteFilter}, + store::{InputNoteRecord, NoteFilter}, }; use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::{AccountId, ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN}, + accounts::{ + account_id::testing::ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, AccountId, AuthSecretKey, + }, assets::FungibleAsset, crypto::dsa::rpo_falcon512::SecretKey, notes::Note, }; - use uuid::Uuid; use crate::cli::{export::export_note, get_input_note_with_id_prefix, import::import_note}; #[tokio::test] async fn test_import_note_validation() { // generate test client - let mut path = temp_dir(); - path.push(Uuid::new_v4().to_string()); - let client_config = ClientConfig::new( - path.into_os_string().into_string().unwrap().try_into().unwrap(), - RpcConfig::default(), - ); - - let rng = get_random_coin(); - let store = SqliteStore::new((&client_config).into()).unwrap(); - - let mut client = - MockClient::new(MockRpcApi::new(&Endpoint::default().to_string()), rng, store, true); + let mut client = create_test_client(); // generate test data let assembler = TransactionKernel::assembler(); @@ -574,18 +559,7 @@ mod tests { // 3. One output note, one input note. Both representing the same note. // generate test client - let mut path = temp_dir(); - path.push(Uuid::new_v4().to_string()); - let client_config = ClientConfig::new( - path.into_os_string().into_string().unwrap().try_into().unwrap(), - RpcConfig::default(), - ); - - let rng = get_random_coin(); - let store = SqliteStore::new((&client_config).into()).unwrap(); - - let mut client = - MockClient::new(MockRpcApi::new(&Endpoint::default().to_string()), rng, store, true); + let mut client = create_test_client(); // Add a faucet account to run a mint tx against it const FAUCET_ID: u64 = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN; @@ -599,7 +573,9 @@ mod tests { ); client.sync_state().await.unwrap(); - client.insert_account(&faucet, None, &AuthInfo::RpoFalcon512(key_pair)).unwrap(); + client + .insert_account(&faucet, None, &AuthSecretKey::RpoFalcon512(key_pair)) + .unwrap(); // Ensure client has no notes assert!(client.get_input_notes(NoteFilter::All).unwrap().is_empty()); @@ -616,7 +592,7 @@ mod tests { let transaction_request = client.build_transaction_request(transaction_template).unwrap(); let transaction = client.new_transaction(transaction_request).unwrap(); - let created_note = transaction.created_notes()[0].clone(); + let created_note = transaction.created_notes().get_note(0).clone(); client.submit_transaction(transaction).await.unwrap(); // Ensure client has no input notes and one output note @@ -658,18 +634,7 @@ mod tests { #[tokio::test] async fn get_input_note_with_prefix() { // generate test client - let mut path = temp_dir(); - path.push(Uuid::new_v4().to_string()); - let client_config = ClientConfig::new( - path.into_os_string().into_string().unwrap().try_into().unwrap(), - RpcConfig::default(), - ); - - let rng = get_random_coin(); - let store = SqliteStore::new((&client_config).into()).unwrap(); - - let mut client = - MockClient::new(MockRpcApi::new(&Endpoint::default().to_string()), rng, store, true); + let mut client = create_test_client(); // Ensure we get an error if no note is found let non_existent_note_id = "0x123456"; diff --git a/src/cli/sync.rs b/src/cli/sync.rs index a1719b527..78e1851eb 100644 --- a/src/cli/sync.rs +++ b/src/cli/sync.rs @@ -3,9 +3,10 @@ use miden_client::{ store::Store, }; use miden_objects::crypto::rand::FeltRng; +use miden_tx::TransactionAuthenticator; -pub async fn sync_state<N: NodeRpcClient, R: FeltRng, S: Store>( - mut client: Client<N, R, S>, +pub async fn sync_state<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + mut client: Client<N, R, S, A>, ) -> Result<(), String> { let new_details = client.sync_state().await?; println!("State synced to block {}", new_details.block_num); diff --git a/src/cli/tags.rs b/src/cli/tags.rs index 16b09be92..8af4fcc96 100644 --- a/src/cli/tags.rs +++ b/src/cli/tags.rs @@ -1,50 +1,44 @@ use miden_client::{client::rpc::NodeRpcClient, store::Store}; use miden_objects::{ crypto::rand::FeltRng, - notes::{NoteExecutionMode, NoteTag}, + notes::{NoteExecutionHint, NoteTag}, }; +use miden_tx::TransactionAuthenticator; use tracing::info; use super::{Client, Parser}; #[derive(Default, Debug, Parser, Clone)] #[clap(about = "View and manage tags. Defaults to `list` command.")] -pub enum TagsCmd { +pub struct TagsCmd { /// List all tags monitored by this client - #[default] - #[clap(short_flag = 'l')] - List, + #[clap(short, long, group = "action")] + list: bool, /// Add a new tag to the list of tags monitored by this client - #[clap(short_flag = 'a')] - Add { - #[clap()] - tag: u32, - }, + #[clap(short, long, group = "action", value_name = "tag")] + add: Option<u32>, /// Removes a tag from the list of tags monitored by this client - #[clap(short_flag = 'r')] - Remove { - #[clap()] - tag: u32, - }, + #[clap(short, long, group = "action", value_name = "tag")] + remove: Option<u32>, } impl TagsCmd { - pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store>( + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( &self, - client: Client<N, R, S>, + client: Client<N, R, S, A>, ) -> Result<(), String> { match self { - TagsCmd::List => { - list_tags(client)?; - }, - TagsCmd::Add { tag } => { + TagsCmd { add: Some(tag), .. } => { add_tag(client, *tag)?; }, - TagsCmd::Remove { tag } => { + TagsCmd { remove: Some(tag), .. } => { remove_tag(client, *tag)?; }, + _ => { + list_tags(client)?; + }, } Ok(()) } @@ -52,22 +46,22 @@ impl TagsCmd { // HELPERS // ================================================================================================ -fn list_tags<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +fn list_tags<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, ) -> Result<(), String> { let tags = client.get_note_tags()?; println!("Tags: {:?}", tags); Ok(()) } -fn add_tag<N: NodeRpcClient, R: FeltRng, S: Store>( - mut client: Client<N, R, S>, +fn add_tag<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + mut client: Client<N, R, S, A>, tag: u32, ) -> Result<(), String> { let tag: NoteTag = tag.into(); - let execution_mode = match tag.execution_mode() { - NoteExecutionMode::Local => "Local", - NoteExecutionMode::Network => "Network", + let execution_mode = match tag.execution_hint() { + NoteExecutionHint::Local => "Local", + NoteExecutionHint::Network => "Network", }; info!( "adding tag - Single Target? {} - Execution mode: {}", @@ -79,8 +73,8 @@ fn add_tag<N: NodeRpcClient, R: FeltRng, S: Store>( Ok(()) } -fn remove_tag<N: NodeRpcClient, R: FeltRng, S: Store>( - mut client: Client<N, R, S>, +fn remove_tag<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + mut client: Client<N, R, S, A>, tag: u32, ) -> Result<(), String> { client.remove_note_tag(tag.into())?; diff --git a/src/cli/transactions.rs b/src/cli/transactions.rs index 52d2a5fc2..413d1b1e8 100644 --- a/src/cli/transactions.rs +++ b/src/cli/transactions.rs @@ -1,358 +1,35 @@ -use std::io; - -use clap::ValueEnum; use miden_client::{ - client::{ - rpc::NodeRpcClient, - transactions::{ - transaction_request::{PaymentTransactionData, TransactionTemplate}, - TransactionRecord, TransactionResult, - }, - }, + client::{rpc::NodeRpcClient, transactions::TransactionRecord}, store::{Store, TransactionFilter}, }; -use miden_objects::{ - assets::{Asset, FungibleAsset}, - crypto::rand::FeltRng, - notes::{NoteId, NoteType as MidenNoteType}, - transaction::TransactionId, - Digest, -}; +use miden_objects::crypto::rand::FeltRng; +use miden_tx::TransactionAuthenticator; -use super::{get_input_note_with_id_prefix, parse_account_id, Client, Parser}; +use super::{Client, Parser}; use crate::cli::create_dynamic_table; -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum NoteType { - Public, - Private, -} - -impl From<&NoteType> for MidenNoteType { - fn from(note_type: &NoteType) -> Self { - match note_type { - NoteType::Public => MidenNoteType::Public, - NoteType::Private => MidenNoteType::OffChain, - } - } -} - -#[derive(Clone, Debug, Parser)] -#[clap()] -pub enum TransactionType { - /// Create a pay-to-id transaction. - P2ID { - /// Sender account ID or its hex prefix. If none is provided, the default account's ID is used instead - #[clap(short = 's', long = "source")] - sender_account_id: Option<String>, - /// Target account ID or its hex prefix - #[clap(short = 't', long = "target")] - target_account_id: String, - /// Faucet account ID or its hex prefix - #[clap(short = 'f', long = "faucet")] - faucet_id: String, - amount: u64, - #[clap(short, long, value_enum)] - note_type: NoteType, - }, - /// Mint `amount` tokens from the specified fungible faucet (corresponding to `faucet_id`). The created note can then be then consumed by - /// `target_account_id`. - Mint { - /// Target account ID or its hex prefix - #[clap(short = 't', long = "target")] - target_account_id: String, - /// Faucet account ID or its hex prefix - #[clap(short = 'f', long = "faucet")] - faucet_id: String, - amount: u64, - #[clap(short, long, value_enum)] - note_type: NoteType, - }, - /// Create a pay-to-id with recall transaction. - P2IDR { - /// Sender account ID or its hex prefix. If none is provided, the default account's ID is used instead - #[clap(short = 's', long = "source")] - sender_account_id: Option<String>, - /// Target account ID or its hex prefix - #[clap(short = 't', long = "target")] - target_account_id: String, - /// Faucet account ID or its hex prefix - #[clap(short = 'f', long = "faucet")] - faucet_id: String, - amount: u64, - recall_height: u32, - #[clap(short, long, value_enum)] - note_type: NoteType, - }, - /// Consume with the account corresponding to `account_id` all of the notes from `list_of_notes`. - ConsumeNotes { - /// The account ID to be used to consume the note or its hex prefix. If none is provided, the default - /// account's ID is used instead - #[clap(short = 'a', long = "account")] - account_id: Option<String>, - /// A list of note IDs or the hex prefixes of their corresponding IDs - list_of_notes: Vec<String>, - }, -} - #[derive(Default, Debug, Parser, Clone)] -#[clap(about = "Execute and view transactions. Defaults to `list` command.")] -pub enum Transaction { +#[clap(about = "Manage and view transactions. Defaults to `list` command.")] +pub struct TransactionCmd { /// List currently tracked transactions - #[default] - #[clap(short_flag = 'l')] - List, - /// Execute a transaction, prove and submit it to the node. Once submitted, it - /// gets tracked by the client - #[clap(short_flag = 'n')] - New { - #[clap(subcommand)] - transaction_type: TransactionType, - /// Flag to submit the executed transaction without asking for confirmation - #[clap(short, long, default_value_t = false)] - force: bool, - }, + #[clap(short, long, group = "action")] + list: bool, } -impl Transaction { - pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store>( +impl TransactionCmd { + pub async fn execute<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( &self, - mut client: Client<N, R, S>, - default_account_id: Option<String>, + client: Client<N, R, S, A>, ) -> Result<(), String> { - match self { - Transaction::List => { - list_transactions(client)?; - }, - Transaction::New { transaction_type, force } => { - let transaction_id = - new_transaction(&mut client, transaction_type, *force, default_account_id) - .await?; - match transaction_id { - Some((transaction_id, output_note_ids)) => { - println!("Succesfully created transaction."); - println!("Transaction ID: {}", transaction_id); - println!("Output notes:"); - output_note_ids.iter().for_each(|note_id| println!("\t- {}", note_id)); - }, - None => { - println!("Transaction was cancelled."); - }, - } - }, - } + list_transactions(client)?; Ok(()) } } -// NEW TRANSACTION -// ================================================================================================ -async fn new_transaction<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &mut Client<N, R, S>, - transaction_type: &TransactionType, - force: bool, - default_account_id: Option<String>, -) -> Result<Option<(TransactionId, Vec<NoteId>)>, String> { - let transaction_template: TransactionTemplate = - build_transaction_template(client, transaction_type, default_account_id)?; - - let transaction_request = client.build_transaction_request(transaction_template)?; - - println!("Executing transaction..."); - let transaction_execution_result = client.new_transaction(transaction_request)?; - - // Show delta and ask for confirmation - print_transaction_details(&transaction_execution_result); - if !force { - println!("Continue with proving and submission? Changes will be irreversible once the proof is finalized on the rollup (Y/N)"); - let mut proceed_str: String = String::new(); - io::stdin().read_line(&mut proceed_str).expect("Should read line"); - - if proceed_str.trim().to_lowercase() != "y" { - return Ok(None); - } - } - - println!("Proving transaction and then submitting it to node..."); - - let transaction_id = transaction_execution_result.executed_transaction().id(); - let output_notes = transaction_execution_result - .created_notes() - .iter() - .map(|note| note.id()) - .collect::<Vec<_>>(); - client.submit_transaction(transaction_execution_result).await?; - - Ok(Some((transaction_id, output_notes))) -} - -fn print_transaction_details(transaction_result: &TransactionResult) { - println!( - "The transaction will have the following effects on the account with ID {}", - transaction_result.executed_transaction().account_id() - ); - - let account_delta = transaction_result.account_delta(); - let mut table = create_dynamic_table(&["Storage Slot", "Effect"]); - - for cleared_item_slot in account_delta.storage().cleared_items.iter() { - table.add_row(vec![cleared_item_slot.to_string(), "Cleared".to_string()]); - } - - for (updated_item_slot, new_value) in account_delta.storage().updated_items.iter() { - let value_digest: Digest = new_value.into(); - table.add_row(vec![ - updated_item_slot.to_string(), - format!("Updated ({})", value_digest.to_hex()), - ]); - } - - println!("Storage changes:"); - println!("{table}"); - - let mut table = create_dynamic_table(&["Asset Type", "Faucet ID", "Amount"]); - - for asset in account_delta.vault().added_assets.iter() { - let (asset_type, faucet_id, amount) = match asset { - Asset::Fungible(fungible_asset) => { - ("Fungible Asset", fungible_asset.faucet_id(), fungible_asset.amount()) - }, - Asset::NonFungible(non_fungible_asset) => { - ("Non Fungible Asset", non_fungible_asset.faucet_id(), 1) - }, - }; - table.add_row(vec![asset_type, &faucet_id.to_hex(), &format!("+{}", amount)]); - } - - for asset in account_delta.vault().removed_assets.iter() { - let (asset_type, faucet_id, amount) = match asset { - Asset::Fungible(fungible_asset) => { - ("Fungible Asset", fungible_asset.faucet_id(), fungible_asset.amount()) - }, - Asset::NonFungible(non_fungible_asset) => { - ("Non Fungible Asset", non_fungible_asset.faucet_id(), 1) - }, - }; - table.add_row(vec![asset_type, &faucet_id.to_hex(), &format!("-{}", amount)]); - } - - println!("Vault changes:"); - println!("{table}"); - - if let Some(new_nonce) = account_delta.nonce() { - println!("New nonce: {new_nonce}.") - } else { - println!("No nonce changes.") - } -} - -/// Builds a [TransactionTemplate] based on the transaction type provided via cli args -/// -/// For all transactions it'll try to find the corresponding accounts by using the -/// account IDs prefixes -/// -/// For [TransactionTemplate::ConsumeNotes], it'll try to find the corresponding notes by using the -/// provided IDs as prefixes -fn build_transaction_template<N: NodeRpcClient, R: FeltRng, S: Store>( - client: &Client<N, R, S>, - transaction_type: &TransactionType, - default_account_id: Option<String>, -) -> Result<TransactionTemplate, String> { - match transaction_type { - TransactionType::P2ID { - sender_account_id, - target_account_id, - faucet_id, - amount, - note_type, - } => { - let faucet_id = parse_account_id(client, faucet_id)?; - let fungible_asset = - FungibleAsset::new(faucet_id, *amount).map_err(|err| err.to_string())?.into(); - - // try to use either the provided argument or the default account - let sender_account_id = sender_account_id - .clone() - .or(default_account_id) - .ok_or("Neither a sender nor a default account was provided".to_string())?; - let sender_account_id = parse_account_id(client, &sender_account_id)?; - let target_account_id = parse_account_id(client, target_account_id)?; - - let payment_transaction = - PaymentTransactionData::new(fungible_asset, sender_account_id, target_account_id); - - Ok(TransactionTemplate::PayToId(payment_transaction, note_type.into())) - }, - TransactionType::P2IDR { - sender_account_id, - target_account_id, - faucet_id, - amount, - recall_height, - note_type, - } => { - let faucet_id = parse_account_id(client, faucet_id)?; - let fungible_asset = - FungibleAsset::new(faucet_id, *amount).map_err(|err| err.to_string())?.into(); - - // try to use either the provided argument or the default account - let sender_account_id = sender_account_id - .clone() - .or(default_account_id) - .ok_or("Neither a sender nor a default account was provided".to_string())?; - let sender_account_id = parse_account_id(client, &sender_account_id)?; - let target_account_id = parse_account_id(client, target_account_id)?; - - let payment_transaction = - PaymentTransactionData::new(fungible_asset, sender_account_id, target_account_id); - Ok(TransactionTemplate::PayToIdWithRecall( - payment_transaction, - *recall_height, - note_type.into(), - )) - }, - TransactionType::Mint { - faucet_id, - target_account_id, - amount, - note_type, - } => { - let faucet_id = parse_account_id(client, faucet_id)?; - let fungible_asset = - FungibleAsset::new(faucet_id, *amount).map_err(|err| err.to_string())?; - let target_account_id = parse_account_id(client, target_account_id)?; - - Ok(TransactionTemplate::MintFungibleAsset( - fungible_asset, - target_account_id, - note_type.into(), - )) - }, - TransactionType::ConsumeNotes { account_id, list_of_notes } => { - let list_of_notes = list_of_notes - .iter() - .map(|note_id| { - get_input_note_with_id_prefix(client, note_id) - .map(|note_record| note_record.id()) - .map_err(|err| err.to_string()) - }) - .collect::<Result<Vec<NoteId>, _>>()?; - - let account_id = account_id - .clone() - .or(default_account_id) - .ok_or("Neither a sender nor a default account was provided".to_string())?; - let account_id = parse_account_id(client, &account_id)?; - - Ok(TransactionTemplate::ConsumeNotes(account_id, list_of_notes)) - }, - } -} - // LIST TRANSACTIONS // ================================================================================================ -fn list_transactions<N: NodeRpcClient, R: FeltRng, S: Store>( - client: Client<N, R, S>, +fn list_transactions<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator>( + client: Client<N, R, S, A>, ) -> Result<(), String> { let transactions = client.get_transactions(TransactionFilter::All)?; print_transactions_summary(&transactions); diff --git a/src/client/accounts.rs b/src/client/accounts.rs index 5740d2443..b2a4d5aee 100644 --- a/src/client/accounts.rs +++ b/src/client/accounts.rs @@ -1,21 +1,17 @@ use miden_lib::AuthScheme; use miden_objects::{ accounts::{ - Account, AccountData, AccountId, AccountStorageType, AccountStub, AccountType, AuthData, + Account, AccountData, AccountId, AccountStorageType, AccountStub, AccountType, + AuthSecretKey, }, assets::TokenSymbol, - crypto::{ - dsa::rpo_falcon512::SecretKey, - rand::{FeltRng, RpoRandomCoin}, - }, - Digest, Felt, Word, + crypto::{dsa::rpo_falcon512::SecretKey, rand::FeltRng}, + Felt, Word, }; +use miden_tx::TransactionAuthenticator; use super::{rpc::NodeRpcClient, Client}; -use crate::{ - errors::ClientError, - store::{AuthInfo, Store}, -}; +use crate::{errors::ClientError, store::Store}; pub enum AccountTemplate { BasicWallet { @@ -46,7 +42,7 @@ impl From<AccountStorageMode> for AccountStorageType { } } -impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { +impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> { // ACCOUNT CREATION // -------------------------------------------------------------------------------------------- @@ -81,34 +77,20 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { /// Will panic when trying to import a non-new account without a seed since this functionality /// is not currently implemented pub fn import_account(&mut self, account_data: AccountData) -> Result<(), ClientError> { - match account_data.auth { - AuthData::RpoFalcon512Seed(key_pair_seed) => { - let seed = Digest::try_from(&key_pair_seed)?.into(); - let mut rng = RpoRandomCoin::new(seed); - - let key_pair = SecretKey::with_rng(&mut rng); - - let account_seed = if !account_data.account.is_new() - && account_data.account_seed.is_some() - { - tracing::warn!("Imported an existing account and still provided a seed when it is not needed. It's possible that the account's file was incorrectly generated. The seed will be ignored."); - // Ignore the seed since it's not a new account - - // TODO: The alternative approach to this is to store the seed anyway, but - // ignore it at the point of executing against this transaction, but that - // approach seems a little bit more incorrect - None - } else { - account_data.account_seed - }; - - self.insert_account( - &account_data.account, - account_seed, - &AuthInfo::RpoFalcon512(key_pair), - ) - }, - } + let account_seed = if !account_data.account.is_new() && account_data.account_seed.is_some() + { + tracing::warn!("Imported an existing account and still provided a seed when it is not needed. It's possible that the account's file was incorrectly generated. The seed will be ignored."); + // Ignore the seed since it's not a new account + + // TODO: The alternative approach to this is to store the seed anyway, but + // ignore it at the point of executing against this transaction, but that + // approach seems a little bit more incorrect + None + } else { + account_data.account_seed + }; + + self.insert_account(&account_data.account, account_seed, &account_data.auth_secret_key) } /// Creates a new regular account and saves it in the store along with its seed and auth data @@ -141,7 +123,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { ) }?; - self.insert_account(&account, Some(seed), &AuthInfo::RpoFalcon512(key_pair))?; + self.insert_account(&account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair))?; Ok((account, seed)) } @@ -170,7 +152,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { auth_scheme, )?; - self.insert_account(&account, Some(seed), &AuthInfo::RpoFalcon512(key_pair))?; + self.insert_account(&account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair))?; Ok((account, seed)) } @@ -184,7 +166,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { &mut self, account: &Account, account_seed: Option<Word>, - auth_info: &AuthInfo, + auth_info: &AuthSecretKey, ) -> Result<(), ClientError> { if account.is_new() && account_seed.is_none() { return Err(ClientError::ImportNewAccountWithoutSeed); @@ -219,13 +201,13 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { self.store.get_account_stub(account_id).map_err(|err| err.into()) } - /// Returns an [AuthInfo] object utilized to authenticate an account. + /// Returns an [AuthSecretKey] object utilized to authenticate an account. /// /// # Errors /// /// Returns a [ClientError::StoreError] with a [StoreError::AccountDataNotFound](crate::errors::StoreError::AccountDataNotFound) if the provided ID does /// not correspond to an existing account. - pub fn get_account_auth(&self, account_id: AccountId) -> Result<AuthInfo, ClientError> { + pub fn get_account_auth(&self, account_id: AccountId) -> Result<AuthSecretKey, ClientError> { self.store.get_account_auth(account_id).map_err(|err| err.into()) } } @@ -236,17 +218,15 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { #[cfg(test)] pub mod tests { use miden_objects::{ - accounts::{Account, AccountData, AccountId, AuthData}, + accounts::{Account, AccountData, AccountId, AuthSecretKey}, crypto::dsa::rpo_falcon512::SecretKey, Word, }; - use crate::{ - mock::{ - get_account_with_default_account_code, get_new_account_with_default_account_code, - ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_REGULAR, - }, - store::{sqlite_store::tests::create_test_client, AuthInfo}, + use crate::mock::{ + create_test_client, get_account_with_default_account_code, + get_new_account_with_default_account_code, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_REGULAR, }; fn create_account_data(account_id: u64) -> AccountData { @@ -256,7 +236,7 @@ pub mod tests { AccountData::new( account.clone(), Some(Word::default()), - AuthData::RpoFalcon512Seed([0; 32]), + AuthSecretKey::RpoFalcon512(SecretKey::new()), ) } @@ -285,10 +265,10 @@ pub mod tests { let key_pair = SecretKey::new(); assert!(client - .insert_account(&account, None, &AuthInfo::RpoFalcon512(key_pair.clone())) + .insert_account(&account, None, &AuthSecretKey::RpoFalcon512(key_pair.clone())) .is_err()); assert!(client - .insert_account(&account, Some(Word::default()), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&account, Some(Word::default()), &AuthSecretKey::RpoFalcon512(key_pair)) .is_ok()); } diff --git a/src/client/chain_data.rs b/src/client/chain_data.rs index 8962a9173..30695bf18 100644 --- a/src/client/chain_data.rs +++ b/src/client/chain_data.rs @@ -1,6 +1,7 @@ use miden_objects::crypto::rand::FeltRng; #[cfg(test)] use miden_objects::BlockHeader; +use miden_tx::TransactionAuthenticator; #[cfg(test)] use crate::{ @@ -10,7 +11,7 @@ use crate::{ }; #[cfg(test)] -impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { +impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> { pub fn get_block_headers_in_range( &self, start: u32, diff --git a/src/client/mod.rs b/src/client/mod.rs index 5693ea8ff..7254eaf00 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -4,7 +4,7 @@ use miden_objects::{ crypto::rand::{FeltRng, RpoRandomCoin}, Felt, }; -use miden_tx::TransactionExecutor; +use miden_tx::{TransactionAuthenticator, TransactionExecutor}; use rand::Rng; use tracing::info; @@ -18,7 +18,8 @@ pub mod accounts; mod chain_data; mod note_screener; mod notes; -pub(crate) mod sync; +pub mod store_authenticator; +pub mod sync; pub mod transactions; pub use note_screener::NoteRelevance; pub(crate) use note_screener::NoteScreener; @@ -35,7 +36,7 @@ pub use notes::ConsumableNote; /// - Connects to one or more Miden nodes to periodically sync with the current state of the /// network. /// - Executes, proves, and submits transactions to the network as directed by the user. -pub struct Client<N: NodeRpcClient, R: FeltRng, S: Store> { +pub struct Client<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> { /// The client's store, which provides a way to write and read entities to provide persistence. store: Rc<S>, /// An instance of [FeltRng] which provides randomness tools for generating new keys, @@ -44,10 +45,10 @@ pub struct Client<N: NodeRpcClient, R: FeltRng, S: Store> { /// An instance of [NodeRpcClient] which provides a way for the client to connect to the /// Miden node. rpc_api: N, - tx_executor: TransactionExecutor<ClientDataStore<S>>, + tx_executor: TransactionExecutor<ClientDataStore<S>, A>, } -impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { +impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> { // CONSTRUCTOR // -------------------------------------------------------------------------------------------- @@ -62,6 +63,8 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { /// - `executor_store`: An instance of [Store] that provides a way for [TransactionExecutor] to /// retrieve relevant inputs at the moment of transaction execution. It should be the same /// store as the one for `store`, but it doesn't have to be the **same instance**. + /// - `authenticator`: Defines the transaction authenticator that will be used by the + /// transaction executor whenever a signature is requested from within the VM. /// - `in_debug_mode`: Instantiates the transaction executor (and in turn, its compiler) /// in debug mode, which will enable debug logs for scripts compiled with this mode for /// easier MASM debugging. @@ -69,14 +72,14 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { /// # Errors /// /// Returns an error if the client could not be instantiated. - pub fn new(api: N, rng: R, store: S, in_debug_mode: bool) -> Self { + pub fn new(api: N, rng: R, store: Rc<S>, authenticator: A, in_debug_mode: bool) -> Self { if in_debug_mode { info!("Creating the Client in debug mode."); } - let store = Rc::new(store); let data_store = ClientDataStore::new(store.clone()); - let tx_executor = TransactionExecutor::new(data_store); + let authenticator = Some(Rc::new(authenticator)); + let tx_executor = TransactionExecutor::new(data_store, authenticator); Self { store, rng, rpc_api: api, tx_executor } } diff --git a/src/client/note_screener.rs b/src/client/note_screener.rs index e70915e70..c5618cac5 100644 --- a/src/client/note_screener.rs +++ b/src/client/note_screener.rs @@ -1,9 +1,9 @@ -use alloc::collections::BTreeSet; +use alloc::{collections::BTreeSet, rc::Rc}; use core::fmt; use miden_objects::{accounts::AccountId, assets::Asset, notes::Note, Word}; -use super::transactions::transaction_request::known_script_hashs::{P2ID, P2IDR, SWAP}; +use super::transactions::transaction_request::known_script_roots::{P2ID, P2IDR, SWAP}; use crate::{ errors::{InvalidNoteInputsError, ScreenerError}, store::Store, @@ -26,12 +26,12 @@ impl fmt::Display for NoteRelevance { } } -pub struct NoteScreener<'a, S: Store> { - store: &'a S, +pub struct NoteScreener<S: Store> { + store: Rc<S>, } -impl<'a, S: Store> NoteScreener<'a, S> { - pub fn new(store: &'a S) -> Self { +impl<S: Store> NoteScreener<S> { + pub fn new(store: Rc<S>) -> Self { Self { store } } @@ -183,57 +183,3 @@ impl<'a, S: Store> NoteScreener<'a, S> { .collect()) } } - -#[cfg(test)] -mod tests { - use miden_lib::notes::{create_p2id_note, create_p2idr_note, create_swap_note}; - use miden_objects::{ - accounts::{AccountId, ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN}, - assets::FungibleAsset, - crypto::rand::RpoRandomCoin, - notes::NoteType, - }; - - use crate::client::transactions::transaction_request::known_script_hashs::{P2ID, P2IDR, SWAP}; - - // We need to make sure the script roots we use for filters are in line with the note scripts - // coming from Miden objects - #[test] - fn ensure_correct_script_roots() { - // create dummy data for the notes - let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(); - let account_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(); - let rng = RpoRandomCoin::new(Default::default()); - - // create dummy notes to compare note script roots - let p2id_note = create_p2id_note( - account_id, - account_id, - vec![FungibleAsset::new(faucet_id, 100u64).unwrap().into()], - NoteType::OffChain, - rng, - ) - .unwrap(); - let p2idr_note = create_p2idr_note( - account_id, - account_id, - vec![FungibleAsset::new(faucet_id, 100u64).unwrap().into()], - NoteType::OffChain, - 10, - rng, - ) - .unwrap(); - let (swap_note, _serial_num) = create_swap_note( - account_id, - FungibleAsset::new(faucet_id, 100u64).unwrap().into(), - FungibleAsset::new(faucet_id, 100u64).unwrap().into(), - NoteType::OffChain, - rng, - ) - .unwrap(); - - assert_eq!(p2id_note.script().hash().to_string(), P2ID); - assert_eq!(p2idr_note.script().hash().to_string(), P2IDR); - assert_eq!(swap_note.script().hash().to_string(), SWAP); - } -} diff --git a/src/client/notes.rs b/src/client/notes.rs index 8ffca071f..4a78c68c3 100644 --- a/src/client/notes.rs +++ b/src/client/notes.rs @@ -4,7 +4,8 @@ use miden_objects::{ crypto::rand::FeltRng, notes::{NoteId, NoteInclusionProof, NoteScript}, }; -use miden_tx::ScriptTarget; +use miden_tx::{ScriptTarget, TransactionAuthenticator}; +use tracing::info; use super::{note_screener::NoteRelevance, rpc::NodeRpcClient, Client}; use crate::{ @@ -23,7 +24,7 @@ pub struct ConsumableNote { pub relevances: Vec<(AccountId, NoteRelevance)>, } -impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { +impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> { // INPUT NOTE DATA RETRIEVAL // -------------------------------------------------------------------------------------------- @@ -41,7 +42,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { ) -> Result<Vec<ConsumableNote>, ClientError> { let commited_notes = self.store.get_input_notes(NoteFilter::Committed)?; - let note_screener = NoteScreener::new(self.store.as_ref()); + let note_screener = NoteScreener::new(self.store.clone()); let mut relevant_notes = Vec::new(); for input_note in commited_notes { @@ -101,11 +102,11 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { /// not the method verifies the existence of the note in the chain. /// /// If the imported note is verified to be on chain and it doesn't contain an inclusion proof - /// the method tries to build one if possible. + /// the method tries to build one. /// If the verification fails then a [ClientError::ExistenceVerificationError] is raised. pub async fn import_input_note( &mut self, - mut note: InputNoteRecord, + note: InputNoteRecord, verify: bool, ) -> Result<(), ClientError> { if !verify { @@ -120,44 +121,50 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { } let note_details = chain_notes.pop().expect("chain_notes should have at least one element"); + let inclusion_details = note_details.inclusion_details(); - let inclusion_details = match note_details { - super::rpc::NoteDetails::OffChain(_, _, inclusion) => inclusion, - super::rpc::NoteDetails::Public(_, inclusion) => inclusion, - }; - - // Check to see if it's possible to create an inclusion proof if the note doesn't have one. - // Only do this if the note exists in the chain and the client is synced to a height equal or - // greater than the note's creation block. - if note.inclusion_proof().is_none() - && self.get_sync_height()? >= inclusion_details.block_num - { + // If the note exists in the chain and the client is synced to a height equal or + // greater than the note's creation block, get MMR and block header data for the + // note's block. Additionally create the inclusion proof if none is provided. + let inclusion_proof = if self.get_sync_height()? >= inclusion_details.block_num { // Add the inclusion proof to the imported note - let block_header = self - .rpc_api - .get_block_header_by_number(Some(inclusion_details.block_num)) - .await?; + info!("Requesting MMR data for past block num {}", inclusion_details.block_num); + let block_header = + self.get_and_store_authenticated_block(inclusion_details.block_num).await?; - let inclusion_proof = NoteInclusionProof::new( + let built_inclusion_proof = NoteInclusionProof::new( inclusion_details.block_num, block_header.sub_hash(), block_header.note_root(), inclusion_details.note_index.into(), - inclusion_details.merkle_path, + inclusion_details.merkle_path.clone(), )?; - note = InputNoteRecord::new( - note.id(), - note.recipient(), - note.assets().clone(), - note.status(), - note.metadata().copied(), - Some(inclusion_proof), - note.details().clone(), - None, - ); - } + // If the imported note already provides an inclusion proof, check that + // it equals the one we constructed from node data. + if let Some(proof) = note.inclusion_proof() { + if proof != &built_inclusion_proof { + return Err(ClientError::NoteImportError( + "Constructed inclusion proof does not equal the provided one".to_string(), + )); + } + } + + Some(built_inclusion_proof) + } else { + None + }; + let note = InputNoteRecord::new( + note.id(), + note.recipient(), + note.assets().clone(), + note.status(), + note.metadata().copied(), + inclusion_proof, + note.details().clone(), + None, + ); self.store.insert_input_note(¬e).map_err(|err| err.into()) } diff --git a/src/client/rpc/mod.rs b/src/client/rpc/mod.rs index 8f4cfa151..b7e64c1bd 100644 --- a/src/client/rpc/mod.rs +++ b/src/client/rpc/mod.rs @@ -3,7 +3,7 @@ use core::fmt; use async_trait::async_trait; use miden_objects::{ accounts::{Account, AccountId}, - crypto::merkle::{MerklePath, MmrDelta}, + crypto::merkle::{MerklePath, MmrDelta, MmrProof}, notes::{Note, NoteId, NoteMetadata, NoteTag}, transaction::ProvenTransaction, BlockHeader, Digest, @@ -23,6 +23,15 @@ pub enum NoteDetails { Public(Note, NoteInclusionDetails), } +impl NoteDetails { + pub fn inclusion_details(&self) -> &NoteInclusionDetails { + match self { + NoteDetails::OffChain(_, _, inclusion_details) => inclusion_details, + NoteDetails::Public(_, inclusion_details) => inclusion_details, + } + } +} + /// Describes the possible responses from the `GetAccountDetails` endpoint for an account pub enum AccountDetails { OffChain(AccountId, AccountUpdateSummary), @@ -75,13 +84,16 @@ pub trait NodeRpcClient { ) -> Result<(), NodeRpcClientError>; /// Given a block number, fetches the block header corresponding to that height from the node - /// using the `/GetBlockHeaderByNumber` endpoint + /// using the `/GetBlockHeaderByNumber` endpoint. + /// If `include_mmr_proof` is set to true and the function returns an `Ok`, the second value + /// of the return tuple should always be Some(MmrProof) /// /// When `None` is provided, returns info regarding the latest block async fn get_block_header_by_number( &mut self, - block_number: Option<u32>, - ) -> Result<BlockHeader, NodeRpcClientError>; + block_num: Option<u32>, + include_mmr_proof: bool, + ) -> Result<(BlockHeader, Option<MmrProof>), NodeRpcClientError>; /// Fetches note-related data for a list of [NoteId] using the `/GetNotesById` rpc endpoint /// diff --git a/src/client/rpc/tonic_client.rs b/src/client/rpc/tonic_client.rs index c88a31181..41bfa5024 100644 --- a/src/client/rpc/tonic_client.rs +++ b/src/client/rpc/tonic_client.rs @@ -14,6 +14,7 @@ use miden_node_proto::{ }; use miden_objects::{ accounts::{Account, AccountId}, + crypto::merkle::{MerklePath, MmrProof}, notes::{Note, NoteId, NoteMetadata, NoteTag, NoteType}, transaction::ProvenTransaction, utils::Deserializable, @@ -90,8 +91,13 @@ impl NodeRpcClient for TonicRpcClient { async fn get_block_header_by_number( &mut self, block_num: Option<u32>, - ) -> Result<BlockHeader, NodeRpcClientError> { - let request = GetBlockHeaderByNumberRequest { block_num }; + include_mmr_proof: bool, + ) -> Result<(BlockHeader, Option<MmrProof>), NodeRpcClientError> { + let request = GetBlockHeaderByNumberRequest { + block_num, + include_mmr_proof: Some(include_mmr_proof), + }; + let rpc_api = self.rpc_api().await?; let api_response = rpc_api.get_block_header_by_number(request).await.map_err(|err| { NodeRpcClientError::RequestError( @@ -100,12 +106,38 @@ impl NodeRpcClient for TonicRpcClient { ) })?; - api_response - .into_inner() + let response = api_response.into_inner(); + + let block_header: BlockHeader = response .block_header .ok_or(NodeRpcClientError::ExpectedFieldMissing("BlockHeader".into()))? .try_into() - .map_err(|err: ConversionError| NodeRpcClientError::ConversionFailure(err.to_string())) + .map_err(|err: ConversionError| { + NodeRpcClientError::ConversionFailure(err.to_string()) + })?; + + let mmr_proof = if include_mmr_proof { + let forest = response + .chain_length + .ok_or(NodeRpcClientError::ExpectedFieldMissing("ChainLength".into()))?; + let merkle_path: MerklePath = response + .mmr_path + .ok_or(NodeRpcClientError::ExpectedFieldMissing("MmrPath".into()))? + .try_into() + .map_err(|err: ConversionError| { + NodeRpcClientError::ConversionFailure(err.to_string()) + })?; + + Some(MmrProof { + forest: forest as usize, + position: block_header.block_num() as usize, + merkle_path, + }) + } else { + None + }; + + Ok((block_header, mmr_proof)) } async fn get_notes_by_id( @@ -126,8 +158,11 @@ impl NodeRpcClient for TonicRpcClient { let rpc_notes = api_response.into_inner().notes; let mut response_notes = Vec::with_capacity(rpc_notes.len()); for note in rpc_notes { - let sender_id = - note.sender.ok_or(NodeRpcClientError::ExpectedFieldMissing("Sender".into()))?; + let sender_id = note + .metadata + .clone() + .and_then(|metadata| metadata.sender) + .ok_or(NodeRpcClientError::ExpectedFieldMissing("Metadata.Sender".into()))?; let inclusion_details = { let merkle_path = note @@ -147,7 +182,11 @@ impl NodeRpcClient for TonicRpcClient { }, // Off-chain notes do not have details None => { - let note_tag = NoteTag::from(note.tag).validate(NoteType::OffChain)?; + let tag = note + .metadata + .ok_or(NodeRpcClientError::ExpectedFieldMissing("Metadata".into()))? + .tag; + let note_tag = NoteTag::from(tag).validate(NoteType::OffChain)?; let note_metadata = NoteMetadata::new( sender_id.try_into()?, NoteType::OffChain, @@ -310,17 +349,26 @@ impl TryFrom<SyncStateResponse> for StateSyncInfo { .try_into()?; let sender_account_id = note - .sender - .ok_or(NodeRpcClientError::ExpectedFieldMissing("Notes.Sender".into()))? + .metadata + .clone() + .and_then(|m| m.sender) + .ok_or(NodeRpcClientError::ExpectedFieldMissing("Notes.Metadata.Sender".into()))? .try_into()?; - let note_type = NoteType::try_from(Felt::new(note.note_type.into()))?; - let metadata = NoteMetadata::new( - sender_account_id, - note_type, - note.tag.into(), - Default::default(), - )?; + let tag = note + .metadata + .clone() + .ok_or(NodeRpcClientError::ExpectedFieldMissing("Notes.Metadata".into()))? + .tag; + + let note_type = note + .metadata + .ok_or(NodeRpcClientError::ExpectedFieldMissing("Notes.Metadata".into()))? + .note_type; + + let note_type = NoteType::try_from(note_type)?; + let metadata = + NoteMetadata::new(sender_account_id, note_type, tag.into(), Default::default())?; let committed_note = CommittedNote::new(note_id, note.note_index, merkle_path, metadata); diff --git a/src/client/store_authenticator.rs b/src/client/store_authenticator.rs new file mode 100644 index 000000000..e6976af82 --- /dev/null +++ b/src/client/store_authenticator.rs @@ -0,0 +1,102 @@ +use alloc::rc::Rc; +use core::cell::RefCell; + +use miden_objects::{ + accounts::{AccountDelta, AuthSecretKey}, + crypto::dsa::rpo_falcon512::{self, Polynomial}, + Digest, Felt, Word, +}; +use miden_tx::{AuthenticationError, TransactionAuthenticator}; +use rand::Rng; + +use crate::store::Store; + +/// Represents an authenticator based on a [Store] +pub struct StoreAuthenticator<R, S> { + store: Rc<S>, + rng: RefCell<R>, +} + +impl<R: Rng, S: Store> StoreAuthenticator<R, S> { + pub fn new_with_rng(store: Rc<S>, rng: R) -> Self { + StoreAuthenticator { store, rng: RefCell::new(rng) } + } +} + +impl<R: Rng, S: Store> TransactionAuthenticator for StoreAuthenticator<R, S> { + /// Gets a signature over a message, given a public key. + /// + /// The pub key should correspond to one of the keys tracked by the authenticator's store. + /// + /// # Errors + /// If the public key is not found in the store, [AuthenticationError::UnknownKey] is + /// returned. + fn get_signature( + &self, + pub_key: Word, + message: Word, + _account_delta: &AccountDelta, + ) -> Result<Vec<Felt>, AuthenticationError> { + let mut rng = self.rng.borrow_mut(); + + let secret_key = self + .store + .get_account_auth_by_pub_key(pub_key) + .map_err(|_| AuthenticationError::UnknownKey(format!("{}", Digest::from(pub_key))))?; + + let AuthSecretKey::RpoFalcon512(k) = secret_key; + get_falcon_signature(&k, message, &mut *rng) + } +} +// HELPER FUNCTIONS +// ================================================================================================ + +// TODO: Remove the falcon signature function once it's available on base and made public + +/// Retrieves a falcon signature over a message. +/// Gets as input a [Word] containing a secret key, and a [Word] representing a message and +/// outputs a vector of values to be pushed onto the advice stack. +/// The values are the ones required for a Falcon signature verification inside the VM and they are: +/// +/// 1. The nonce represented as 8 field elements. +/// 2. The expanded public key represented as the coefficients of a polynomial of degree < 512. +/// 3. The signature represented as the coefficients of a polynomial of degree < 512. +/// 4. The product of the above two polynomials in the ring of polynomials with coefficients +/// in the Miden field. +/// +/// # Errors +/// Will return an error if either: +/// - The secret key is malformed due to either incorrect length or failed decoding. +/// - The signature generation failed. +/// +/// TODO: once this gets made public in miden base, remve this implementation and use the one from +/// base +fn get_falcon_signature<R: Rng>( + key: &rpo_falcon512::SecretKey, + message: Word, + rng: &mut R, +) -> Result<Vec<Felt>, AuthenticationError> { + // Generate the signature + let sig = key.sign_with_rng(message, rng); + // The signature is composed of a nonce and a polynomial s2 + // The nonce is represented as 8 field elements. + let nonce = sig.nonce(); + // We convert the signature to a polynomial + let s2 = sig.sig_poly(); + // We also need in the VM the expanded key corresponding to the public key the was provided + // via the operand stack + let h = key.compute_pub_key_poly().0; + // Lastly, for the probabilistic product routine that is part of the verification procedure, + // we need to compute the product of the expanded key and the signature polynomial in + // the ring of polynomials with coefficients in the Miden field. + let pi = Polynomial::mul_modulo_p(&h, s2); + // We now push the nonce, the expanded key, the signature polynomial, and the product of the + // expanded key and the signature polynomial to the advice stack. + let mut result: Vec<Felt> = nonce.to_elements().to_vec(); + + result.extend(h.coefficients.iter().map(|a| Felt::from(a.value() as u32))); + result.extend(s2.coefficients.iter().map(|a| Felt::from(a.value() as u32))); + result.extend(pi.iter().map(|a| Felt::new(*a))); + result.reverse(); + Ok(result) +} diff --git a/src/client/sync.rs b/src/client/sync.rs index 6b74e0d3a..5a2ae1c39 100644 --- a/src/client/sync.rs +++ b/src/client/sync.rs @@ -1,16 +1,15 @@ use alloc::collections::{BTreeMap, BTreeSet}; -use std::{cmp::max, collections::HashMap}; +use core::cmp::max; use crypto::merkle::{InOrderIndex, MmrDelta, MmrPeaks, PartialMmr}; use miden_objects::{ accounts::{Account, AccountId, AccountStub}, - crypto::{self, rand::FeltRng}, - notes::{ - Note, NoteExecutionMode, NoteId, NoteInclusionProof, NoteInputs, NoteRecipient, NoteTag, - }, + crypto::{self, merkle::MerklePath, rand::FeltRng}, + notes::{Note, NoteId, NoteInclusionProof, NoteInputs, NoteRecipient, NoteTag}, transaction::{InputNote, TransactionId}, BlockHeader, Digest, }; +use miden_tx::TransactionAuthenticator; use tracing::{info, warn}; use super::{ @@ -154,7 +153,7 @@ pub struct StateSyncUpdate { /// The number of bits to shift identifiers for in use of filters. pub const FILTER_ID_SHIFT: u8 = 48; -impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { +impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> { // SYNC STATE // -------------------------------------------------------------------------------------------- @@ -231,7 +230,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { /// Calls `get_block_header_by_number` requesting the genesis block and storing it /// in the local database async fn retrieve_and_store_genesis(&mut self) -> Result<(), ClientError> { - let genesis_block = self.rpc_api.get_block_header_by_number(Some(0)).await?; + let (genesis_block, _) = self.rpc_api.get_block_header_by_number(Some(0), false).await?; let blank_mmr_peaks = MmrPeaks::new(0, vec![]).expect("Blank MmrPeaks should not fail to instantiate"); @@ -253,7 +252,9 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { let account_note_tags: Vec<NoteTag> = accounts .iter() - .map(|acc| NoteTag::from_account_id(acc.id(), NoteExecutionMode::Local)) + .map(|acc| { + NoteTag::from_account_id(acc.id(), miden_objects::notes::NoteExecutionHint::Local) + }) .collect::<Result<Vec<_>, _>>()?; let stored_note_tags: Vec<NoteTag> = self.store.get_note_tags()?; @@ -265,14 +266,11 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { .filter_map(|note| note.metadata().map(|metadata| metadata.tag())) .collect(); - //TODO: Use BTreeSet to remove duplicates more efficiently once `Ord` is implemented for `NoteTag` let note_tags: Vec<NoteTag> = [account_note_tags, stored_note_tags, uncommited_note_tags] .concat() .into_iter() - .map(|tag| (tag.to_string(), tag)) - .collect::<HashMap<String, NoteTag>>() - .values() - .cloned() + .collect::<BTreeSet<NoteTag>>() + .into_iter() .collect(); // To receive information about added nullifiers, we reduce them to the higher 16 bits @@ -297,6 +295,12 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { return Ok(SyncStatus::SyncedToLastBlock(SyncSummary::new_empty(current_block_num))); } + let committed_note_ids: Vec<NoteId> = response + .note_inclusions + .iter() + .map(|committed_note| *(committed_note.note_id())) + .collect(); + let new_note_details = self.get_note_details(response.note_inclusions, &response.block_header).await?; @@ -329,18 +333,12 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { )? }; - let updated_output_note_ids: Vec<NoteId> = new_note_details - .updated_output_notes() - .iter() - .map(|(output_note_id, _)| *output_note_id) - .collect(); - let uncommitted_transactions = self.store.get_transactions(TransactionFilter::Uncomitted)?; let transactions_to_commit = get_transactions_to_commit( &uncommitted_transactions, - &updated_output_note_ids, + &committed_note_ids, &new_nullifiers, &response.account_hash_updates, ); @@ -533,7 +531,7 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { // We'll only do the check for either incoming public notes or pending input notes as // output notes are not really candidates to be consumed here. - let note_screener = NoteScreener::new(self.store.as_ref()); + let note_screener = NoteScreener::new(self.store.clone()); // Find all relevant Input Notes using the note checker for input_note in committed_notes.updated_input_notes() { @@ -642,6 +640,54 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { } Ok(()) } + + /// Retrieves and stores a [BlockHeader] by number, and stores its authentication data as well. + pub(crate) async fn get_and_store_authenticated_block( + &mut self, + block_num: u32, + ) -> Result<BlockHeader, ClientError> { + let mut current_partial_mmr = self.build_current_partial_mmr()?; + + if current_partial_mmr.is_tracked(block_num as usize) { + warn!("Current partial MMR already contains the requested data"); + let (block_header, _) = self.store.get_block_header_by_num(block_num)?; + return Ok(block_header); + } + + let (block_header, mmr_proof) = + self.rpc_api.get_block_header_by_number(Some(block_num), true).await?; + + let mut path_nodes: Vec<(InOrderIndex, Digest)> = vec![]; + + let mmr_proof = mmr_proof + .expect("NodeRpcApi::get_block_header_by_number() should have returned an MMR proof"); + // Trim merkle path to keep nodes relevant to our current PartialMmr + let rightmost_index = InOrderIndex::from_leaf_pos(current_partial_mmr.forest() - 1); + let mut idx = InOrderIndex::from_leaf_pos(block_num as usize); + for node in mmr_proof.merkle_path { + idx = idx.sibling(); + // Rightmost index is always the biggest value, so if the path contains any node + // past it, we can discard it for our version of the forest + if idx > rightmost_index { + continue; + } + path_nodes.push((idx, node)); + idx = idx.parent(); + } + + let merkle_path = MerklePath::new(path_nodes.iter().map(|(_, n)| *n).collect()); + + current_partial_mmr + .track(block_num as usize, block_header.hash(), &merkle_path) + .map_err(StoreError::MmrError)?; + + // Insert header and MMR nodes + self.store + .insert_block_header(block_header, current_partial_mmr.peaks(), true)?; + self.store.insert_chain_mmr_nodes(&path_nodes)?; + + Ok(block_header) + } } // UTILS diff --git a/src/client/transactions/mod.rs b/src/client/transactions/mod.rs index a72089fad..0a9e3d556 100644 --- a/src/client/transactions/mod.rs +++ b/src/client/transactions/mod.rs @@ -1,28 +1,30 @@ use alloc::collections::{BTreeMap, BTreeSet}; -use miden_lib::notes::{create_p2id_note, create_p2idr_note}; +use miden_lib::notes::{create_p2id_note, create_p2idr_note, create_swap_note}; use miden_objects::{ - accounts::{AccountDelta, AccountId}, + accounts::{AccountDelta, AccountId, AuthSecretKey}, assembly::ProgramAst, assets::FungibleAsset, crypto::rand::RpoRandomCoin, - notes::{Note, NoteId, NoteType}, + notes::{Note, NoteDetails, NoteId, NoteType}, transaction::{ - ExecutedTransaction, InputNotes, OutputNotes, ProvenTransaction, TransactionArgs, - TransactionId, TransactionScript, + ExecutedTransaction, InputNotes, OutputNote, OutputNotes, ProvenTransaction, + TransactionArgs, TransactionId, TransactionScript, }, Digest, Felt, Word, }; -use miden_tx::{ProvingOptions, ScriptTarget, TransactionProver}; +use miden_tx::{ProvingOptions, ScriptTarget, TransactionAuthenticator, TransactionProver}; use rand::Rng; use tracing::info; -use self::transaction_request::{PaymentTransactionData, TransactionRequest, TransactionTemplate}; -use super::{note_screener::NoteRelevance, rpc::NodeRpcClient, Client, FeltRng}; +use self::transaction_request::{ + PaymentTransactionData, SwapTransactionData, TransactionRequest, TransactionTemplate, +}; +use super::{rpc::NodeRpcClient, Client, FeltRng}; use crate::{ client::NoteScreener, errors::ClientError, - store::{AuthInfo, Store, TransactionFilter}, + store::{InputNoteRecord, Store, TransactionFilter}, }; pub mod transaction_request; @@ -30,66 +32,68 @@ pub mod transaction_request; // TRANSACTION RESULT // -------------------------------------------------------------------------------------------- -/// Represents the result of executing a transaction by the client +/// Represents the result of executing a transaction by the client. /// -/// It contains an [ExecutedTransaction], a list of [Note] that describe the details of the notes -/// created by the transaction execution, and a list of `usize` `relevant_notes` that contain the -/// indices of `output_notes` that are relevant to the client +/// It contains an [ExecutedTransaction], and a list of `relevant_notes` that contains the +/// `output_notes` that the client has to store as input notes, based on the NoteScreener +/// output from filtering the transaction's output notes or some partial note we expect to receive +/// in the future (you can check at swap notes for an example of this). pub struct TransactionResult { - executed_transaction: ExecutedTransaction, - output_notes: Vec<Note>, - relevant_notes: Option<BTreeMap<usize, Vec<(AccountId, NoteRelevance)>>>, + transaction: ExecutedTransaction, + relevant_notes: Vec<InputNoteRecord>, } impl TransactionResult { - pub fn new(executed_transaction: ExecutedTransaction, created_notes: Vec<Note>) -> Self { - Self { - executed_transaction, - output_notes: created_notes, - relevant_notes: None, + /// Screens the output notes to store and track the relevant ones, and instantiates a [TransactionResult] + pub fn new<S: Store>( + transaction: ExecutedTransaction, + note_screener: NoteScreener<S>, + partial_notes: Vec<NoteDetails>, + ) -> Result<Self, ClientError> { + let mut relevant_notes = vec![]; + + for note in notes_from_output(transaction.output_notes()) { + let account_relevance = note_screener.check_relevance(note)?; + + if !account_relevance.is_empty() { + relevant_notes.push(note.clone().into()); + } } - } - pub fn executed_transaction(&self) -> &ExecutedTransaction { - &self.executed_transaction + // Include partial output notes into the relevant notes + relevant_notes.extend(partial_notes.iter().map(InputNoteRecord::from)); + + let tx_result = Self { transaction, relevant_notes }; + + Ok(tx_result) } - pub fn created_notes(&self) -> &Vec<Note> { - &self.output_notes + pub fn executed_transaction(&self) -> &ExecutedTransaction { + &self.transaction } - pub fn relevant_notes(&self) -> Vec<&Note> { - if let Some(relevant_notes) = &self.relevant_notes { - relevant_notes - .keys() - .map(|note_index| &self.output_notes[*note_index]) - .collect() - } else { - self.created_notes().iter().collect() - } + pub fn created_notes(&self) -> &OutputNotes { + self.transaction.output_notes() } - pub fn set_relevant_notes( - &mut self, - relevant_notes: BTreeMap<usize, Vec<(AccountId, NoteRelevance)>>, - ) { - self.relevant_notes = Some(relevant_notes); + pub fn relevant_notes(&self) -> &[InputNoteRecord] { + &self.relevant_notes } pub fn block_num(&self) -> u32 { - self.executed_transaction.block_header().block_num() + self.transaction.block_header().block_num() } pub fn transaction_arguments(&self) -> &TransactionArgs { - self.executed_transaction.tx_args() + self.transaction.tx_args() } pub fn account_delta(&self) -> &AccountDelta { - self.executed_transaction.account_delta() + self.transaction.account_delta() } pub fn consumed_notes(&self) -> &InputNotes { - self.executed_transaction.tx_inputs().input_notes() + self.transaction.tx_inputs().input_notes() } } @@ -158,7 +162,7 @@ impl std::fmt::Display for TransactionStatus { } } -impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { +impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> { // TRANSACTION DATA RETRIEVAL // -------------------------------------------------------------------------------------------- @@ -182,26 +186,33 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { let account_id = transaction_template.account_id(); let account_auth = self.store.get_account_auth(account_id)?; + let (_pk, _sk) = match account_auth { + AuthSecretKey::RpoFalcon512(key) => { + (key.public_key(), AuthSecretKey::RpoFalcon512(key)) + }, + }; + match transaction_template { TransactionTemplate::ConsumeNotes(_, notes) => { let program_ast = ProgramAst::parse(transaction_request::AUTH_CONSUME_NOTES_SCRIPT) .expect("shipped MASM is well-formed"); let notes = notes.iter().map(|id| (*id, None)).collect(); - let tx_script = { - let script_inputs = vec![account_auth.into_advice_inputs()]; - self.tx_executor.compile_tx_script(program_ast, script_inputs, vec![])? - }; - Ok(TransactionRequest::new(account_id, notes, vec![], Some(tx_script))) + let tx_script = self.tx_executor.compile_tx_script(program_ast, vec![], vec![])?; + Ok(TransactionRequest::new(account_id, notes, vec![], vec![], Some(tx_script))) }, TransactionTemplate::MintFungibleAsset(asset, target_account_id, note_type) => { - self.build_mint_tx_request(asset, account_auth, target_account_id, note_type) + self.build_mint_tx_request(asset, target_account_id, note_type) }, TransactionTemplate::PayToId(payment_data, note_type) => { - self.build_p2id_tx_request(account_auth, payment_data, None, note_type) + self.build_p2id_tx_request(payment_data, None, note_type) + }, + TransactionTemplate::PayToIdWithRecall(payment_data, recall_height, note_type) => { + self.build_p2id_tx_request(payment_data, Some(recall_height), note_type) + }, + TransactionTemplate::Swap(swap_data, note_type) => { + self.build_swap_tx_request(swap_data, note_type) }, - TransactionTemplate::PayToIdWithRecall(payment_data, recall_height, note_type) => self - .build_p2id_tx_request(account_auth, payment_data, Some(recall_height), note_type), } } @@ -225,8 +236,8 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { let block_num = self.store.get_sync_height()?; let note_ids = transaction_request.get_input_note_ids(); - let output_notes = transaction_request.expected_output_notes().to_vec(); + let partial_notes = transaction_request.expected_partial_notes().to_vec(); // Execute the transaction and get the witness let executed_transaction = self.tx_executor.execute_transaction( @@ -236,20 +247,29 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { transaction_request.into(), )?; - // Check that the expected output notes is a subset of the transaction's output notes - let tx_note_ids: BTreeSet<NoteId> = - executed_transaction.output_notes().iter().map(|n| n.id()).collect(); + // Check that the expected output notes matches the transaction outcome. + // We comprare authentication hashes where possible since that involves note IDs + metadata + // (as opposed to just note ID which remains the same regardless of metadata) + // We also do the check for partial output notes + let tx_note_auth_hashes: BTreeSet<Digest> = + notes_from_output(executed_transaction.output_notes()) + .map(Note::authentication_hash) + .collect(); let missing_note_ids: Vec<NoteId> = output_notes .iter() - .filter_map(|n| (!tx_note_ids.contains(&n.id())).then_some(n.id())) + .filter_map(|n| { + (!tx_note_auth_hashes.contains(&n.authentication_hash())).then_some(n.id()) + }) .collect(); if !missing_note_ids.is_empty() { return Err(ClientError::MissingOutputNotes(missing_note_ids)); } - Ok(TransactionResult::new(executed_transaction, output_notes)) + let screener = NoteScreener::new(self.store.clone()); + + TransactionResult::new(executed_transaction, screener, partial_notes) } /// Proves the specified transaction witness, submits it to the node, and stores the transaction in @@ -267,19 +287,6 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { self.submit_proven_transaction_request(proven_transaction.clone()).await?; - let note_screener = NoteScreener::new(self.store.as_ref()); - let mut relevant_notes = BTreeMap::new(); - - for (idx, note) in tx_result.created_notes().iter().enumerate() { - let account_relevance = note_screener.check_relevance(note)?; - if !account_relevance.is_empty() { - relevant_notes.insert(idx, account_relevance); - } - } - - let mut tx_result = tx_result; - tx_result.set_relevant_notes(relevant_notes); - // Transaction was proven and submitted to the node correctly, persist note details and update account self.store.apply_transaction(tx_result)?; @@ -328,7 +335,6 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { /// - If recall_height is Some(), a P2IDR note will be created. Otherwise, a P2ID is created. fn build_p2id_tx_request( &self, - auth_info: AuthInfo, payment_data: PaymentTransactionData, recall_height: Option<u32>, note_type: NoteType, @@ -355,7 +361,8 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { }; let recipient = created_note - .recipient_digest() + .recipient() + .digest() .iter() .map(|x| x.as_int().to_string()) .collect::<Vec<_>>() @@ -372,15 +379,63 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { ) .expect("shipped MASM is well-formed"); - let tx_script = { - let script_inputs = vec![auth_info.into_advice_inputs()]; - self.tx_executor.compile_tx_script(tx_script, script_inputs, vec![])? - }; + let tx_script = self.tx_executor.compile_tx_script(tx_script, vec![], vec![])?; Ok(TransactionRequest::new( payment_data.account_id(), BTreeMap::new(), vec![created_note], + vec![], + Some(tx_script), + )) + } + + /// Helper to build a [TransactionRequest] for Swap-type transactions easily. + /// + /// - auth_info has to be from the executor account + fn build_swap_tx_request( + &self, + swap_data: SwapTransactionData, + note_type: NoteType, + ) -> Result<TransactionRequest, ClientError> { + let random_coin = self.get_random_coin(); + + // The created note is the one that we need as the output of the tx, the other one is the + // one that we expect to receive and consume eventually + let (created_note, payback_note_details) = create_swap_note( + swap_data.account_id(), + swap_data.offered_asset(), + swap_data.requested_asset(), + note_type, + random_coin, + )?; + + let recipient = created_note + .recipient() + .digest() + .iter() + .map(|x| x.as_int().to_string()) + .collect::<Vec<_>>() + .join("."); + + let note_tag = created_note.metadata().tag().inner(); + + let tx_script = ProgramAst::parse( + &transaction_request::AUTH_SEND_ASSET_SCRIPT + .replace("{recipient}", &recipient) + .replace("{note_type}", &Felt::new(note_type as u64).to_string()) + .replace("{tag}", &Felt::new(note_tag.into()).to_string()) + .replace("{asset}", &prepare_word(&swap_data.offered_asset().into()).to_string()), + ) + .expect("shipped MASM is well-formed"); + + let tx_script = self.tx_executor.compile_tx_script(tx_script, vec![], vec![])?; + + Ok(TransactionRequest::new( + swap_data.account_id(), + BTreeMap::new(), + vec![created_note], + vec![payback_note_details], Some(tx_script), )) } @@ -391,7 +446,6 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { fn build_mint_tx_request( &self, asset: FungibleAsset, - faucet_auth_info: AuthInfo, target_account_id: AccountId, note_type: NoteType, ) -> Result<TransactionRequest, ClientError> { @@ -405,7 +459,8 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { )?; let recipient = created_note - .recipient_digest() + .recipient() + .digest() .iter() .map(|x| x.as_int().to_string()) .collect::<Vec<_>>() @@ -422,15 +477,13 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { ) .expect("shipped MASM is well-formed"); - let tx_script = { - let script_inputs = vec![faucet_auth_info.into_advice_inputs()]; - self.tx_executor.compile_tx_script(tx_script, script_inputs, vec![])? - }; + let tx_script = self.tx_executor.compile_tx_script(tx_script, vec![], vec![])?; Ok(TransactionRequest::new( asset.faucet_id(), BTreeMap::new(), vec![created_note], + vec![], Some(tx_script), )) } @@ -442,3 +495,21 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store> Client<N, R, S> { pub(crate) fn prepare_word(word: &Word) -> String { word.iter().map(|x| x.as_int().to_string()).collect::<Vec<_>>().join(".") } + +/// Extracts notes from [OutputNotes] +/// Used for: +/// - checking the relevance of notes to save them as input notes +/// - validate hashes versus expected output notes after a transaction is executed +pub(crate) fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> { + output_notes + .iter() + .filter(|n| matches!(n, OutputNote::Full(_))) + .map(|n| match n { + OutputNote::Full(n) => n, + // The following todo!() applies until we have a way to support flows where we have + // partial details of the note + OutputNote::Header(_) => { + todo!("For now, all details should be held in OutputNote::Fulls") + }, + }) +} diff --git a/src/client/transactions/transaction_request.rs b/src/client/transactions/transaction_request.rs index 4a3d78fb1..e12dedf6f 100644 --- a/src/client/transactions/transaction_request.rs +++ b/src/client/transactions/transaction_request.rs @@ -3,7 +3,7 @@ use alloc::collections::BTreeMap; use miden_objects::{ accounts::AccountId, assets::{Asset, FungibleAsset}, - notes::{Note, NoteId, NoteType}, + notes::{Note, NoteDetails, NoteId, NoteType}, transaction::{TransactionArgs, TransactionScript}, vm::AdviceMap, Word, @@ -33,6 +33,8 @@ pub struct TransactionRequest { input_notes: BTreeMap<NoteId, Option<NoteArgs>>, /// A list of notes expected to be generated by the transactions. expected_output_notes: Vec<Note>, + /// A list of note details of notes we expect to be created as part of future transactions. + expected_partial_notes: Vec<NoteDetails>, /// Optional transaction script (together with its arguments). tx_script: Option<TransactionScript>, } @@ -45,12 +47,14 @@ impl TransactionRequest { account_id: AccountId, input_notes: BTreeMap<NoteId, Option<NoteArgs>>, expected_output_notes: Vec<Note>, + expected_partial_notes: Vec<NoteDetails>, tx_script: Option<TransactionScript>, ) -> Self { Self { account_id, input_notes, expected_output_notes, + expected_partial_notes, tx_script, } } @@ -81,6 +85,10 @@ impl TransactionRequest { &self.expected_output_notes } + pub fn expected_partial_notes(&self) -> &[NoteDetails] { + &self.expected_partial_notes + } + pub fn tx_script(&self) -> Option<&TransactionScript> { self.tx_script.as_ref() } @@ -113,6 +121,8 @@ pub enum TransactionTemplate { /// Creates a pay-to-id note directed to a specific account, specifying a block height after /// which the note can be recalled PayToIdWithRecall(PaymentTransactionData, u32, NoteType), + /// Creates a swap note offering a specific asset in exchange for another specific asset + Swap(SwapTransactionData, NoteType), } impl TransactionTemplate { @@ -123,6 +133,7 @@ impl TransactionTemplate { TransactionTemplate::MintFungibleAsset(asset, ..) => asset.faucet_id(), TransactionTemplate::PayToId(payment_data, _) => payment_data.account_id(), TransactionTemplate::PayToIdWithRecall(payment_data, ..) => payment_data.account_id(), + TransactionTemplate::Swap(swap_data, ..) => swap_data.account_id(), } } } @@ -169,12 +180,112 @@ impl PaymentTransactionData { } } -// KNOWN SCRIPT HASHES +// SWAP TRANSACTION DATA // -------------------------------------------------------------------------------------------- -pub struct KnownScriptHash; -pub mod known_script_hashs { - pub const P2ID: &str = "0xcdfd70344b952980272119bc02b837d14c07bbfc54f86a254422f39391b77b35"; - pub const P2IDR: &str = "0x41e5727b99a12b36066c09854d39d64dd09d9265c442a9be3626897572bf1745"; - pub const SWAP: &str = "0x5852920f88985b651cf7ef5e48623f898b6c292f4a2c25dd788ff8b46dd90417"; +#[derive(Clone, Debug)] +pub struct SwapTransactionData { + sender_account_id: AccountId, + offered_asset: Asset, + requested_asset: Asset, +} + +impl SwapTransactionData { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + pub fn new( + sender_account_id: AccountId, + offered_asset: Asset, + requested_asset: Asset, + ) -> SwapTransactionData { + SwapTransactionData { + sender_account_id, + offered_asset, + requested_asset, + } + } + + /// Returns the executor [AccountId] + pub fn account_id(&self) -> AccountId { + self.sender_account_id + } + + /// Returns the transaction offered [Asset] + pub fn offered_asset(&self) -> Asset { + self.offered_asset + } + + /// Returns the transaction requested [Asset] + pub fn requested_asset(&self) -> Asset { + self.requested_asset + } +} + +// KNOWN SCRIPT ROOTS +// -------------------------------------------------------------------------------------------- + +pub mod known_script_roots { + pub const P2ID: &str = "0x0007b2229f7c8e3205a485a9879f1906798a2e27abd1706eaf58536e7cc3868b"; + pub const P2IDR: &str = "0x418ae31e80b53ddc99179d3cacbc4140c7b36ab04ddb26908b3a6ed2e40061d5"; + pub const SWAP: &str = "0xebbc82ad1688925175599bee2fb56bde649ebb9986fbce957ebee3eb4be5f140"; +} + +#[cfg(test)] +mod tests { + use miden_lib::notes::{create_p2id_note, create_p2idr_note, create_swap_note}; + use miden_objects::{ + accounts::{ + account_id::testing::{ + ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + }, + AccountId, + }, + assets::FungibleAsset, + crypto::rand::RpoRandomCoin, + notes::NoteType, + }; + + use crate::client::transactions::transaction_request::known_script_roots::{P2ID, P2IDR, SWAP}; + + // We need to make sure the script roots we use for filters are in line with the note scripts + // coming from Miden objects + #[test] + fn ensure_correct_script_roots() { + // create dummy data for the notes + let faucet_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(); + let account_id: AccountId = ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(); + let rng = RpoRandomCoin::new(Default::default()); + + // create dummy notes to compare note script roots + let p2id_note = create_p2id_note( + account_id, + account_id, + vec![FungibleAsset::new(faucet_id, 100u64).unwrap().into()], + NoteType::OffChain, + rng, + ) + .unwrap(); + let p2idr_note = create_p2idr_note( + account_id, + account_id, + vec![FungibleAsset::new(faucet_id, 100u64).unwrap().into()], + NoteType::OffChain, + 10, + rng, + ) + .unwrap(); + let (swap_note, _serial_num) = create_swap_note( + account_id, + FungibleAsset::new(faucet_id, 100u64).unwrap().into(), + FungibleAsset::new(faucet_id, 100u64).unwrap().into(), + NoteType::OffChain, + rng, + ) + .unwrap(); + + assert_eq!(p2id_note.script().hash().to_string(), P2ID); + assert_eq!(p2idr_note.script().hash().to_string(), P2IDR); + assert_eq!(swap_note.script().hash().to_string(), SWAP); + } } diff --git a/src/config.rs b/src/config.rs index 045c241de..2fbc46eb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -220,7 +220,7 @@ impl Default for RpcConfig { // CLI CONFIG // ================================================================================================ -#[derive(Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CliConfig { /// Address of the Miden node to connect to. pub default_account_id: Option<String>, diff --git a/src/errors.rs b/src/errors.rs index c65e8e567..371a6a69f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -3,7 +3,7 @@ use core::fmt; use miden_node_proto::errors::ConversionError; use miden_objects::{ accounts::AccountId, crypto::merkle::MmrError, notes::NoteId, AccountError, AssetError, - AssetVaultError, Digest, NoteError, TransactionScriptError, + AssetVaultError, Digest, NoteError, TransactionScriptError, Word, }; use miden_tx::{ utils::{DeserializationError, HexParseError}, @@ -22,6 +22,7 @@ pub enum ClientError { ImportNewAccountWithoutSeed, MissingOutputNotes(Vec<NoteId>), NoteError(NoteError), + NoteImportError(String), NoteRecordError(String), NoConsumableNoteForAccount(AccountId), NodeRpcClientError(NodeRpcClientError), @@ -48,7 +49,7 @@ impl fmt::Display for ClientError { ClientError::MissingOutputNotes(note_ids) => { write!( f, - "transaction error: The transaction did not produce expected Note IDs: {}", + "transaction error: The transaction did not produce the expected notes corresponding to Note IDs: {}", note_ids.iter().map(|&id| id.to_hex()).collect::<Vec<_>>().join(", ") ) }, @@ -56,6 +57,7 @@ impl fmt::Display for ClientError { write!(f, "No consumable note for account ID {}", account_id) }, ClientError::NoteError(err) => write!(f, "note error: {err}"), + ClientError::NoteImportError(err) => write!(f, "error importing note: {err}"), ClientError::NoteRecordError(err) => write!(f, "note record error: {err}"), ClientError::NodeRpcClientError(err) => write!(f, "rpc api error: {err}"), ClientError::ScreenerError(err) => write!(f, "note screener error: {err}"), @@ -155,6 +157,7 @@ pub enum StoreError { AccountDataNotFound(AccountId), AccountError(AccountError), AccountHashMismatch(AccountId), + AccountKeyNotFound(Word), AccountStorageNotFound(Digest), BlockHeaderNotFound(u32), ChainMmrNodeNotFound(u64), @@ -258,6 +261,9 @@ impl fmt::Display for StoreError { AccountHashMismatch(account_id) => { write!(f, "account hash mismatch for account {account_id}") }, + AccountKeyNotFound(pub_key) => { + write!(f, "error: Public Key {} not found", Digest::from(pub_key)) + }, AccountStorageNotFound(root) => { write!(f, "account storage data with root {} not found", root) }, diff --git a/src/mock.rs b/src/mock.rs index c6478d511..24a96573e 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -1,4 +1,5 @@ use alloc::collections::BTreeMap; +use std::{env::temp_dir, rc::Rc}; use async_trait::async_trait; use miden_lib::{transaction::TransactionKernel, AuthScheme}; @@ -6,19 +7,20 @@ use miden_node_proto::generated::{ account::AccountId as ProtoAccountId, block_header::BlockHeader as NodeBlockHeader, note::NoteSyncRecord, - requests::{GetBlockHeaderByNumberRequest, SyncStateRequest}, + requests::SyncStateRequest, responses::{NullifierUpdate, SyncStateResponse}, }; use miden_objects::{ accounts::{ - get_account_seed_single, Account, AccountCode, AccountId, AccountStorage, - AccountStorageType, AccountType, SlotItem, StorageSlot, ACCOUNT_ID_OFF_CHAIN_SENDER, + account_id::testing::ACCOUNT_ID_OFF_CHAIN_SENDER, get_account_seed_single, Account, + AccountCode, AccountId, AccountStorage, AccountStorageType, AccountType, AuthSecretKey, + SlotItem, StorageSlot, }, assembly::{Assembler, ModuleAst, ProgramAst}, assets::{Asset, AssetVault, FungibleAsset, TokenSymbol}, crypto::{ dsa::rpo_falcon512::SecretKey, - merkle::{Mmr, MmrDelta, NodeIndex, SimpleSmt}, + merkle::{Mmr, MmrDelta, MmrProof, NodeIndex, SimpleSmt}, rand::RpoRandomCoin, }, notes::{ @@ -30,13 +32,16 @@ use miden_objects::{ }; use rand::Rng; use tonic::{Response, Status}; +use uuid::Uuid; use crate::{ client::{ + get_random_coin, rpc::{ AccountDetails, NodeRpcClient, NodeRpcClientEndpoint, NoteDetails, NoteInclusionDetails, StateSyncInfo, }, + store_authenticator::StoreAuthenticator, sync::FILTER_ID_SHIFT, transactions::{ prepare_word, @@ -44,11 +49,13 @@ use crate::{ }, Client, }, + config::{ClientConfig, RpcConfig}, errors::NodeRpcClientError, - store::{sqlite_store::SqliteStore, AuthInfo}, + store::sqlite_store::SqliteStore, }; -pub type MockClient = Client<MockRpcApi, RpoRandomCoin, SqliteStore>; +pub type MockClient = + Client<MockRpcApi, RpoRandomCoin, SqliteStore, StoreAuthenticator<RpoRandomCoin, SqliteStore>>; // MOCK CONSTS // ================================================================================================ @@ -70,7 +77,7 @@ pub const DEFAULT_ACCOUNT_CODE: &str = " /// This struct implements the RPC API used by the client to communicate with the node. It is /// intended to be used for testing purposes only. pub struct MockRpcApi { - pub state_sync_requests: BTreeMap<SyncStateRequest, SyncStateResponse>, + pub state_sync_requests: BTreeMap<u32, SyncStateResponse>, pub genesis_block: BlockHeader, pub notes: BTreeMap<NoteId, InputNote>, } @@ -103,31 +110,29 @@ impl NodeRpcClient for MockRpcApi { _nullifiers_tags: &[u16], ) -> Result<StateSyncInfo, NodeRpcClientError> { // Match request -> response through block_num - let response = - match self.state_sync_requests.iter().find(|(req, _)| req.block_num == block_num) { - Some((_req, response)) => { - let response = response.clone(); - Ok(Response::new(response)) - }, - None => Err(NodeRpcClientError::RequestError( - NodeRpcClientEndpoint::SyncState.to_string(), - Status::not_found("no response for sync state request").to_string(), - )), - }?; + let response = match self.state_sync_requests.get(&block_num) { + Some(response) => { + let response = response.clone(); + Ok(Response::new(response)) + }, + None => Err(NodeRpcClientError::RequestError( + NodeRpcClientEndpoint::SyncState.to_string(), + Status::not_found("no response for sync state request").to_string(), + )), + }?; response.into_inner().try_into() } - /// Creates and executes a [GetBlockHeaderByNumberRequest]. + /// Creates and executes a [GetBlockHeaderByNumberRequest](miden_node_proto::generated::requests::GetBlockHeaderByNumberRequest). /// Only used for retrieving genesis block right now so that's the only case we need to cover. async fn get_block_header_by_number( &mut self, block_num: Option<u32>, - ) -> Result<BlockHeader, NodeRpcClientError> { - let request = GetBlockHeaderByNumberRequest { block_num }; - - if request.block_num == Some(0) { - return Ok(self.genesis_block); + _include_mmr_proof: bool, + ) -> Result<(BlockHeader, Option<MmrProof>), NodeRpcClientError> { + if block_num == Some(0) { + return Ok((self.genesis_block, None)); } panic!("get_block_header_by_number is supposed to be only used for genesis block") } @@ -184,8 +189,8 @@ fn create_mock_sync_state_request_for_account_and_notes( genesis_block: &BlockHeader, mmr_delta: Option<Vec<MmrDelta>>, tracked_block_headers: Option<Vec<BlockHeader>>, -) -> BTreeMap<SyncStateRequest, SyncStateResponse> { - let mut requests: BTreeMap<SyncStateRequest, SyncStateResponse> = BTreeMap::new(); +) -> BTreeMap<u32, SyncStateResponse> { + let mut requests: BTreeMap<u32, SyncStateResponse> = BTreeMap::new(); let accounts = vec![ProtoAccountId { id: u64::from(account_id) }]; @@ -258,6 +263,13 @@ fn create_mock_sync_state_request_for_account_and_notes( nullifiers: nullifiers.clone(), }; + let metadata = miden_node_proto::generated::note::NoteMetadata { + sender: Some(account.id().into()), + note_type: NoteType::OffChain as u32, + tag: NoteTag::for_local_use_case(1u16, 0u16).unwrap().into(), + aux: Default::default(), + }; + // create a state sync response let response = SyncStateResponse { chain_tip, @@ -267,9 +279,7 @@ fn create_mock_sync_state_request_for_account_and_notes( notes: vec![NoteSyncRecord { note_index: 0, note_id: Some(created_notes_iter.next().unwrap().id().into()), - sender: Some(account.id().into()), - tag: 0u32, - note_type: NoteType::OffChain as u32, + metadata: Some(metadata), merkle_path: Some(miden_node_proto::generated::merkle::MerklePath::default()), }], nullifiers: vec![NullifierUpdate { @@ -277,18 +287,15 @@ fn create_mock_sync_state_request_for_account_and_notes( block_num: 7, }], }; - requests.insert(request, response); + requests.insert(request.block_num, response); } requests } /// Generates mock sync state requests and responses -fn generate_state_sync_mock_requests() -> ( - BlockHeader, - BTreeMap<SyncStateRequest, SyncStateResponse>, - BTreeMap<NoteId, InputNote>, -) { +fn generate_state_sync_mock_requests( +) -> (BlockHeader, BTreeMap<u32, SyncStateResponse>, BTreeMap<NoteId, InputNote>) { let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR).unwrap(); // create sync state requests @@ -421,9 +428,9 @@ pub async fn insert_mock_data(client: &mut MockClient) -> Vec<BlockHeader> { } // insert account - let key_pair = SecretKey::new(); + let secret_key = SecretKey::new(); client - .insert_account(&account, Some(account_seed), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&account, Some(account_seed), &AuthSecretKey::RpoFalcon512(secret_key)) .unwrap(); let genesis_block = BlockHeader::mock(0, None, None, &[]); @@ -458,7 +465,7 @@ pub async fn create_mock_transaction(client: &mut MockClient) { .unwrap(); client - .insert_account(&sender_account, Some(seed), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&sender_account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); let key_pair = SecretKey::new(); @@ -478,7 +485,7 @@ pub async fn create_mock_transaction(client: &mut MockClient) { .unwrap(); client - .insert_account(&target_account, Some(seed), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&target_account, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); let key_pair = SecretKey::new(); @@ -502,7 +509,7 @@ pub async fn create_mock_transaction(client: &mut MockClient) { .unwrap(); client - .insert_account(&faucet, Some(seed), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&faucet, Some(seed), &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); let asset: miden_objects::assets::Asset = FungibleAsset::new(faucet.id(), 5u64).unwrap().into(); @@ -542,16 +549,19 @@ pub fn mock_fungible_faucet_account( let faucet_storage_slot_1 = [Felt::new(initial_balance), Felt::new(0), Felt::new(0), Felt::new(0)]; - let faucet_account_storage = AccountStorage::new(vec![ - SlotItem { - index: 0, - slot: StorageSlot::new_value(key_pair.public_key().into()), - }, - SlotItem { - index: 1, - slot: StorageSlot::new_value(faucet_storage_slot_1), - }, - ]) + let faucet_account_storage = AccountStorage::new( + vec![ + SlotItem { + index: 0, + slot: StorageSlot::new_value(key_pair.public_key().into()), + }, + SlotItem { + index: 1, + slot: StorageSlot::new_value(faucet_storage_slot_1), + }, + ], + vec![], + ) .unwrap(); Account::new( @@ -587,10 +597,13 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { let note_program_ast = ProgramAst::parse("begin push.1 drop end").unwrap(); let (note_script, _) = NoteScript::new(note_program_ast, assembler).unwrap(); + let note_tag: NoteTag = + NoteTag::from_account_id(sender, miden_objects::notes::NoteExecutionHint::Local).unwrap(); + // Created Notes const SERIAL_NUM_4: Word = [Felt::new(13), Felt::new(14), Felt::new(15), Felt::new(16)]; let note_metadata = - NoteMetadata::new(sender, NoteType::OffChain, 1u32.into(), Default::default()).unwrap(); + NoteMetadata::new(sender, NoteType::OffChain, note_tag, Default::default()).unwrap(); let note_assets = NoteAssets::new(vec![fungible_asset_1]).unwrap(); let note_recipient = NoteRecipient::new(SERIAL_NUM_4, note_script.clone(), NoteInputs::new(vec![]).unwrap()); @@ -599,7 +612,7 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { const SERIAL_NUM_5: Word = [Felt::new(17), Felt::new(18), Felt::new(19), Felt::new(20)]; let note_metadata = - NoteMetadata::new(sender, NoteType::OffChain, 2u32.into(), Default::default()).unwrap(); + NoteMetadata::new(sender, NoteType::OffChain, note_tag, Default::default()).unwrap(); let note_recipient = NoteRecipient::new(SERIAL_NUM_5, note_script.clone(), NoteInputs::new(vec![]).unwrap()); let note_assets = NoteAssets::new(vec![fungible_asset_2]).unwrap(); @@ -607,7 +620,7 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { const SERIAL_NUM_6: Word = [Felt::new(21), Felt::new(22), Felt::new(23), Felt::new(24)]; let note_metadata = - NoteMetadata::new(sender, NoteType::OffChain, 2u32.into(), Default::default()).unwrap(); + NoteMetadata::new(sender, NoteType::OffChain, note_tag, Default::default()).unwrap(); let note_assets = NoteAssets::new(vec![fungible_asset_3]).unwrap(); let note_recipient = NoteRecipient::new(SERIAL_NUM_6, note_script, NoteInputs::new(vec![Felt::new(2)]).unwrap()); @@ -639,10 +652,10 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { drop dropw dropw end ", - created_note_0_recipient = prepare_word(&created_notes[0].recipient_digest()), + created_note_0_recipient = prepare_word(&created_notes[0].recipient().digest()), created_note_0_tag = created_notes[0].metadata().tag(), created_note_0_asset = prepare_assets(created_notes[0].assets())[0], - created_note_1_recipient = prepare_word(&created_notes[1].recipient_digest()), + created_note_1_recipient = prepare_word(&created_notes[1].recipient().digest()), created_note_1_tag = created_notes[1].metadata().tag(), created_note_1_asset = prepare_assets(created_notes[1].assets())[0], ); @@ -662,7 +675,7 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { drop dropw dropw end ", - created_note_2_recipient = prepare_word(&created_notes[2].recipient_digest()), + created_note_2_recipient = prepare_word(&created_notes[2].recipient().digest()), created_note_2_tag = created_notes[2].metadata().tag(), created_note_2_asset = prepare_assets(created_notes[2].assets())[0], ); @@ -672,7 +685,7 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { // Consumed Notes const SERIAL_NUM_1: Word = [Felt::new(1), Felt::new(2), Felt::new(3), Felt::new(4)]; let note_metadata = - NoteMetadata::new(sender, NoteType::OffChain, 1u32.into(), Default::default()).unwrap(); + NoteMetadata::new(sender, NoteType::OffChain, note_tag, Default::default()).unwrap(); let note_recipient = NoteRecipient::new( SERIAL_NUM_1, note_2_script.clone(), @@ -683,7 +696,7 @@ pub fn mock_notes(assembler: &Assembler) -> (Vec<Note>, Vec<Note>) { const SERIAL_NUM_2: Word = [Felt::new(5), Felt::new(6), Felt::new(7), Felt::new(8)]; let note_metadata = - NoteMetadata::new(sender, NoteType::OffChain, 2u32.into(), Default::default()).unwrap(); + NoteMetadata::new(sender, NoteType::OffChain, note_tag, Default::default()).unwrap(); let note_assets = NoteAssets::new(vec![fungible_asset_2, fungible_asset_3]).unwrap(); let note_recipient = NoteRecipient::new( SERIAL_NUM_2, @@ -713,7 +726,7 @@ fn get_account_with_nonce( index: 0, slot: StorageSlot::new_value(public_key), }; - let account_storage = AccountStorage::new(vec![slot_item]).unwrap(); + let account_storage = AccountStorage::new(vec![slot_item], vec![]).unwrap(); let asset_vault = match assets { Some(asset) => AssetVault::new(&[asset]).unwrap(), @@ -748,3 +761,29 @@ fn prepare_assets(note_assets: &NoteAssets) -> Vec<String> { } assets } + +pub fn create_test_client() -> MockClient { + let store = create_test_store_path() + .into_os_string() + .into_string() + .unwrap() + .try_into() + .unwrap(); + + let client_config = ClientConfig::new(store, RpcConfig::default()); + + let rpc_endpoint = client_config.rpc.endpoint.to_string(); + let store = SqliteStore::new((&client_config).into()).unwrap(); + let store = Rc::new(store); + + let rng = get_random_coin(); + let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng); + + MockClient::new(MockRpcApi::new(&rpc_endpoint), rng, store, authenticator, true) +} + +pub(crate) fn create_test_store_path() -> std::path::PathBuf { + let mut temp_file = temp_dir(); + temp_file.push(format!("{}.sqlite3", Uuid::new_v4())); + temp_file +} diff --git a/src/store/mod.rs b/src/store/mod.rs index 3b7d61316..14e8764b4 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -2,15 +2,11 @@ use alloc::collections::BTreeMap; use clap::error::Result; use miden_objects::{ - accounts::{Account, AccountId, AccountStub}, - crypto::{ - dsa::rpo_falcon512::SecretKey, - merkle::{InOrderIndex, MmrPeaks}, - }, + accounts::{Account, AccountId, AccountStub, AuthSecretKey}, + crypto::merkle::{InOrderIndex, MmrPeaks}, notes::{NoteId, NoteTag, Nullifier}, - BlockHeader, Digest, Felt, Word, + BlockHeader, Digest, Word, }; -use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; use crate::{ client::{ @@ -54,7 +50,7 @@ pub trait Store { /// /// An update involves: /// - Applying the resulting [AccountDelta](miden_objects::accounts::AccountDelta) and storing the new [Account] state - /// - Storing new notes as a result of the transaction execution + /// - Storing new notes and payback note details as a result of the transaction execution /// - Inserting the transaction into the store to track fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), StoreError>; @@ -133,6 +129,11 @@ pub trait Store { filter: ChainMmrNodeFilter, ) -> Result<BTreeMap<InOrderIndex, Digest>, StoreError>; + /// Inserts MMR authentication nodes. + /// + /// In the case where the [InOrderIndex] already exists on the table, the insertion is ignored + fn insert_chain_mmr_nodes(&self, nodes: &[(InOrderIndex, Digest)]) -> Result<(), StoreError>; + /// Returns peaks information from the blockchain by a specific block number. /// /// If there is no chain MMR info stored for the provided block returns an empty [MmrPeaks] @@ -186,19 +187,27 @@ pub trait Store { /// Returns a `StoreError::AccountDataNotFound` if there is no account for the provided ID fn get_account(&self, account_id: AccountId) -> Result<(Account, Option<Word>), StoreError>; - /// Retrieves an account's [AuthInfo], utilized to authenticate the account. + /// Retrieves an account's [AuthSecretKey], utilized to authenticate the account. /// /// # Errors /// /// Returns a `StoreError::AccountDataNotFound` if there is no account for the provided ID - fn get_account_auth(&self, account_id: AccountId) -> Result<AuthInfo, StoreError>; + fn get_account_auth(&self, account_id: AccountId) -> Result<AuthSecretKey, StoreError>; + + /// Retrieves an account's [AuthSecretKey] by pub key, utilized to authenticate the account. + /// This is mainly used for authentication in transactions. + /// + /// # Errors + /// + /// Returns a `StoreError::AccountKeyNotFound` if there is no account for the provided key + fn get_account_auth_by_pub_key(&self, pub_key: Word) -> Result<AuthSecretKey, StoreError>; - /// Inserts an [Account] along with the seed used to create it and its [AuthInfo] + /// Inserts an [Account] along with the seed used to create it and its [AuthSecretKey] fn insert_account( &self, account: &Account, account_seed: Option<Word>, - auth_info: &AuthInfo, + auth_info: &AuthSecretKey, ) -> Result<(), StoreError>; // SYNC @@ -232,65 +241,6 @@ pub trait Store { fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError>; } -// DATABASE AUTH INFO -// ================================================================================================ - -/// Represents the types of authentication information of accounts -#[derive(Debug)] -pub enum AuthInfo { - RpoFalcon512(SecretKey), -} - -const RPO_FALCON512_AUTH: u8 = 0; - -impl AuthInfo { - /// Returns byte identifier of specific AuthInfo - const fn type_byte(&self) -> u8 { - match self { - AuthInfo::RpoFalcon512(_) => RPO_FALCON512_AUTH, - } - } - - /// Returns the authentication information as a tuple of (key, value) - /// that can be input to the advice map at the moment of transaction execution. - pub fn into_advice_inputs(self) -> (Word, Vec<Felt>) { - match self { - AuthInfo::RpoFalcon512(key) => { - let pub_key: Word = key.public_key().into(); - let mut pk_sk_bytes = key.to_bytes(); - pk_sk_bytes.append(&mut pub_key.to_bytes()); - - (pub_key, pk_sk_bytes.iter().map(|a| Felt::new(*a as u64)).collect::<Vec<Felt>>()) - }, - } - } -} - -impl Serializable for AuthInfo { - fn write_into<W: ByteWriter>(&self, target: &mut W) { - let mut bytes = vec![self.type_byte()]; - match self { - AuthInfo::RpoFalcon512(key_pair) => { - bytes.append(&mut key_pair.to_bytes()); - target.write_bytes(&bytes); - }, - } - } -} - -impl Deserializable for AuthInfo { - fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> { - let auth_type: u8 = source.read_u8()?; - match auth_type { - RPO_FALCON512_AUTH => { - let key_pair = SecretKey::read_from(source)?; - Ok(AuthInfo::RpoFalcon512(key_pair)) - }, - val => Err(DeserializationError::InvalidValue(val.to_string())), - } - } -} - // CHAIN MMR NODE FILTER // ================================================================================================ diff --git a/src/store/note_record/input_note_record.rs b/src/store/note_record/input_note_record.rs index 764f4b3a5..dee5509ee 100644 --- a/src/store/note_record/input_note_record.rs +++ b/src/store/note_record/input_note_record.rs @@ -1,7 +1,8 @@ use miden_objects::{ accounts::AccountId, notes::{ - Note, NoteAssets, NoteId, NoteInclusionProof, NoteInputs, NoteMetadata, NoteRecipient, + Note, NoteAssets, NoteDetails, NoteId, NoteInclusionProof, NoteInputs, NoteMetadata, + NoteRecipient, }, transaction::InputNote, utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, @@ -102,6 +103,27 @@ impl InputNoteRecord { } } +impl From<&NoteDetails> for InputNoteRecord { + fn from(note_details: &NoteDetails) -> Self { + InputNoteRecord { + id: note_details.id(), + assets: note_details.assets().clone(), + recipient: note_details.recipient().digest(), + metadata: None, + inclusion_proof: None, + status: NoteStatus::Pending, + details: NoteRecordDetails { + nullifier: note_details.nullifier().to_string(), + script_hash: note_details.script().hash(), + script: note_details.script().clone(), + inputs: note_details.inputs().to_vec(), + serial_num: note_details.serial_num(), + }, + consumer_account_id: None, + } + } +} + impl Serializable for InputNoteRecord { fn write_into<W: ByteWriter>(&self, target: &mut W) { self.id().write_into(target); @@ -141,7 +163,7 @@ impl From<Note> for InputNoteRecord { fn from(note: Note) -> Self { InputNoteRecord { id: note.id(), - recipient: note.recipient_digest(), + recipient: note.recipient().digest(), assets: note.assets().clone(), status: NoteStatus::Pending, metadata: Some(*note.metadata()), @@ -161,7 +183,7 @@ impl From<InputNote> for InputNoteRecord { fn from(recorded_note: InputNote) -> Self { InputNoteRecord { id: recorded_note.note().id(), - recipient: recorded_note.note().recipient_digest(), + recipient: recorded_note.note().recipient().digest(), assets: recorded_note.note().assets().clone(), status: NoteStatus::Pending, metadata: Some(*recorded_note.note().metadata()), diff --git a/src/store/note_record/output_note_record.rs b/src/store/note_record/output_note_record.rs index d4cceb4ce..5b562d0de 100644 --- a/src/store/note_record/output_note_record.rs +++ b/src/store/note_record/output_note_record.rs @@ -91,11 +91,15 @@ impl OutputNoteRecord { } } +// CONVERSIONS +// ================================================================================================ + +// TODO: Improve conversions by implementing into_parts() impl From<Note> for OutputNoteRecord { fn from(note: Note) -> Self { OutputNoteRecord { id: note.id(), - recipient: note.recipient_digest(), + recipient: note.recipient().digest(), assets: note.assets().clone(), status: NoteStatus::Pending, metadata: *note.metadata(), diff --git a/src/store/sqlite_store/accounts.rs b/src/store/sqlite_store/accounts.rs index 3a6294b6b..992fdfcca 100644 --- a/src/store/sqlite_store/accounts.rs +++ b/src/store/sqlite_store/accounts.rs @@ -1,7 +1,7 @@ use clap::error::Result; use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::{Account, AccountCode, AccountId, AccountStorage, AccountStub}, + accounts::{Account, AccountCode, AccountId, AccountStorage, AccountStub, AuthSecretKey}, assembly::{AstSerdeOptions, ModuleAst}, assets::{Asset, AssetVault}, Digest, Felt, Word, @@ -10,14 +10,14 @@ use miden_tx::utils::{Deserializable, Serializable}; use rusqlite::{params, Transaction}; use super::SqliteStore; -use crate::{errors::StoreError, store::AuthInfo}; +use crate::errors::StoreError; // TYPES // ================================================================================================ type SerializedAccountData = (i64, String, String, String, i64, bool); type SerializedAccountsParts = (i64, i64, String, String, String, Option<Vec<u8>>); -type SerializedAccountAuthData = (i64, Vec<u8>); +type SerializedAccountAuthData = (i64, Vec<u8>, Vec<u8>); type SerializedAccountAuthParts = (i64, Vec<u8>); type SerializedAccountVaultData = (String, String); @@ -104,7 +104,10 @@ impl SqliteStore { } /// Retrieve account keys data by Account Id - pub(crate) fn get_account_auth(&self, account_id: AccountId) -> Result<AuthInfo, StoreError> { + pub(crate) fn get_account_auth( + &self, + account_id: AccountId, + ) -> Result<AuthSecretKey, StoreError> { let account_id_int: u64 = account_id.into(); const QUERY: &str = "SELECT account_id, auth_info FROM account_auth WHERE account_id = ?"; self.db() @@ -119,7 +122,7 @@ impl SqliteStore { &self, account: &Account, account_seed: Option<Word>, - auth_info: &AuthInfo, + auth_info: &AuthSecretKey, ) -> Result<(), StoreError> { let mut db = self.db(); let tx = db.transaction()?; @@ -132,6 +135,18 @@ impl SqliteStore { Ok(tx.commit()?) } + + /// Returns an [AuthSecretKey] by a public key represented by a [Word] + pub fn get_account_auth_by_pub_key(&self, pub_key: Word) -> Result<AuthSecretKey, StoreError> { + let pub_key_bytes = pub_key.to_bytes(); + const QUERY: &str = "SELECT account_id, auth_info FROM account_auth WHERE pub_key = ?"; + self.db() + .prepare(QUERY)? + .query_map(params![pub_key_bytes], parse_account_auth_columns)? + .map(|result| Ok(result?).and_then(parse_account_auth)) + .next() + .ok_or(StoreError::AccountKeyNotFound(pub_key))? + } } // HELPERS @@ -199,15 +214,17 @@ pub(super) fn insert_account_asset_vault( Ok(()) } -/// Inserts an [AuthInfo] for the account with id `account_id` +/// Inserts an [AuthSecretKey] for the account with id `account_id` pub(super) fn insert_account_auth( tx: &Transaction<'_>, account_id: AccountId, - auth_info: &AuthInfo, + auth_info: &AuthSecretKey, ) -> Result<(), StoreError> { - let (account_id, auth_info) = serialize_account_auth(account_id, auth_info)?; - const QUERY: &str = "INSERT INTO account_auth (account_id, auth_info) VALUES (?, ?)"; - tx.execute(QUERY, params![account_id, auth_info])?; + let (account_id, auth_info, pub_key) = serialize_account_auth(account_id, auth_info)?; + const QUERY: &str = + "INSERT INTO account_auth (account_id, auth_info, pub_key) VALUES (?, ?, ?)"; + + tx.execute(QUERY, params![account_id, auth_info, pub_key])?; Ok(()) } @@ -293,23 +310,29 @@ fn parse_account_auth_columns( Ok((account_id, auth_info_bytes)) } -/// Parse an `AuthInfo` from the provided parts. +/// Parse an `AuthSecretKey` from the provided parts. fn parse_account_auth( serialized_account_auth_parts: SerializedAccountAuthParts, -) -> Result<AuthInfo, StoreError> { +) -> Result<AuthSecretKey, StoreError> { let (_, auth_info_bytes) = serialized_account_auth_parts; - let auth_info = AuthInfo::read_from_bytes(&auth_info_bytes)?; + let auth_info = AuthSecretKey::read_from_bytes(&auth_info_bytes)?; Ok(auth_info) } /// Serialized the provided account_auth into database compatible types. fn serialize_account_auth( account_id: AccountId, - auth_info: &AuthInfo, + auth_info: &AuthSecretKey, ) -> Result<SerializedAccountAuthData, StoreError> { + let pub_key = match auth_info { + AuthSecretKey::RpoFalcon512(secret) => Word::from(secret.public_key()), + } + .to_bytes(); + let account_id: u64 = account_id.into(); let auth_info = auth_info.to_bytes(); - Ok((account_id as i64, auth_info)) + + Ok((account_id as i64, auth_info, pub_key)) } /// Serialize the provided account_code into database compatible types. @@ -366,7 +389,7 @@ mod tests { }; use miden_tx::utils::{Deserializable, Serializable}; - use super::{insert_account_auth, AuthInfo}; + use super::{insert_account_auth, AuthSecretKey}; use crate::{ mock::DEFAULT_ACCOUNT_CODE, store::sqlite_store::{accounts::insert_account_code, tests::create_test_store}, @@ -400,11 +423,11 @@ mod tests { #[test] fn test_auth_info_serialization() { let exp_key_pair = SecretKey::new(); - let auth_info = AuthInfo::RpoFalcon512(exp_key_pair.clone()); + let auth_info = AuthSecretKey::RpoFalcon512(exp_key_pair.clone()); let bytes = auth_info.to_bytes(); - let actual = AuthInfo::read_from_bytes(&bytes).unwrap(); + let actual = AuthSecretKey::read_from_bytes(&bytes).unwrap(); match actual { - AuthInfo::RpoFalcon512(act_key_pair) => { + AuthSecretKey::RpoFalcon512(act_key_pair) => { assert_eq!(exp_key_pair.to_bytes(), act_key_pair.to_bytes()); assert_eq!(exp_key_pair.public_key(), act_key_pair.public_key()); }, @@ -421,14 +444,18 @@ mod tests { { let mut db = store.db(); let tx = db.transaction().unwrap(); - insert_account_auth(&tx, account_id, &AuthInfo::RpoFalcon512(exp_key_pair.clone())) - .unwrap(); + insert_account_auth( + &tx, + account_id, + &AuthSecretKey::RpoFalcon512(exp_key_pair.clone()), + ) + .unwrap(); tx.commit().unwrap(); } let account_auth = store.get_account_auth(account_id).unwrap(); match account_auth { - AuthInfo::RpoFalcon512(act_key_pair) => { + AuthSecretKey::RpoFalcon512(act_key_pair) => { assert_eq!(exp_key_pair.to_bytes(), act_key_pair.to_bytes()); assert_eq!(exp_key_pair.public_key(), act_key_pair.public_key()); }, diff --git a/src/store/sqlite_store/chain_data.rs b/src/store/sqlite_store/chain_data.rs index 185b1ea90..7430d320c 100644 --- a/src/store/sqlite_store/chain_data.rs +++ b/src/store/sqlite_store/chain_data.rs @@ -39,17 +39,12 @@ impl SqliteStore { chain_mmr_peaks: MmrPeaks, has_client_notes: bool, ) -> Result<(), StoreError> { - let chain_mmr_peaks = chain_mmr_peaks.peaks().to_vec(); - let (block_num, header, chain_mmr, has_client_notes) = - serialize_block_header(block_header, chain_mmr_peaks, has_client_notes)?; - const QUERY: &str = "\ - INSERT INTO block_headers - (block_num, header, chain_mmr_peaks, has_client_notes) - VALUES (?, ?, ?, ?)"; + let mut db = self.db(); + let tx = db.transaction()?; - self.db() - .execute(QUERY, params![block_num, header, chain_mmr, has_client_notes])?; + Self::insert_block_header_tx(&tx, block_header, chain_mmr_peaks, has_client_notes)?; + tx.commit()?; Ok(()) } @@ -123,8 +118,20 @@ impl SqliteStore { Ok(MmrPeaks::new(0, vec![])?) } + pub fn insert_chain_mmr_nodes( + &self, + nodes: &[(InOrderIndex, Digest)], + ) -> Result<(), StoreError> { + let mut db = self.db(); + let tx = db.transaction()?; + + Self::insert_chain_mmr_nodes_tx(&tx, nodes)?; + + Ok(tx.commit().map(|_| ())?) + } + /// Inserts a list of MMR authentication nodes to the Chain MMR nodes table. - pub(crate) fn insert_chain_mmr_nodes( + pub(crate) fn insert_chain_mmr_nodes_tx( tx: &Transaction<'_>, nodes: &[(InOrderIndex, Digest)], ) -> Result<(), StoreError> { @@ -145,7 +152,7 @@ impl SqliteStore { let (block_num, header, chain_mmr, has_client_notes) = serialize_block_header(block_header, chain_mmr_peaks, has_client_notes)?; const QUERY: &str = "\ - INSERT INTO block_headers + INSERT OR IGNORE INTO block_headers (block_num, header, chain_mmr_peaks, has_client_notes) VALUES (?, ?, ?, ?)"; tx.execute(QUERY, params![block_num, header, chain_mmr, has_client_notes])?; @@ -163,7 +170,7 @@ fn insert_chain_mmr_node( node: Digest, ) -> Result<(), StoreError> { let (id, node) = serialize_chain_mmr_node(id, node)?; - const QUERY: &str = "INSERT INTO chain_mmr_nodes (id, node) VALUES (?, ?)"; + const QUERY: &str = "INSERT OR IGNORE INTO chain_mmr_nodes (id, node) VALUES (?, ?)"; tx.execute(QUERY, params![id, node])?; Ok(()) } diff --git a/src/store/sqlite_store/mod.rs b/src/store/sqlite_store/mod.rs index 118131c3b..9794ce28f 100644 --- a/src/store/sqlite_store/mod.rs +++ b/src/store/sqlite_store/mod.rs @@ -2,7 +2,7 @@ use alloc::collections::BTreeMap; use core::cell::{RefCell, RefMut}; use miden_objects::{ - accounts::{Account, AccountId, AccountStub}, + accounts::{Account, AccountId, AccountStub, AuthSecretKey}, crypto::merkle::{InOrderIndex, MmrPeaks}, notes::NoteTag, BlockHeader, Digest, Word, @@ -10,8 +10,7 @@ use miden_objects::{ use rusqlite::{vtab::array, Connection}; use super::{ - AuthInfo, ChainMmrNodeFilter, InputNoteRecord, NoteFilter, OutputNoteRecord, Store, - TransactionFilter, + ChainMmrNodeFilter, InputNoteRecord, NoteFilter, OutputNoteRecord, Store, TransactionFilter, }; use crate::{ client::{ @@ -24,7 +23,7 @@ use crate::{ mod accounts; mod chain_data; -mod migrations; +pub(crate) mod migrations; mod notes; mod sync; mod transactions; @@ -191,6 +190,10 @@ impl Store for SqliteStore { self.get_chain_mmr_nodes(filter) } + fn insert_chain_mmr_nodes(&self, nodes: &[(InOrderIndex, Digest)]) -> Result<(), StoreError> { + self.insert_chain_mmr_nodes(nodes) + } + fn get_chain_mmr_peaks_by_block_num(&self, block_num: u32) -> Result<MmrPeaks, StoreError> { self.get_chain_mmr_peaks_by_block_num(block_num) } @@ -199,7 +202,7 @@ impl Store for SqliteStore { &self, account: &Account, account_seed: Option<Word>, - auth_info: &AuthInfo, + auth_info: &AuthSecretKey, ) -> Result<(), StoreError> { self.insert_account(account, account_seed, auth_info) } @@ -223,9 +226,13 @@ impl Store for SqliteStore { self.get_account(account_id) } - fn get_account_auth(&self, account_id: AccountId) -> Result<AuthInfo, StoreError> { + fn get_account_auth(&self, account_id: AccountId) -> Result<AuthSecretKey, StoreError> { self.get_account_auth(account_id) } + + fn get_account_auth_by_pub_key(&self, pub_key: Word) -> Result<AuthSecretKey, StoreError> { + self.get_account_auth_by_pub_key(pub_key) + } } // TESTS @@ -233,40 +240,12 @@ impl Store for SqliteStore { #[cfg(test)] pub mod tests { - use std::{cell::RefCell, env::temp_dir}; + use std::cell::RefCell; use rusqlite::{vtab::array, Connection}; - use uuid::Uuid; use super::{migrations, SqliteStore}; - use crate::{ - client::get_random_coin, - config::{ClientConfig, RpcConfig}, - mock::{MockClient, MockRpcApi}, - }; - - pub fn create_test_client() -> MockClient { - let store = create_test_store_path() - .into_os_string() - .into_string() - .unwrap() - .try_into() - .unwrap(); - - let client_config = ClientConfig::new(store, RpcConfig::default()); - - let rpc_endpoint = client_config.rpc.endpoint.to_string(); - let store = SqliteStore::new((&client_config).into()).unwrap(); - let rng = get_random_coin(); - - MockClient::new(MockRpcApi::new(&rpc_endpoint), rng, store, true) - } - - pub(crate) fn create_test_store_path() -> std::path::PathBuf { - let mut temp_file = temp_dir(); - temp_file.push(format!("{}.sqlite3", Uuid::new_v4())); - temp_file - } + use crate::mock::create_test_store_path; pub(crate) fn create_test_store() -> SqliteStore { let temp_file = create_test_store_path(); diff --git a/src/store/sqlite_store/store.sql b/src/store/sqlite_store/store.sql index 1202801ee..a8fd81610 100644 --- a/src/store/sqlite_store/store.sql +++ b/src/store/sqlite_store/store.sql @@ -24,6 +24,7 @@ CREATE TABLE account_vaults ( CREATE TABLE account_auth ( account_id UNSIGNED BIG INT NOT NULL, -- ID of the account auth_info BLOB NOT NULL, -- Serialized representation of information needed for authentication + pub_key BLOB NOT NULL, -- Public key for easier authenticator use PRIMARY KEY (account_id) ); @@ -40,7 +41,7 @@ CREATE TABLE accounts ( FOREIGN KEY (code_root) REFERENCES account_code(root), FOREIGN KEY (storage_root) REFERENCES account_storage(root), FOREIGN KEY (vault_root) REFERENCES account_vaults(root) - + CONSTRAINT check_seed_nonzero CHECK (NOT (nonce = 0 AND account_seed IS NULL)) ); @@ -51,11 +52,11 @@ CREATE TABLE transactions ( init_account_state BLOB NOT NULL, -- Hash of the account state before the transaction was executed. final_account_state BLOB NOT NULL, -- Hash of the account state after the transaction was executed. input_notes BLOB, -- Serialized list of input note hashes - output_notes BLOB, -- Serialized list of output note hashes + output_notes BLOB, -- Serialized list of output note hashes script_hash BLOB, -- Transaction script hash script_inputs BLOB, -- Transaction script inputs block_num UNSIGNED BIG INT, -- Block number for the block against which the transaction was executed. - commit_height UNSIGNED BIG INT NULL, -- Block number of the block at which the transaction was included in the chain. + commit_height UNSIGNED BIG INT NULL, -- Block number of the block at which the transaction was included in the chain. FOREIGN KEY (script_hash) REFERENCES transaction_scripts(script_hash), PRIMARY KEY (id) ); @@ -82,7 +83,7 @@ CREATE TABLE input_notes ( -- sub_hash -- sub hash of the block the note was included in stored as a hex string -- note_root -- the note root of the block the note was created in -- note_path -- the Merkle path to the note in the note Merkle tree of the block the note was created in, stored as an array of digests - + metadata JSON NULL, -- JSON consisting of the following fields: -- sender_id -- the account ID of the sender -- tag -- the note tag @@ -97,7 +98,7 @@ CREATE TABLE input_notes ( PRIMARY KEY (note_id) CONSTRAINT check_valid_inclusion_proof_json CHECK ( - inclusion_proof IS NULL OR + inclusion_proof IS NULL OR ( json_extract(inclusion_proof, '$.origin.block_num') IS NOT NULL AND json_extract(inclusion_proof, '$.origin.node_index') IS NOT NULL AND @@ -124,7 +125,7 @@ CREATE TABLE output_notes ( -- sub_hash -- sub hash of the block the note was included in stored as a hex string -- note_root -- the note root of the block the note was created in -- note_path -- the Merkle path to the note in the note Merkle tree of the block the note was created in, stored as an array of digests - + metadata JSON NOT NULL, -- JSON consisting of the following fields: -- sender_id -- the account ID of the sender -- tag -- the note tag @@ -139,7 +140,7 @@ CREATE TABLE output_notes ( PRIMARY KEY (note_id) CONSTRAINT check_valid_inclusion_proof_json CHECK ( - inclusion_proof IS NULL OR + inclusion_proof IS NULL OR ( json_extract(inclusion_proof, '$.origin.block_num') IS NOT NULL AND json_extract(inclusion_proof, '$.origin.node_index') IS NOT NULL AND @@ -148,7 +149,7 @@ CREATE TABLE output_notes ( json_extract(inclusion_proof, '$.note_path') IS NOT NULL )) CONSTRAINT check_valid_details_json CHECK ( - details IS NULL OR + details IS NULL OR ( json_extract(details, '$.nullifier') IS NOT NULL AND json_extract(details, '$.script_hash') IS NOT NULL AND diff --git a/src/store/sqlite_store/sync.rs b/src/store/sqlite_store/sync.rs index 496e16b8f..174da825c 100644 --- a/src/store/sqlite_store/sync.rs +++ b/src/store/sqlite_store/sync.rs @@ -105,7 +105,7 @@ impl SqliteStore { Self::insert_block_header_tx(&tx, block_header, new_mmr_peaks, block_has_relevant_notes)?; // Insert new authentication nodes (inner nodes of the PartialMmr) - Self::insert_chain_mmr_nodes(&tx, &new_authentication_nodes)?; + Self::insert_chain_mmr_nodes_tx(&tx, &new_authentication_nodes)?; // Update tracked output notes for (note_id, inclusion_proof) in committed_notes.updated_output_notes().iter() { diff --git a/src/store/sqlite_store/transactions.rs b/src/store/sqlite_store/transactions.rs index a261e2800..fe7c74cb0 100644 --- a/src/store/sqlite_store/transactions.rs +++ b/src/store/sqlite_store/transactions.rs @@ -16,9 +16,11 @@ use super::{ SqliteStore, }; use crate::{ - client::transactions::{TransactionRecord, TransactionResult, TransactionStatus}, + client::transactions::{ + notes_from_output, TransactionRecord, TransactionResult, TransactionStatus, + }, errors::StoreError, - store::{InputNoteRecord, OutputNoteRecord, TransactionFilter}, + store::{OutputNoteRecord, TransactionFilter}, }; pub(crate) const INSERT_TRANSACTION_QUERY: &str = @@ -87,16 +89,13 @@ impl SqliteStore { account.apply_delta(account_delta).map_err(StoreError::AccountError)?; - let created_input_notes = tx_result - .relevant_notes() - .into_iter() - .map(|note| InputNoteRecord::from(note.clone())) - .collect::<Vec<_>>(); + // Save only input notes that we care for (based on the note screener assessment) + let created_input_notes = tx_result.relevant_notes().to_vec(); - let created_output_notes = tx_result - .created_notes() - .iter() - .map(|note| OutputNoteRecord::from(note.clone())) + // Save all output notes + let created_output_notes = notes_from_output(tx_result.created_notes()) + .cloned() + .map(OutputNoteRecord::from) .collect::<Vec<_>>(); let consumed_note_ids = @@ -112,11 +111,8 @@ impl SqliteStore { update_account(&tx, &account)?; // Updates for notes - - // TODO: see if we should filter the input notes we store to keep notes we can consume with - // existing accounts - for note in &created_input_notes { - insert_input_note_tx(&tx, note)?; + for note in created_input_notes { + insert_input_note_tx(&tx, ¬e)?; } for note in &created_output_notes { diff --git a/src/tests.rs b/src/tests.rs index ef76a71b1..e71db3690 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,7 +2,10 @@ // ================================================================================================ use miden_lib::transaction::TransactionKernel; use miden_objects::{ - accounts::{AccountId, AccountStub, ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN}, + accounts::{ + account_id::testing::ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, AccountId, AccountStub, + AuthSecretKey, + }, assembly::{AstSerdeOptions, ModuleAst}, assets::{FungibleAsset, TokenSymbol}, crypto::dsa::rpo_falcon512::SecretKey, @@ -16,10 +19,10 @@ use crate::{ transactions::transaction_request::TransactionTemplate, }, mock::{ - get_account_with_default_account_code, mock_full_chain_mmr_and_notes, + create_test_client, get_account_with_default_account_code, mock_full_chain_mmr_and_notes, mock_fungible_faucet_account, mock_notes, ACCOUNT_ID_REGULAR, }, - store::{sqlite_store::tests::create_test_client, AuthInfo, InputNoteRecord, NoteFilter}, + store::{InputNoteRecord, NoteFilter}, }; #[tokio::test] @@ -152,10 +155,14 @@ async fn insert_same_account_twice_fails() { let key_pair = SecretKey::new(); assert!(client - .insert_account(&account, Some(Word::default()), &AuthInfo::RpoFalcon512(key_pair.clone())) + .insert_account( + &account, + Some(Word::default()), + &AuthSecretKey::RpoFalcon512(key_pair.clone()) + ) .is_ok()); assert!(client - .insert_account(&account, Some(Word::default()), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&account, Some(Word::default()), &AuthSecretKey::RpoFalcon512(key_pair)) .is_err()); } @@ -183,7 +190,7 @@ async fn test_account_code() { assert_eq!(account_module, reconstructed_ast); client - .insert_account(&account, Some(Word::default()), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&account, Some(Word::default()), &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); let (retrieved_acc, _) = client.get_account(account.id()).unwrap(); @@ -207,7 +214,7 @@ async fn test_get_account_by_id() { let key_pair = SecretKey::new(); client - .insert_account(&account, Some(Word::default()), &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&account, Some(Word::default()), &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); // Retrieving an existing account should succeed @@ -370,7 +377,7 @@ async fn test_mint_transaction() { client .store() - .insert_account(&faucet, None, &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&faucet, None, &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); client.sync_state().await.unwrap(); @@ -407,7 +414,7 @@ async fn test_get_output_notes() { client .store() - .insert_account(&faucet, None, &AuthInfo::RpoFalcon512(key_pair)) + .insert_account(&faucet, None, &AuthSecretKey::RpoFalcon512(key_pair)) .unwrap(); client.sync_state().await.unwrap(); diff --git a/tests/config/genesis.toml b/tests/config/genesis.toml new file mode 100644 index 000000000..b9f3c425c --- /dev/null +++ b/tests/config/genesis.toml @@ -0,0 +1,17 @@ +version = 1 +timestamp = 1672531200 + +[[accounts]] +type = "BasicWallet" +init_seed = "0xa123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +auth_scheme = "RpoFalcon512" +auth_seed = "0xb123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + +[[accounts]] +type = "BasicFungibleFaucet" +init_seed = "0xc123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +auth_scheme = "RpoFalcon512" +auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +token_symbol = "POL" +decimals = 12 +max_supply = 1000000 diff --git a/tests/config/miden-node.toml b/tests/config/miden-node.toml new file mode 100644 index 000000000..85ae4c97a --- /dev/null +++ b/tests/config/miden-node.toml @@ -0,0 +1,20 @@ +[block_producer] +# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-block-producer', 1)) % 2**16 +endpoint = { host = "localhost", port = 48046 } +store_url = "http://localhost:28943" +# enables or disables the verification of transaction proofs before they are accepted into the +# transaction queue. +verify_tx_proofs = true + +[rpc] +# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-rpc', 1)) % 2**16 +endpoint = { host = "0.0.0.0", port = 57291 } +block_producer_url = "http://localhost:48046" +store_url = "http://localhost:28943" + +[store] +# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-store', 1)) % 2**16 +endpoint = { host = "localhost", port = 28943 } +database_filepath = "./miden-store.sqlite3" +genesis_filepath = "./genesis.dat" +blockstore_dir = "./blocks" diff --git a/tests/integration/asm/custom_p2id.masm b/tests/integration/asm/custom_p2id.masm index 3d9b07be5..827b6aa68 100644 --- a/tests/integration/asm/custom_p2id.masm +++ b/tests/integration/asm/custom_p2id.masm @@ -49,8 +49,11 @@ end begin # drop the note script root dropw + # => [NOTE_ARG] push.{expected_note_arg} assert_eqw + # drop the note script root + dropw # store the note inputs to memory starting at address 0 push.0 exec.note::get_inputs diff --git a/tests/integration/common.rs b/tests/integration/common.rs index 9f390d72a..7e001598a 100644 --- a/tests/integration/common.rs +++ b/tests/integration/common.rs @@ -1,4 +1,4 @@ -use std::{env::temp_dir, time::Duration}; +use std::{env::temp_dir, rc::Rc, time::Duration}; use figment::{ providers::{Format, Toml}, @@ -9,6 +9,8 @@ use miden_client::{ accounts::{AccountStorageMode, AccountTemplate}, get_random_coin, rpc::TonicRpcClient, + store_authenticator::StoreAuthenticator, + sync::SyncSummary, transactions::transaction_request::{TransactionRequest, TransactionTemplate}, Client, }, @@ -17,7 +19,10 @@ use miden_client::{ store::{sqlite_store::SqliteStore, NoteFilter, TransactionFilter}, }; use miden_objects::{ - accounts::{Account, AccountId, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN}, + accounts::{ + account_id::testing::ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, Account, + AccountId, + }, assets::{Asset, FungibleAsset, TokenSymbol}, crypto::rand::RpoRandomCoin, notes::{NoteId, NoteType}, @@ -28,7 +33,12 @@ use uuid::Uuid; pub const ACCOUNT_ID_REGULAR: u64 = ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN; -pub type TestClient = Client<TonicRpcClient, RpoRandomCoin, SqliteStore>; +pub type TestClient = Client< + TonicRpcClient, + RpoRandomCoin, + SqliteStore, + StoreAuthenticator<RpoRandomCoin, SqliteStore>, +>; pub const TEST_CLIENT_CONFIG_FILE_PATH: &str = "./tests/config/miden-client.toml"; /// Creates a `TestClient` @@ -51,9 +61,15 @@ pub fn create_test_client() -> TestClient { .try_into() .unwrap(); - let store = SqliteStore::new((&client_config).into()).unwrap(); + let store = { + let sqlite_store = SqliteStore::new((&client_config).into()).unwrap(); + Rc::new(sqlite_store) + }; + let rng = get_random_coin(); - TestClient::new(TonicRpcClient::new(&client_config.rpc), rng, store, true) + + let authenticator = StoreAuthenticator::new_with_rng(store.clone(), rng); + TestClient::new(TonicRpcClient::new(&client_config.rpc), rng, store, authenticator, true) } pub fn create_test_store_path() -> std::path::PathBuf { @@ -90,6 +106,24 @@ pub async fn execute_tx_and_sync(client: &mut TestClient, tx_request: Transactio } } +// Syncs until `amount_of_blocks` have been created onchain compared to client's sync height +pub async fn wait_for_blocks(client: &mut TestClient, amount_of_blocks: u32) -> SyncSummary { + let current_block = client.get_sync_height().unwrap(); + let final_block = current_block + amount_of_blocks; + println!("Syncing until block {}...", final_block); + // wait until tx is committed + loop { + let summary = client.sync_state().await.unwrap(); + println!("Synced to block {} (syncing until {})...", summary.block_num, final_block); + + if summary.block_num >= final_block { + return summary; + } + + std::thread::sleep(std::time::Duration::new(3, 0)); + } +} + /// Waits for node to be running. /// /// # Panics @@ -164,6 +198,7 @@ pub async fn setup( } /// Mints a note from faucet_account_id for basic_account_id, waits for inclusion and returns it +/// with 1000 units of the corresponding fungible asset pub async fn mint_note( client: &mut TestClient, basic_account_id: AccountId, diff --git a/tests/integration/custom_transactions_tests.rs b/tests/integration/custom_transactions_tests.rs index f4022c657..7fa6d2896 100644 --- a/tests/integration/custom_transactions_tests.rs +++ b/tests/integration/custom_transactions_tests.rs @@ -1,19 +1,16 @@ use std::collections::BTreeMap; -use miden_client::{ - client::{ - accounts::{AccountStorageMode, AccountTemplate}, - transactions::transaction_request::TransactionRequest, - }, - store::AuthInfo, +use miden_client::client::{ + accounts::{AccountStorageMode, AccountTemplate}, + transactions::transaction_request::TransactionRequest, }; use miden_objects::{ - accounts::AccountId, + accounts::{AccountId, AuthSecretKey}, assembly::ProgramAst, assets::{FungibleAsset, TokenSymbol}, crypto::rand::{FeltRng, RpoRandomCoin}, notes::{ - Note, NoteAssets, NoteExecutionMode, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, + Note, NoteAssets, NoteExecutionHint, NoteInputs, NoteMetadata, NoteRecipient, NoteTag, NoteType, }, Felt, Word, @@ -57,10 +54,10 @@ async fn test_transaction_request() { storage_mode: AccountStorageMode::Local, }; let (fungible_faucet, _seed) = client.new_account(account_template).unwrap(); + println!("sda1"); // Execute mint transaction in order to create custom note let note = mint_custom_note(&mut client, fungible_faucet.id(), regular_account.id()).await; - client.sync_state().await.unwrap(); // Prepare transaction @@ -93,7 +90,7 @@ async fn test_transaction_request() { let tx_script = { let account_auth = client.get_account_auth(regular_account.id()).unwrap(); let (pubkey_input, advice_map): (Word, Vec<Felt>) = match account_auth { - AuthInfo::RpoFalcon512(key) => ( + AuthSecretKey::RpoFalcon512(key) => ( key.public_key().into(), key.to_bytes().iter().map(|a| Felt::new(*a as u64)).collect::<Vec<Felt>>(), ), @@ -107,6 +104,7 @@ async fn test_transaction_request() { regular_account.id(), note_args_map.clone(), vec![], + vec![], Some(tx_script), ); @@ -121,7 +119,7 @@ async fn test_transaction_request() { let tx_script = { let account_auth = client.get_account_auth(regular_account.id()).unwrap(); let (pubkey_input, advice_map): (Word, Vec<Felt>) = match account_auth { - AuthInfo::RpoFalcon512(key) => ( + AuthSecretKey::RpoFalcon512(key) => ( key.public_key().into(), key.to_bytes().iter().map(|a| Felt::new(*a as u64)).collect::<Vec<Felt>>(), ), @@ -131,8 +129,13 @@ async fn test_transaction_request() { client.compile_tx_script(program, script_inputs, vec![]).unwrap() }; - let transaction_request = - TransactionRequest::new(regular_account.id(), note_args_map, vec![], Some(tx_script)); + let transaction_request = TransactionRequest::new( + regular_account.id(), + note_args_map, + vec![], + vec![], + Some(tx_script), + ); execute_tx_and_sync(&mut client, transaction_request).await; @@ -149,7 +152,8 @@ async fn mint_custom_note( let note = create_custom_note(client, faucet_account_id, target_account_id, &mut random_coin); let recipient = note - .recipient_digest() + .recipient() + .digest() .iter() .map(|x| x.as_int().to_string()) .collect::<Vec<_>>() @@ -179,23 +183,13 @@ async fn mint_custom_note( let program = ProgramAst::parse(&code).unwrap(); - let tx_script = { - let account_auth = client.get_account_auth(faucet_account_id).unwrap(); - let (pubkey_input, advice_map): (Word, Vec<Felt>) = match account_auth { - AuthInfo::RpoFalcon512(key) => ( - key.public_key().into(), - key.to_bytes().iter().map(|a| Felt::new(*a as u64)).collect::<Vec<Felt>>(), - ), - }; - - let script_inputs = vec![(pubkey_input, advice_map)]; - client.compile_tx_script(program, script_inputs, vec![]).unwrap() - }; + let tx_script = client.compile_tx_script(program, vec![], vec![]).unwrap(); let transaction_request = TransactionRequest::new( faucet_account_id, BTreeMap::new(), vec![note.clone()], + vec![], Some(tx_script), ); @@ -211,7 +205,7 @@ fn create_custom_note( ) -> Note { let expected_note_arg = [Felt::new(9), Felt::new(12), Felt::new(18), Felt::new(3)] .iter() - .map(|x| x.to_string()) + .map(|x| x.as_int().to_string()) .collect::<Vec<_>>() .join("."); @@ -225,7 +219,7 @@ fn create_custom_note( let note_metadata = NoteMetadata::new( faucet_account_id, NoteType::OffChain, - NoteTag::from_account_id(target_account_id, NoteExecutionMode::Local).unwrap(), + NoteTag::from_account_id(target_account_id, NoteExecutionHint::Local).unwrap(), Default::default(), ) .unwrap(); diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 4c32d67df..b78f48369 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -20,6 +20,7 @@ use common::*; mod custom_transactions_tests; mod onchain_tests; +mod swap_transactions_tests; #[tokio::test] async fn test_added_notes() { @@ -372,7 +373,7 @@ async fn test_get_output_notes() { let faucet_account_id = faucet_account_stub.id(); let random_account_id = AccountId::from_hex("0x0123456789abcdef").unwrap(); - //No output notes initially + // No output notes initially assert!(client.get_output_notes(NoteFilter::All).unwrap().is_empty()); // First Mint necesary token @@ -398,12 +399,12 @@ async fn test_get_output_notes() { let output_note_id = tx_request.expected_output_notes()[0].id(); - //Before executing, the output note is not found + // Before executing, the output note is not found assert!(client.get_output_note(output_note_id).is_err()); execute_tx_and_sync(&mut client, tx_request).await; - //After executing, the note is only found in output notes + // After executing, the note is only found in output notes assert!(client.get_output_note(output_note_id).is_ok()); assert!(client.get_input_note(output_note_id).is_err()); } @@ -413,31 +414,45 @@ async fn test_import_pending_notes() { let mut client_1 = create_test_client(); let (first_basic_account, _second_basic_account, faucet_account) = setup(&mut client_1, AccountStorageMode::Local).await; + let mut client_2 = create_test_client(); + let (client_2_account, _seed) = client_2 + .new_account(AccountTemplate::BasicWallet { + mutable_code: true, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + wait_for_node(&mut client_2).await; - client_1.sync_state().await.unwrap(); - client_2.sync_state().await.unwrap(); let tx_template = TransactionTemplate::MintFungibleAsset( FungibleAsset::new(faucet_account.id(), MINT_AMOUNT).unwrap(), - first_basic_account.id(), + client_2_account.id(), NoteType::OffChain, ); let tx_request = client_1.build_transaction_request(tx_template).unwrap(); let note = tx_request.expected_output_notes()[0].clone(); + client_2.sync_state().await.unwrap(); // If the verification is requested before execution then the import should fail assert!(client_2.import_input_note(note.clone().into(), true).await.is_err()); execute_tx_and_sync(&mut client_1, tx_request).await; - client_2.sync_state().await.unwrap(); + // Use client 1 to wait until a couple of blocks have passed + wait_for_blocks(&mut client_1, 3).await; + + let new_sync_data = client_2.sync_state().await.unwrap(); client_2.import_input_note(note.clone().into(), true).await.unwrap(); let input_note = client_2.get_input_note(note.id()).unwrap(); + assert!(new_sync_data.block_num > input_note.inclusion_proof().unwrap().origin().block_num + 1); // If imported after execution and syncing then the inclusion proof should be Some assert!(input_note.inclusion_proof().is_some()); + // If client 2 succesfully consumes the note, we confirm we have MMR and block header data + consume_notes(&mut client_2, client_2_account.id(), &[input_note.try_into().unwrap()]).await; + let tx_template = TransactionTemplate::MintFungibleAsset( FungibleAsset::new(faucet_account.id(), MINT_AMOUNT).unwrap(), first_basic_account.id(), diff --git a/tests/integration/onchain_tests.rs b/tests/integration/onchain_tests.rs index 4fbd9cc25..8f31ee246 100644 --- a/tests/integration/onchain_tests.rs +++ b/tests/integration/onchain_tests.rs @@ -8,7 +8,7 @@ use miden_client::{ use miden_objects::{ accounts::AccountId, assets::{Asset, FungibleAsset, TokenSymbol}, - notes::{NoteExecutionMode, NoteTag, NoteType}, + notes::{NoteTag, NoteType}, transaction::InputNote, }; @@ -291,7 +291,11 @@ async fn test_onchain_notes_sync_with_tag() { // Load tag into client 2 client_2 .add_note_tag( - NoteTag::from_account_id(target_account_id, NoteExecutionMode::Local).unwrap(), + NoteTag::from_account_id( + target_account_id, + miden_objects::notes::NoteExecutionHint::Local, + ) + .unwrap(), ) .unwrap(); diff --git a/tests/integration/swap_transactions_tests.rs b/tests/integration/swap_transactions_tests.rs new file mode 100644 index 000000000..585ee1679 --- /dev/null +++ b/tests/integration/swap_transactions_tests.rs @@ -0,0 +1,483 @@ +use miden_client::client::{ + accounts::{AccountStorageMode, AccountTemplate}, + transactions::transaction_request::{SwapTransactionData, TransactionTemplate}, +}; +use miden_objects::{ + accounts::AccountId, + assets::{Asset, FungibleAsset, TokenSymbol}, + notes::{NoteExecutionHint, NoteTag, NoteType}, +}; + +use super::common::*; + +// SWAP FULLY ONCHAIN +// ================================================================================================ + +#[tokio::test] +async fn test_swap_fully_onchain() { + const OFFERED_ASSET_AMOUNT: u64 = 1; + const REQUESTED_ASSET_AMOUNT: u64 = 25; + const BTC_MINT_AMOUNT: u64 = 1000; + const ETH_MINT_AMOUNT: u64 = 1000; + let mut client1 = create_test_client(); + wait_for_node(&mut client1).await; + let mut client2 = create_test_client(); + let mut client_with_faucets = create_test_client(); + + client1.sync_state().await.unwrap(); + client2.sync_state().await.unwrap(); + client_with_faucets.sync_state().await.unwrap(); + + // Create Client 1's basic wallet (We'll call it accountA) + let (account_a, _) = client1 + .new_account(AccountTemplate::BasicWallet { + mutable_code: false, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + + // Create Client 2's basic wallet (We'll call it accountB) + let (account_b, _) = client2 + .new_account(AccountTemplate::BasicWallet { + mutable_code: false, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + + // Create client with faucets BTC faucet (note: it's not real BTC) + let (btc_faucet_account, _) = client_with_faucets + .new_account(AccountTemplate::FungibleFaucet { + token_symbol: TokenSymbol::new("BTC").unwrap(), + decimals: 8, + max_supply: 1_000_000, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + // Create client with faucets ETH faucet (note: it's not real ETH) + let (eth_faucet_account, _) = client_with_faucets + .new_account(AccountTemplate::FungibleFaucet { + token_symbol: TokenSymbol::new("ETH").unwrap(), + decimals: 8, + max_supply: 1_000_000, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + + // mint 1000 BTC for accountA + println!("minting 1000 btc for account A"); + mint( + &mut client_with_faucets, + account_a.id(), + btc_faucet_account.id(), + NoteType::Public, + BTC_MINT_AMOUNT, + ) + .await; + println!("minting 1000 eth for account B"); + // mint 1000 ETH for accountB + mint( + &mut client_with_faucets, + account_b.id(), + eth_faucet_account.id(), + NoteType::Public, + ETH_MINT_AMOUNT, + ) + .await; + + // Sync and consume note for accountA + client1.sync_state().await.unwrap(); + let client_1_notes = client1.get_input_notes(miden_client::store::NoteFilter::All).unwrap(); + assert_eq!(client_1_notes.len(), 1); + + println!("Consuming mint note on first client..."); + let tx_template = + TransactionTemplate::ConsumeNotes(account_a.id(), vec![client_1_notes[0].id()]); + let tx_request = client1.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client1, tx_request).await; + + // Sync and consume note for accountB + client2.sync_state().await.unwrap(); + let client_2_notes = client2.get_input_notes(miden_client::store::NoteFilter::All).unwrap(); + assert_eq!(client_2_notes.len(), 1); + + println!("Consuming mint note on second client..."); + let tx_template = + TransactionTemplate::ConsumeNotes(account_b.id(), vec![client_2_notes[0].id()]); + let tx_request = client2.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client2, tx_request).await; + + // Create ONCHAIN swap note (clientA offers 1 BTC in exchange of 25 ETH) + // check that account now has 1 less BTC + println!("creating swap note with accountA"); + let offered_asset = FungibleAsset::new(btc_faucet_account.id(), OFFERED_ASSET_AMOUNT).unwrap(); + let requested_asset = + FungibleAsset::new(eth_faucet_account.id(), REQUESTED_ASSET_AMOUNT).unwrap(); + let tx_template = TransactionTemplate::Swap( + SwapTransactionData::new( + account_a.id(), + Asset::Fungible(offered_asset), + Asset::Fungible(requested_asset), + ), + NoteType::Public, + ); + println!("Running SWAP tx..."); + let tx_request = client1.build_transaction_request(tx_template).unwrap(); + + let expected_output_notes = tx_request.expected_output_notes().to_vec(); + let expected_payback_note_details = tx_request.expected_partial_notes().to_vec(); + assert_eq!(expected_output_notes.len(), 1); + assert_eq!(expected_payback_note_details.len(), 1); + + execute_tx_and_sync(&mut client1, tx_request).await; + + let payback_note_tag = + build_swap_tag(NoteType::Public, btc_faucet_account.id(), eth_faucet_account.id()); + + // add swap note's tag to both client 1 and client 2 (TODO: check if it's needed for both) + // we could technically avoid this step, but for the first iteration of swap notes we'll + // require to manually add tags + println!("Adding swap tags"); + client1.add_note_tag(payback_note_tag).unwrap(); + client2.add_note_tag(payback_note_tag).unwrap(); + + // sync on client 2, we should get the swap note + // consume swap note with accountB, and check that the vault changed appropiately + client2.sync_state().await.unwrap(); + println!("Consuming swap note on second client..."); + let tx_template = + TransactionTemplate::ConsumeNotes(account_b.id(), vec![expected_output_notes[0].id()]); + let tx_request = client2.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client2, tx_request).await; + + // sync on client 1, we should get the missing payback note details. + // try consuming the received note with accountA, it should now have 25 ETH + client1.sync_state().await.unwrap(); + println!("Consuming swap payback note on first client..."); + let tx_template = TransactionTemplate::ConsumeNotes( + account_a.id(), + vec![expected_payback_note_details[0].id()], + ); + let tx_request = client1.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client1, tx_request).await; + + // At the end we should end up with + // + // - accountA: 999 BTC, 25 ETH + // - accountB: 1 BTC, 975 ETH + + // first reload the account + let (account_a, _) = client1.get_account(account_a.id()).unwrap(); + let account_a_assets = account_a.vault().assets(); + assert_eq!(account_a_assets.count(), 2); + let mut account_a_assets = account_a.vault().assets(); + + let asset_1 = account_a_assets.next().unwrap(); + let asset_2 = account_a_assets.next().unwrap(); + + match (asset_1, asset_2) { + (Asset::Fungible(btc_asset), Asset::Fungible(eth_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 999); + assert_eq!(eth_asset.amount(), 25); + }, + (Asset::Fungible(eth_asset), Asset::Fungible(btc_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 999); + assert_eq!(eth_asset.amount(), 25); + }, + _ => panic!("should only have fungible assets!"), + } + + let (account_b, _) = client2.get_account(account_b.id()).unwrap(); + let account_b_assets = account_b.vault().assets(); + assert_eq!(account_b_assets.count(), 2); + let mut account_b_assets = account_b.vault().assets(); + + let asset_1 = account_b_assets.next().unwrap(); + let asset_2 = account_b_assets.next().unwrap(); + + match (asset_1, asset_2) { + (Asset::Fungible(btc_asset), Asset::Fungible(eth_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 1); + assert_eq!(eth_asset.amount(), 975); + }, + (Asset::Fungible(eth_asset), Asset::Fungible(btc_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 1); + assert_eq!(eth_asset.amount(), 975); + }, + _ => panic!("should only have fungible assets!"), + } +} + +#[tokio::test] +async fn test_swap_offchain() { + const OFFERED_ASSET_AMOUNT: u64 = 1; + const REQUESTED_ASSET_AMOUNT: u64 = 25; + const BTC_MINT_AMOUNT: u64 = 1000; + const ETH_MINT_AMOUNT: u64 = 1000; + let mut client1 = create_test_client(); + wait_for_node(&mut client1).await; + let mut client2 = create_test_client(); + let mut client_with_faucets = create_test_client(); + + client1.sync_state().await.unwrap(); + client2.sync_state().await.unwrap(); + client_with_faucets.sync_state().await.unwrap(); + + // Create Client 1's basic wallet (We'll call it accountA) + let (account_a, _) = client1 + .new_account(AccountTemplate::BasicWallet { + mutable_code: false, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + + // Create Client 2's basic wallet (We'll call it accountB) + let (account_b, _) = client2 + .new_account(AccountTemplate::BasicWallet { + mutable_code: false, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + + // Create client with faucets BTC faucet (note: it's not real BTC) + let (btc_faucet_account, _) = client_with_faucets + .new_account(AccountTemplate::FungibleFaucet { + token_symbol: TokenSymbol::new("BTC").unwrap(), + decimals: 8, + max_supply: 1_000_000, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + // Create client with faucets ETH faucet (note: it's not real ETH) + let (eth_faucet_account, _) = client_with_faucets + .new_account(AccountTemplate::FungibleFaucet { + token_symbol: TokenSymbol::new("ETH").unwrap(), + decimals: 8, + max_supply: 1_000_000, + storage_mode: AccountStorageMode::Local, + }) + .unwrap(); + + // mint 1000 BTC for accountA + println!("minting 1000 btc for account A"); + mint( + &mut client_with_faucets, + account_a.id(), + btc_faucet_account.id(), + NoteType::Public, + BTC_MINT_AMOUNT, + ) + .await; + // mint 1000 ETH for accountB + println!("minting 1000 eth for account B"); + mint( + &mut client_with_faucets, + account_b.id(), + eth_faucet_account.id(), + NoteType::Public, + ETH_MINT_AMOUNT, + ) + .await; + + // Sync and consume note for accountA + client1.sync_state().await.unwrap(); + let client_1_notes = client1.get_input_notes(miden_client::store::NoteFilter::All).unwrap(); + assert_eq!(client_1_notes.len(), 1); + + println!("Consuming mint note on first client..."); + let tx_template = + TransactionTemplate::ConsumeNotes(account_a.id(), vec![client_1_notes[0].id()]); + let tx_request = client1.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client1, tx_request).await; + + // Sync and consume note for accountB + client2.sync_state().await.unwrap(); + let client_2_notes = client2.get_input_notes(miden_client::store::NoteFilter::All).unwrap(); + assert_eq!(client_2_notes.len(), 1); + + println!("Consuming mint note on second client..."); + let tx_template = + TransactionTemplate::ConsumeNotes(account_b.id(), vec![client_2_notes[0].id()]); + let tx_request = client2.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client2, tx_request).await; + + // Create ONCHAIN swap note (clientA offers 1 BTC in exchange of 25 ETH) + // check that account now has 1 less BTC + println!("creating swap note with accountA"); + let offered_asset = FungibleAsset::new(btc_faucet_account.id(), OFFERED_ASSET_AMOUNT).unwrap(); + let requested_asset = + FungibleAsset::new(eth_faucet_account.id(), REQUESTED_ASSET_AMOUNT).unwrap(); + let tx_template = TransactionTemplate::Swap( + SwapTransactionData::new( + account_a.id(), + Asset::Fungible(offered_asset), + Asset::Fungible(requested_asset), + ), + NoteType::OffChain, + ); + println!("Running SWAP tx..."); + let tx_request = client1.build_transaction_request(tx_template).unwrap(); + + let expected_output_notes = tx_request.expected_output_notes().to_vec(); + let expected_payback_note_details = tx_request.expected_partial_notes().to_vec(); + assert_eq!(expected_output_notes.len(), 1); + assert_eq!(expected_payback_note_details.len(), 1); + + execute_tx_and_sync(&mut client1, tx_request).await; + + // Export note from client 1 to client 2 + let exported_note = client1.get_output_note(expected_output_notes[0].id()).unwrap(); + + client2 + .import_input_note(exported_note.try_into().unwrap(), true) + .await + .unwrap(); + + // Sync so we get the inclusion proof info + client2.sync_state().await.unwrap(); + + // consume swap note with accountB, and check that the vault changed appropiately + println!("Consuming swap note on second client..."); + let tx_template = + TransactionTemplate::ConsumeNotes(account_b.id(), vec![expected_output_notes[0].id()]); + let tx_request = client2.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client2, tx_request).await; + + // sync on client 1, we should get the missing payback note details. + // try consuming the received note with accountA, it should now have 25 ETH + client1.sync_state().await.unwrap(); + println!("Consuming swap payback note on first client..."); + let tx_template = TransactionTemplate::ConsumeNotes( + account_a.id(), + vec![expected_payback_note_details[0].id()], + ); + let tx_request = client1.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(&mut client1, tx_request).await; + + // At the end we should end up with + // + // - accountA: 999 BTC, 25 ETH + // - accountB: 1 BTC, 975 ETH + + // first reload the account + let (account_a, _) = client1.get_account(account_a.id()).unwrap(); + let account_a_assets = account_a.vault().assets(); + assert_eq!(account_a_assets.count(), 2); + let mut account_a_assets = account_a.vault().assets(); + + let asset_1 = account_a_assets.next().unwrap(); + let asset_2 = account_a_assets.next().unwrap(); + + match (asset_1, asset_2) { + (Asset::Fungible(btc_asset), Asset::Fungible(eth_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 999); + assert_eq!(eth_asset.amount(), 25); + }, + (Asset::Fungible(eth_asset), Asset::Fungible(btc_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 999); + assert_eq!(eth_asset.amount(), 25); + }, + _ => panic!("should only have fungible assets!"), + } + + let (account_b, _) = client2.get_account(account_b.id()).unwrap(); + let account_b_assets = account_b.vault().assets(); + assert_eq!(account_b_assets.count(), 2); + let mut account_b_assets = account_b.vault().assets(); + + let asset_1 = account_b_assets.next().unwrap(); + let asset_2 = account_b_assets.next().unwrap(); + + match (asset_1, asset_2) { + (Asset::Fungible(btc_asset), Asset::Fungible(eth_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 1); + assert_eq!(eth_asset.amount(), 975); + }, + (Asset::Fungible(eth_asset), Asset::Fungible(btc_asset)) + if btc_asset.faucet_id() == btc_faucet_account.id() + && eth_asset.faucet_id() == eth_faucet_account.id() => + { + assert_eq!(btc_asset.amount(), 1); + assert_eq!(eth_asset.amount(), 975); + }, + _ => panic!("should only have fungible assets!"), + } +} + +/// Returns a note tag for a swap note with the specified parameters. +/// +/// Use case ID for the returned tag is set to 0. +/// +/// Tag payload is constructed by taking asset tags (8 bits of faucet ID) and concatenating them +/// together as offered_asset_tag + requested_asset tag. +/// +/// Network execution hint for the returned tag is set to `Local`. +/// +/// Based on miden-base's implementation (<https://github.com/0xPolygonMiden/miden-base/blob/9e4de88031b55bcc3524cb0ccfb269821d97fb29/miden-lib/src/notes/mod.rs#L153>) +fn build_swap_tag( + note_type: NoteType, + offered_asset_faucet_id: AccountId, + requested_asset_faucet_id: AccountId, +) -> NoteTag { + const SWAP_USE_CASE_ID: u16 = 0; + + // get bits 4..12 from faucet IDs of both assets, these bits will form the tag payload; the + // reason we skip the 4 most significant bits is that these encode metadata of underlying + // faucets and are likely to be the same for many different faucets. + + let offered_asset_id: u64 = offered_asset_faucet_id.into(); + let offered_asset_tag = (offered_asset_id >> 52) as u8; + + let requested_asset_id: u64 = requested_asset_faucet_id.into(); + let requested_asset_tag = (requested_asset_id >> 52) as u8; + + let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16); + + let execution = NoteExecutionHint::Local; + match note_type { + NoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution), + _ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload), + } + .unwrap() +} + +/// Mints a note from faucet_account_id for basic_account_id, waits for inclusion and returns it +/// with 1000 units of the corresponding fungible asset +/// +/// `basic_account_id` does not need to be tracked by the client, but `faucet_account_id` does +async fn mint( + client: &mut TestClient, + basic_account_id: AccountId, + faucet_account_id: AccountId, + note_type: NoteType, + mint_amount: u64, +) { + // Create a Mint Tx for 1000 units of our fungible asset + let fungible_asset = FungibleAsset::new(faucet_account_id, mint_amount).unwrap(); + let tx_template = + TransactionTemplate::MintFungibleAsset(fungible_asset, basic_account_id, note_type); + + println!("Minting Asset"); + let tx_request = client.build_transaction_request(tx_template).unwrap(); + execute_tx_and_sync(client, tx_request.clone()).await; +}