From b1c79f40a0948d2c02e0dae4d58f2eed9f41c522 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 30 Oct 2024 16:51:12 -0500 Subject: [PATCH] feat: Added wasm consumable notes + improved note models (#561) * feat: Added wasm consumable notes + improved note models * improved tests + cleaned up model constructors * cleanup --- CHANGELOG.md | 1 + crates/web-client/js/index.js | 4 + crates/web-client/js/types/index.d.ts | 2 + crates/web-client/package.json | 2 +- crates/web-client/rollup.config.js | 79 ++-- .../src/models/consumable_note_record.rs | 89 ++++ .../web-client/src/models/fungible_asset.rs | 12 + crates/web-client/src/models/mod.rs | 1 + crates/web-client/src/models/note_assets.rs | 13 + crates/web-client/src/notes.rs | 21 +- crates/web-client/test/account.test.ts | 428 +++++++++--------- crates/web-client/test/global.test.d.ts | 4 + crates/web-client/test/mocha.global.setup.mjs | 14 + crates/web-client/test/notes.test.ts | 96 +++- crates/web-client/test/webClientTestUtils.ts | 71 ++- 15 files changed, 589 insertions(+), 248 deletions(-) create mode 100644 crates/web-client/src/models/consumable_note_record.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a2f550c..dbe863c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.6.0 (TBD) * Allow to set expiration delta for `TransactionRequest` (#553). +* Added WASM consumable notes API + improved note models (#561). * [BREAKING] Refactored `OutputNoteRecord` to use states and transitions for updates (#551). * Added better error handling for WASM sync state (#558). * Added WASM Input note tests + updated input note models (#554) diff --git a/crates/web-client/js/index.js b/crates/web-client/js/index.js index dd784adff..88c885a25 100644 --- a/crates/web-client/js/index.js +++ b/crates/web-client/js/index.js @@ -7,12 +7,14 @@ const { AccountStorageMode, AdviceMap, AuthSecretKey, + ConsumableNoteRecord, Felt, FeltArray, FungibleAsset, InputNoteState, Note, NoteAssets, + NoteConsumability, NoteExecutionHint, NoteExecutionMode, NoteFilter, @@ -47,12 +49,14 @@ export { AccountStorageMode, AdviceMap, AuthSecretKey, + ConsumableNoteRecord, Felt, FeltArray, FungibleAsset, InputNoteState, Note, NoteAssets, + NoteConsumability, NoteExecutionHint, NoteExecutionMode, NoteFilter, diff --git a/crates/web-client/js/types/index.d.ts b/crates/web-client/js/types/index.d.ts index 19b138098..d28049f9a 100644 --- a/crates/web-client/js/types/index.d.ts +++ b/crates/web-client/js/types/index.d.ts @@ -5,6 +5,7 @@ export { AccountStorageMode, AdviceMap, AuthSecretKey, + ConsumableNoteRecord, Felt, FeltArray, FungibleAsset, @@ -13,6 +14,7 @@ export { NewTransactionResult, Note, NoteAssets, + NoteConsumability, NoteExecutionHint, NoteExecutionMode, NoteFilter, diff --git a/crates/web-client/package.json b/crates/web-client/package.json index eacaddf27..175578cc4 100644 --- a/crates/web-client/package.json +++ b/crates/web-client/package.json @@ -1,6 +1,6 @@ { "name": "@demox-labs/miden-sdk", - "version": "0.0.15", + "version": "0.0.16", "description": "Polygon Miden Wasm SDK", "collaborators": [ "Polygon Miden", diff --git a/crates/web-client/rollup.config.js b/crates/web-client/rollup.config.js index abc3e7250..c02f61fa9 100644 --- a/crates/web-client/rollup.config.js +++ b/crates/web-client/rollup.config.js @@ -3,7 +3,7 @@ import resolve from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; // Flag that indicates if the build is meant for testing purposes. -const testing = process.env.MIDEN_WEB_TESTING === 'true'; +const testing = process.env.MIDEN_WEB_TESTING === "true"; /** * Rollup configuration file for building a Cargo project and creating a WebAssembly (WASM) module. @@ -20,46 +20,45 @@ const testing = process.env.MIDEN_WEB_TESTING === 'true'; * Both configurations output ES module format files with source maps for easier debugging. */ export default [ - { - input: { - wasm: "./js/wasm.js", - }, - output: { - dir: `dist`, - format: "es", - sourcemap: true, - assetFileNames: "assets/[name][extname]", - }, - plugins: [ - rust({ - cargoArgs: [ - "--features", "testing", - "--config", `build.rustflags=["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", "-C", "link-arg=--max-memory=4294967296"]`, - "--no-default-features", - ], + { + input: { + wasm: "./js/wasm.js", + }, + output: { + dir: `dist`, + format: "es", + sourcemap: true, + assetFileNames: "assets/[name][extname]", + }, + plugins: [ + rust({ + cargoArgs: [ + "--features", + "testing", + "--config", + `build.rustflags=["-C", "target-feature=+atomics,+bulk-memory,+mutable-globals", "-C", "link-arg=--max-memory=4294967296"]`, + "--no-default-features", + ], - experimental: { - typescriptDeclarationDir: "dist/crates", - }, + experimental: { + typescriptDeclarationDir: "dist/crates", + }, - wasmOptArgs: testing ? ["-O0"] : null, - }), - resolve(), - commonjs(), - ], + wasmOptArgs: testing ? ["-O0"] : null, + }), + resolve(), + commonjs(), + ], + }, + { + input: { + index: "./js/index.js", }, - { - input: { - index: "./js/index.js", - }, - output: { - dir: `dist`, - format: "es", - sourcemap: true, - }, - plugins: [ - resolve(), - commonjs(), - ], - } + output: { + dir: `dist`, + format: "es", + sourcemap: true, + }, + plugins: [resolve(), commonjs()], + }, ]; diff --git a/crates/web-client/src/models/consumable_note_record.rs b/crates/web-client/src/models/consumable_note_record.rs new file mode 100644 index 000000000..76f8851b1 --- /dev/null +++ b/crates/web-client/src/models/consumable_note_record.rs @@ -0,0 +1,89 @@ +use miden_client::{ + notes::{NoteConsumability as NativeNoteConsumability, NoteRelevance}, + store::InputNoteRecord as NativeInputNoteRecord, +}; +use wasm_bindgen::prelude::*; + +use super::{account_id::AccountId, input_note_record::InputNoteRecord}; + +#[derive(Clone)] +#[wasm_bindgen] +pub struct ConsumableNoteRecord { + input_note_record: InputNoteRecord, + note_consumability: Vec, +} + +#[derive(Clone, Copy)] +#[wasm_bindgen] +pub struct NoteConsumability { + account_id: AccountId, + + // The block number after which the note can be consumed, + // if None then the note can be consumed immediately + consumable_after_block: Option, +} + +#[wasm_bindgen] +impl NoteConsumability { + pub(crate) fn new( + account_id: AccountId, + consumable_after_block: Option, + ) -> NoteConsumability { + NoteConsumability { account_id, consumable_after_block } + } + + pub fn account_id(&self) -> AccountId { + self.account_id + } + + pub fn consumable_after_block(&self) -> Option { + self.consumable_after_block + } +} + +#[wasm_bindgen] +impl ConsumableNoteRecord { + #[wasm_bindgen(constructor)] + pub fn new( + input_note_record: InputNoteRecord, + note_consumability: Vec, + ) -> ConsumableNoteRecord { + ConsumableNoteRecord { input_note_record, note_consumability } + } + + pub fn input_note_record(&self) -> InputNoteRecord { + self.input_note_record.clone() + } + + pub fn note_consumability(&self) -> Vec { + self.note_consumability.clone() + } +} + +// CONVERSIONS +// ================================================================================================ +impl From<(NativeInputNoteRecord, Vec)> for ConsumableNoteRecord { + fn from( + (input_note_record, note_consumability): ( + NativeInputNoteRecord, + Vec, + ), + ) -> Self { + ConsumableNoteRecord::new( + input_note_record.into(), + note_consumability.into_iter().map(|c| c.into()).collect(), + ) + } +} + +impl From for NoteConsumability { + fn from(note_consumability: NativeNoteConsumability) -> Self { + NoteConsumability::new( + note_consumability.0.into(), + match note_consumability.1 { + NoteRelevance::After(block) => Some(block), + NoteRelevance::Always => None, + }, + ) + } +} diff --git a/crates/web-client/src/models/fungible_asset.rs b/crates/web-client/src/models/fungible_asset.rs index e655bb270..b214c2400 100644 --- a/crates/web-client/src/models/fungible_asset.rs +++ b/crates/web-client/src/models/fungible_asset.rs @@ -49,3 +49,15 @@ impl From<&FungibleAsset> for NativeAsset { fungible_asset.0.into() } } + +impl From for FungibleAsset { + fn from(native_asset: FungibleAssetNative) -> Self { + FungibleAsset(native_asset) + } +} + +impl From<&FungibleAssetNative> for FungibleAsset { + fn from(native_asset: &FungibleAssetNative) -> Self { + FungibleAsset(*native_asset) + } +} diff --git a/crates/web-client/src/models/mod.rs b/crates/web-client/src/models/mod.rs index 776872db6..bcbec9f7d 100644 --- a/crates/web-client/src/models/mod.rs +++ b/crates/web-client/src/models/mod.rs @@ -38,6 +38,7 @@ pub mod advice_map; pub mod asset_vault; pub mod auth_secret_key; pub mod block_header; +pub mod consumable_note_record; pub mod executed_transaction; pub mod felt; pub mod fungible_asset; diff --git a/crates/web-client/src/models/note_assets.rs b/crates/web-client/src/models/note_assets.rs index e1b74528f..abf421d7b 100644 --- a/crates/web-client/src/models/note_assets.rs +++ b/crates/web-client/src/models/note_assets.rs @@ -20,6 +20,19 @@ impl NoteAssets { pub fn push(&mut self, asset: &FungibleAsset) { let _ = self.0.add_asset(asset.into()); } + + pub fn assets(&self) -> Vec { + self.0 + .iter() + .filter_map(|asset| { + if asset.is_fungible() { + Some(asset.unwrap_fungible().into()) + } else { + None // TODO: Support non fungible assets + } + }) + .collect() + } } // CONVERSIONS diff --git a/crates/web-client/src/notes.rs b/crates/web-client/src/notes.rs index 7a056cf69..cff0dd823 100644 --- a/crates/web-client/src/notes.rs +++ b/crates/web-client/src/notes.rs @@ -7,7 +7,10 @@ use wasm_bindgen::prelude::*; use super::models::note_script::NoteScript; use crate::{ - models::{input_note_record::InputNoteRecord, note_filter::NoteFilter}, + models::{ + account_id::AccountId, consumable_note_record::ConsumableNoteRecord, + input_note_record::InputNoteRecord, note_filter::NoteFilter, + }, WebClient, }; @@ -80,4 +83,20 @@ impl WebClient { Err(JsValue::from_str("Client not initialized")) } } + + pub async fn get_consumable_notes( + &mut self, + account_id: Option, + ) -> Result, JsValue> { + if let Some(client) = self.get_mut_inner() { + let native_account_id = account_id.map(|id| id.into()); + let result = client.get_consumable_notes(native_account_id).await.map_err(|err| { + JsValue::from_str(&format!("Failed to get consumable notes: {}", err)) + })?; + + Ok(result.into_iter().map(|record| record.into()).collect()) + } else { + Err(JsValue::from_str("Client not initialized")) + } + } } diff --git a/crates/web-client/test/account.test.ts b/crates/web-client/test/account.test.ts index d9f4331bc..0c3dc2997 100644 --- a/crates/web-client/test/account.test.ts +++ b/crates/web-client/test/account.test.ts @@ -1,290 +1,310 @@ -import { expect } from 'chai'; +import { expect } from "chai"; import { testingPage } from "./mocha.global.setup.mjs"; // GET_ACCOUNT TESTS // ======================================================================================================= interface GetAccountSuccessResult { - hashOfCreatedAccount: string; - hashOfGetAccountResult: string; - isAccountType: boolean | undefined; + hashOfCreatedAccount: string; + hashOfGetAccountResult: string; + isAccountType: boolean | undefined; } -export const getAccountOneMatch = async (): Promise => { +export const getAccountOneMatch = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - const newAccount = await client.new_wallet(window.AccountStorageMode.private(), true); - const result = await client.get_account(newAccount.id()); - - return { - hashOfCreatedAccount: newAccount.hash().to_hex(), - hashOfGetAccountResult: result.hash().to_hex(), - isAccountType: result instanceof window.Account - } - }); -}; + const client = window.client; + const newAccount = await client.new_wallet( + window.AccountStorageMode.private(), + true + ); + const result = await client.get_account(newAccount.id()); + + return { + hashOfCreatedAccount: newAccount.hash().to_hex(), + hashOfGetAccountResult: result.hash().to_hex(), + isAccountType: result instanceof window.Account, + }; + }); + }; interface GetAccountFailureResult { - nonExistingAccountId: string; - errorMessage: string; + nonExistingAccountId: string; + errorMessage: string; } export const getAccountNoMatch = async (): Promise => { - return await testingPage.evaluate(async () => { - const client = window.client; - const nonExistingAccountId = window.TestUtils.create_mock_account_id(); - - try { - await client.get_account(nonExistingAccountId); - } catch (error: any) { - return { - nonExistingAccountId: nonExistingAccountId.to_string(), - errorMessage: error.message || error.toString() // Capture the error message - }; - } - - // If no error occurred (should not happen in this test case), return a generic error - return { - nonExistingAccountId: nonExistingAccountId.to_string(), - errorMessage: 'Unexpected success when fetching non-existing account' - }; - }); + return await testingPage.evaluate(async () => { + const client = window.client; + const nonExistingAccountId = window.TestUtils.create_mock_account_id(); + + try { + await client.get_account(nonExistingAccountId); + } catch (error: any) { + return { + nonExistingAccountId: nonExistingAccountId.to_string(), + errorMessage: error.message || error.toString(), // Capture the error message + }; + } + + // If no error occurred (should not happen in this test case), return a generic error + return { + nonExistingAccountId: nonExistingAccountId.to_string(), + errorMessage: "Unexpected success when fetching non-existing account", + }; + }); }; describe("get_account tests", () => { - it("retrieves an existing account", async () => { - const result = await getAccountOneMatch(); + it("retrieves an existing account", async () => { + const result = await getAccountOneMatch(); - expect(result.hashOfCreatedAccount).to.equal(result.hashOfGetAccountResult); - expect(result.isAccountType).to.be.true; - }); + expect(result.hashOfCreatedAccount).to.equal(result.hashOfGetAccountResult); + expect(result.isAccountType).to.be.true; + }); - it("returns error attempting to retrieve a non-existing account", async () => { - const result = await getAccountNoMatch(); - const expectedErrorMessage = `Failed to get account: Store error: Account data was not found for Account Id ${result.nonExistingAccountId}`; + it("returns error attempting to retrieve a non-existing account", async () => { + const result = await getAccountNoMatch(); + const expectedErrorMessage = `Failed to get account: Store error: Account data was not found for Account Id ${result.nonExistingAccountId}`; - expect(result.errorMessage).to.equal(expectedErrorMessage); - }); + expect(result.errorMessage).to.equal(expectedErrorMessage); }); +}); // GET_ACCOUNTS TESTS // ======================================================================================================= interface GetAccountsSuccessResult { - hashesOfCreatedAccounts: string[]; - hashesOfGetAccountsResult: string[]; - resultTypes: boolean[]; + hashesOfCreatedAccounts: string[]; + hashesOfGetAccountsResult: string[]; + resultTypes: boolean[]; } -export const getAccountsManyMatches = async (): Promise => { +export const getAccountsManyMatches = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - const newAccount1 = await client.new_wallet(window.AccountStorageMode.private(), true); - const newAccount2 = await client.new_wallet(window.AccountStorageMode.private(), true); - const hashesOfCreatedAccounts = [newAccount1.hash().to_hex(), newAccount2.hash().to_hex()]; - - const result = await client.get_accounts(); - - const hashesOfGetAccountsResult = []; - const resultTypes = []; - - for (let i = 0; i < result.length; i++) { - hashesOfGetAccountsResult.push(result[i].hash().to_hex()); - resultTypes.push(result[i] instanceof window.AccountHeader); - } - - return { - hashesOfCreatedAccounts: hashesOfCreatedAccounts, - hashesOfGetAccountsResult: hashesOfGetAccountsResult, - resultTypes: resultTypes - } + const client = window.client; + const newAccount1 = await client.new_wallet( + window.AccountStorageMode.private(), + true + ); + const newAccount2 = await client.new_wallet( + window.AccountStorageMode.private(), + true + ); + const hashesOfCreatedAccounts = [ + newAccount1.hash().to_hex(), + newAccount2.hash().to_hex(), + ]; + + const result = await client.get_accounts(); + + const hashesOfGetAccountsResult = []; + const resultTypes = []; + + for (let i = 0; i < result.length; i++) { + hashesOfGetAccountsResult.push(result[i].hash().to_hex()); + resultTypes.push(result[i] instanceof window.AccountHeader); + } + + return { + hashesOfCreatedAccounts: hashesOfCreatedAccounts, + hashesOfGetAccountsResult: hashesOfGetAccountsResult, + resultTypes: resultTypes, + }; }); -}; + }; -export const getAccountsNoMatches = async (): Promise => { +export const getAccountsNoMatches = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - - const result = await client.get_accounts(); - - const hashesOfGetAccountsResult = []; - const resultTypes = []; - - for (let i = 0; i < result.length; i++) { - hashesOfGetAccountsResult.push(result[i].hash().to_hex()); - resultTypes.push(result[i] instanceof window.AccountHeader); - } + const client = window.client; - return { - hashesOfCreatedAccounts: [], - hashesOfGetAccountsResult: hashesOfGetAccountsResult, - resultTypes: resultTypes - } - }); -}; + const result = await client.get_accounts(); -describe("get_accounts tests", () => { - beforeEach(async () => { - await testingPage.evaluate(async () => { - // Open a connection to the list of databases - const databases = await indexedDB.databases(); - for (const db of databases) { - // Delete each database by name - indexedDB.deleteDatabase(db.name!); - } - }); - }); + const hashesOfGetAccountsResult = []; + const resultTypes = []; - it("retrieves all existing accounts", async () => { - const result = await getAccountsManyMatches(); + for (let i = 0; i < result.length; i++) { + hashesOfGetAccountsResult.push(result[i].hash().to_hex()); + resultTypes.push(result[i] instanceof window.AccountHeader); + } - for (let address of result.hashesOfGetAccountsResult) { - expect(result.hashesOfCreatedAccounts.includes(address)).to.be.true; - } - expect(result.resultTypes).to.deep.equal([true, true]); + return { + hashesOfCreatedAccounts: [], + hashesOfGetAccountsResult: hashesOfGetAccountsResult, + resultTypes: resultTypes, + }; }); + }; - it("returns empty array when no accounts exist", async () => { - const result = await getAccountsNoMatches(); +describe("get_accounts tests", () => { + it("retrieves all existing accounts", async () => { + const result = await getAccountsManyMatches(); - expect(result.hashesOfCreatedAccounts.length).to.equal(0); - expect(result.hashesOfGetAccountsResult.length).to.equal(0); - expect(result.resultTypes.length).to.equal(0); - }); + for (let address of result.hashesOfGetAccountsResult) { + expect(result.hashesOfCreatedAccounts.includes(address)).to.be.true; + } + expect(result.resultTypes).to.deep.equal([true, true]); + }); + + it("returns empty array when no accounts exist", async () => { + const result = await getAccountsNoMatches(); + + expect(result.hashesOfCreatedAccounts.length).to.equal(0); + expect(result.hashesOfGetAccountsResult.length).to.equal(0); + expect(result.resultTypes.length).to.equal(0); + }); }); // GET_ACCOUNT_AUTH TESTS // ======================================================================================================= interface GetAccountAuthSuccessResult { - publicKey: any; - secretKey: any; - isAuthSecretKeyType: boolean | undefined; + publicKey: any; + secretKey: any; + isAuthSecretKeyType: boolean | undefined; } -export const getAccountAuth = async (): Promise => { +export const getAccountAuth = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - const newAccount = await client.new_wallet(window.AccountStorageMode.private(), true); - - const result = await client.get_account_auth(newAccount.id()); - - return { - publicKey: result.get_rpo_falcon_512_public_key_as_word(), - secretKey: result.get_rpo_falcon_512_secret_key_as_felts(), - isAuthSecretKeyType: result instanceof window.AuthSecretKey - } + const client = window.client; + const newAccount = await client.new_wallet( + window.AccountStorageMode.private(), + true + ); + + const result = await client.get_account_auth(newAccount.id()); + + return { + publicKey: result.get_rpo_falcon_512_public_key_as_word(), + secretKey: result.get_rpo_falcon_512_secret_key_as_felts(), + isAuthSecretKeyType: result instanceof window.AuthSecretKey, + }; }); -}; + }; interface GetAccountAuthFailureResult { - nonExistingAccountId: string; - errorMessage: string; + nonExistingAccountId: string; + errorMessage: string; } -export const getAccountAuthNoMatch = async (): Promise => { +export const getAccountAuthNoMatch = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - const nonExistingAccountId = window.TestUtils.create_mock_account_id(); - - try { - await client.get_account_auth(nonExistingAccountId); - } catch (error: any) { - return { - nonExistingAccountId: nonExistingAccountId.to_string(), - errorMessage: error.message || error.toString() // Capture the error message - }; - } - - // If no error occurred (should not happen in this test case), return a generic error + const client = window.client; + const nonExistingAccountId = window.TestUtils.create_mock_account_id(); + + try { + await client.get_account_auth(nonExistingAccountId); + } catch (error: any) { return { - nonExistingAccountId: nonExistingAccountId.to_string(), - errorMessage: 'Unexpected success when fetching non-existing account auth' + nonExistingAccountId: nonExistingAccountId.to_string(), + errorMessage: error.message || error.toString(), // Capture the error message }; + } + + // If no error occurred (should not happen in this test case), return a generic error + return { + nonExistingAccountId: nonExistingAccountId.to_string(), + errorMessage: + "Unexpected success when fetching non-existing account auth", + }; }); -}; + }; describe("get_account_auth tests", () => { - it("retrieves an existing account auth", async () => { - const result = await getAccountAuth(); + it("retrieves an existing account auth", async () => { + const result = await getAccountAuth(); - expect(result.publicKey).to.not.be.empty; - expect(result.secretKey).to.not.be.empty; - expect(result.isAuthSecretKeyType).to.be.true; - }); + expect(result.publicKey).to.not.be.empty; + expect(result.secretKey).to.not.be.empty; + expect(result.isAuthSecretKeyType).to.be.true; + }); - it("returns error attempting to retrieve a non-existing account auth", async () => { - const result = await getAccountAuthNoMatch(); - const expectedErrorMessage = `Failed to get account auth: Store error: Account data was not found for Account Id ${result.nonExistingAccountId}`; + it("returns error attempting to retrieve a non-existing account auth", async () => { + const result = await getAccountAuthNoMatch(); + const expectedErrorMessage = `Failed to get account auth: Store error: Account data was not found for Account Id ${result.nonExistingAccountId}`; - expect(result.errorMessage).to.equal(expectedErrorMessage); - }); + expect(result.errorMessage).to.equal(expectedErrorMessage); + }); }); // FETCH_AND_CACHE_ACCOUNT_AUTH_BY_PUB_KEY TESTS // ======================================================================================================= interface FetchAndCacheAccountAuthByPubKeySuccessResult { - publicKey: any; - secretKey: any; - isAuthSecretKeyType: boolean | undefined; + publicKey: any; + secretKey: any; + isAuthSecretKeyType: boolean | undefined; } -export const fetchAndCacheAccountAuthByPubKey = async (): Promise => { +export const fetchAndCacheAccountAuthByPubKey = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - const newAccount = await client.new_wallet(window.AccountStorageMode.private(), true); - - const result = await client.fetch_and_cache_account_auth_by_pub_key(newAccount.id()); - - return { - publicKey: result.get_rpo_falcon_512_public_key_as_word(), - secretKey: result.get_rpo_falcon_512_secret_key_as_felts(), - isAuthSecretKeyType: result instanceof window.AuthSecretKey - } + const client = window.client; + const newAccount = await client.new_wallet( + window.AccountStorageMode.private(), + true + ); + + const result = await client.fetch_and_cache_account_auth_by_pub_key( + newAccount.id() + ); + + return { + publicKey: result.get_rpo_falcon_512_public_key_as_word(), + secretKey: result.get_rpo_falcon_512_secret_key_as_felts(), + isAuthSecretKeyType: result instanceof window.AuthSecretKey, + }; }); -}; + }; interface FetchAndCacheAccountAuthByPubKeyFailureResult { - nonExistingAccountId: string; - errorMessage: string; + nonExistingAccountId: string; + errorMessage: string; } -export const fetchAndCacheAccountAuthByPubKeyNoMatch = async (): Promise => { +export const fetchAndCacheAccountAuthByPubKeyNoMatch = + async (): Promise => { return await testingPage.evaluate(async () => { - const client = window.client; - const nonExistingAccountId = window.TestUtils.create_mock_account_id(); - - try { - await client.fetch_and_cache_account_auth_by_pub_key(nonExistingAccountId); - } catch (error: any) { - return { - nonExistingAccountId: nonExistingAccountId.to_string(), - errorMessage: error.message || error.toString() // Capture the error message - }; - } - - // If no error occurred (should not happen in this test case), return a generic error + const client = window.client; + const nonExistingAccountId = window.TestUtils.create_mock_account_id(); + + try { + await client.fetch_and_cache_account_auth_by_pub_key( + nonExistingAccountId + ); + } catch (error: any) { return { - nonExistingAccountId: nonExistingAccountId.to_string(), - errorMessage: 'Unexpected success when fetching non-existing account auth' + nonExistingAccountId: nonExistingAccountId.to_string(), + errorMessage: error.message || error.toString(), // Capture the error message }; + } + + // If no error occurred (should not happen in this test case), return a generic error + return { + nonExistingAccountId: nonExistingAccountId.to_string(), + errorMessage: + "Unexpected success when fetching non-existing account auth", + }; }); -}; + }; describe("fetch_and_cache_account_auth_by_pub_key tests", () => { - it("retrieves an existing account auth and caches it", async () => { - const result = await fetchAndCacheAccountAuthByPubKey(); + it("retrieves an existing account auth and caches it", async () => { + const result = await fetchAndCacheAccountAuthByPubKey(); - expect(result.publicKey).to.not.be.empty; - expect(result.secretKey).to.not.be.empty; - expect(result.isAuthSecretKeyType).to.be.true; - }); + expect(result.publicKey).to.not.be.empty; + expect(result.secretKey).to.not.be.empty; + expect(result.isAuthSecretKeyType).to.be.true; + }); - it("returns error attempting to retrieve/cache a non-existing account auth", async () => { - const result = await fetchAndCacheAccountAuthByPubKeyNoMatch(); - const expectedErrorMessage = `Failed to fetch and cache account auth: Account data was not found for Account Id ${result.nonExistingAccountId}`; + it("returns error attempting to retrieve/cache a non-existing account auth", async () => { + const result = await fetchAndCacheAccountAuthByPubKeyNoMatch(); + const expectedErrorMessage = `Failed to fetch and cache account auth: Account data was not found for Account Id ${result.nonExistingAccountId}`; - expect(result.errorMessage).to.equal(expectedErrorMessage); - }); + expect(result.errorMessage).to.equal(expectedErrorMessage); + }); }); diff --git a/crates/web-client/test/global.test.d.ts b/crates/web-client/test/global.test.d.ts index 7d3052e50..f1480b8aa 100644 --- a/crates/web-client/test/global.test.d.ts +++ b/crates/web-client/test/global.test.d.ts @@ -6,11 +6,13 @@ import { AccountStorageMode, AdviceMap, AuthSecretKey, + ConsumableNoteRecord, Felt, FeltArray, FungibleAsset, Note, NoteAssets, + NoteConsumability, NoteExecutionHint, NoteExecutionMode, NoteFilter, @@ -42,11 +44,13 @@ declare global { AccountStorageMode: typeof AccountStorageMode; AdviceMap: typeof AdviceMap; AuthSecretKey: typeof AuthSecretKey; + ConsumableNoteRecord: typeof ConsumableNoteRecord; Felt: typeof Felt; FeltArray: typeof FeltArray; FungibleAsset: typeof FungibleAsset; Note: typeof Note; NoteAssets: typeof NoteAssets; + NoteConsumability: typeof NoteConsumability; NoteExecutionHint: typeof NoteExecutionHint; NoteExecutionMode: typeof NoteExecutionMode; NoteFilter: typeof NoteFilter; diff --git a/crates/web-client/test/mocha.global.setup.mjs b/crates/web-client/test/mocha.global.setup.mjs index 4c49d7ca7..3c22606e5 100644 --- a/crates/web-client/test/mocha.global.setup.mjs +++ b/crates/web-client/test/mocha.global.setup.mjs @@ -46,11 +46,13 @@ before(async () => { AccountStorageMode, AdviceMap, AuthSecretKey, + ConsumableNoteRecord, Felt, FeltArray, FungibleAsset, Note, NoteAssets, + NoteConsumability, NoteExecutionHint, NoteExecutionMode, NoteFilter, @@ -83,11 +85,13 @@ before(async () => { window.AccountStorageMode = AccountStorageMode; window.AdviceMap = AdviceMap; window.AuthSecretKey = AuthSecretKey; + window.ConsumableNoteRecord = ConsumableNoteRecord; window.Felt = Felt; window.FeltArray = FeltArray; window.FungibleAsset = FungibleAsset; window.Note = Note; window.NoteAssets = NoteAssets; + window.NoteConsumability = NoteConsumability; window.NoteExecutionHint = NoteExecutionHint; window.NoteExecutionMode = NoteExecutionMode; window.NoteFilter = NoteFilter; @@ -108,7 +112,17 @@ before(async () => { window.TransactionScriptInputPair = TransactionScriptInputPair; window.TransactionScriptInputPairArray = TransactionScriptInputPairArray; }, LOCAL_MIDEN_NODE_PORT); +}); +beforeEach(async () => { + await testingPage.evaluate(async () => { + // Open a connection to the list of databases + const databases = await indexedDB.databases(); + for (const db of databases) { + // Delete each database by name + indexedDB.deleteDatabase(db.name); + } + }); }); after(async () => { diff --git a/crates/web-client/test/notes.test.ts b/crates/web-client/test/notes.test.ts index c8efc20de..3df1d7e6e 100644 --- a/crates/web-client/test/notes.test.ts +++ b/crates/web-client/test/notes.test.ts @@ -5,7 +5,9 @@ import { consumeTransaction, fetchAndCacheAccountAuth, mintTransaction, + sendTransaction, setupWalletAndFaucet, + syncState, } from "./webClientTestUtils"; const getInputNote = async (noteId: string) => { @@ -30,14 +32,42 @@ const getInputNotes = async () => { }); }; -const setupConsumedNote = async () => { +const setupMintedNote = async () => { const { accountId, faucetId } = await setupWalletAndFaucet(); const { createdNoteId } = await mintTransaction(accountId, faucetId); + + return { createdNoteId, accountId, faucetId }; +}; + +const setupConsumedNote = async () => { + const { createdNoteId, accountId, faucetId } = await setupMintedNote(); await consumeTransaction(accountId, faucetId, createdNoteId); return { consumedNoteId: createdNoteId }; }; +const getConsumableNotes = async (accountId?: string) => { + return await testingPage.evaluate(async (_accountId) => { + const client = window.client; + let records; + if (_accountId) { + console.log({ _accountId }); + const accountId = window.AccountId.from_hex(_accountId); + records = await client.get_consumable_notes(accountId); + } else { + records = await client.get_consumable_notes(); + } + + return records.map((record) => ({ + noteId: record.input_note_record().id().to_string(), + consumability: record.note_consumability().map((c) => ({ + accountId: c.account_id().to_string(), + consumableAfterBlock: c.consumable_after_block(), + })), + })); + }, accountId); +}; + describe("get_input_note", () => { it("retrieve input note that does not exist", async () => { await setupWalletAndFaucet(); @@ -63,6 +93,70 @@ describe("get_input_notes", () => { }); }); +describe("get_consumable_notes", () => { + it("filter by account", async () => { + const { createdNoteId: noteId1, accountId: accountId1 } = + await setupMintedNote(); + await setupMintedNote(); + + const result = await getConsumableNotes(accountId1); + expect(result).to.have.lengthOf(1); + result.forEach((record) => { + expect(record.consumability).to.have.lengthOf(1); + expect(record.consumability[0].accountId).to.equal(accountId1); + expect(record.noteId).to.equal(noteId1); + expect(record.consumability[0].consumableAfterBlock).to.be.undefined; + }); + }); + it("no filter by account", async () => { + const { createdNoteId: noteId1, accountId: accountId1 } = + await setupMintedNote(); + const { createdNoteId: noteId2, accountId: accountId2 } = + await setupMintedNote(); + + const result = await getConsumableNotes(); + expect(result.map((r) => r.noteId)).to.include.members([noteId1, noteId2]); + expect(result.map((r) => r.consumability[0].accountId)).to.include.members([ + accountId1, + accountId2, + ]); + expect(result).to.have.lengthOf(2); + const consumableRecord1 = result.find((r) => r.noteId === noteId1); + const consumableRecord2 = result.find((r) => r.noteId === noteId2); + + consumableRecord1!!.consumability.forEach((c) => { + expect(c.accountId).to.equal(accountId1); + }); + + consumableRecord2!!.consumability.forEach((c) => { + expect(c.accountId).to.equal(accountId2); + }); + }); + it("p2idr consume after block", async () => { + const { accountId: senderAccountId, faucetId } = + await setupWalletAndFaucet(); + const { accountId: targetAccountId } = await setupWalletAndFaucet(); + const recallHeight = 100; + await sendTransaction( + senderAccountId, + targetAccountId, + faucetId, + 100, + recallHeight + ); + + const consumableRecipient = await getConsumableNotes(targetAccountId); + const consumableSender = await getConsumableNotes(senderAccountId); + expect(consumableSender).to.have.lengthOf(1); + expect(consumableSender[0].consumability[0].consumableAfterBlock).to.equal( + recallHeight + ); + expect(consumableRecipient).to.have.lengthOf(1); + expect(consumableRecipient[0].consumability[0].consumableAfterBlock).to.be + .undefined; + }); +}); + // TODO: describe("get_output_note", () => {}); diff --git a/crates/web-client/test/webClientTestUtils.ts b/crates/web-client/test/webClientTestUtils.ts index 1426e6729..ce601d3d1 100644 --- a/crates/web-client/test/webClientTestUtils.ts +++ b/crates/web-client/test/webClientTestUtils.ts @@ -54,6 +54,72 @@ export const mintTransaction = async ( ); }; +export const sendTransaction = async ( + senderAccountId: string, + targetAccountId: string, + faucetAccountId: string, + amount: number, + recallHeight?: number +) => { + return testingPage.evaluate( + async ( + _senderAccountId, + _targetAccountId, + _faucetAccountId, + _amount, + _recallHeight + ) => { + const client = window.client; + + const senderAccountId = window.AccountId.from_hex(_senderAccountId); + const targetAccountId = window.AccountId.from_hex(_targetAccountId); + const faucetAccountId = window.AccountId.from_hex(_faucetAccountId); + + await client.fetch_and_cache_account_auth_by_pub_key( + window.AccountId.from_hex(_faucetAccountId) + ); + let mint_transaction_result = await client.new_mint_transaction( + senderAccountId, + window.AccountId.from_hex(_faucetAccountId), + window.NoteType.private(), + BigInt(_amount) + ); + let created_notes = mint_transaction_result.created_notes().notes(); + let created_note_ids = created_notes.map((note) => note.id().to_string()); + await new Promise((r) => setTimeout(r, 20000)); // TODO: Replace this with loop of sync -> check uncommitted transactions -> sleep + await client.sync_state(); + + await client.fetch_and_cache_account_auth_by_pub_key(senderAccountId); + await client.new_consume_transaction(senderAccountId, created_note_ids); + await new Promise((r) => setTimeout(r, 20000)); // TODO: Replace this with loop of sync -> check uncommitted transactions -> sleep + await client.sync_state(); + + await client.fetch_and_cache_account_auth_by_pub_key(senderAccountId); + let send_transaction_result = await client.new_send_transaction( + senderAccountId, + targetAccountId, + faucetAccountId, + window.NoteType.public(), + BigInt(_amount), + _recallHeight + ); + let send_created_notes = send_transaction_result.created_notes().notes(); + let send_created_note_ids = send_created_notes.map((note) => + note.id().to_string() + ); + await new Promise((r) => setTimeout(r, 20000)); // TODO: Replace this with loop of sync -> check uncommitted transactions -> sleep + await client.sync_state(); + + return send_created_note_ids; + }, + senderAccountId, + targetAccountId, + faucetAccountId, + amount, + recallHeight + ); +}; + interface ConsumeTransactionResult { transactionId: string; nonce: string | undefined; @@ -145,7 +211,10 @@ export const fetchAndCacheAccountAuth = async (accountId: string) => { export const syncState = async () => { return await testingPage.evaluate(async () => { const client = window.client; - await client.sync_state(); + const summary = await client.sync_state(); + return { + blockNum: summary.block_num(), + }; }); };