Skip to content

Commit 31d2684

Browse files
authored
feat(web-client): add getMapEntries API for StorageMap iteration (0xMiden#1323)
1 parent 4135581 commit 31d2684

File tree

6 files changed

+134
-13
lines changed

6 files changed

+134
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Implemented account lazy loading with more granular account data getters ([#1321](https://github.com/0xMiden/miden-client/pull/1321)).
1414
* Added `NoAuth` component to the web client ([#1330](https://github.com/0xMiden/miden-client/pull/1330)).
1515
* Implemented shared source manager for better error reporting ([#1275](https://github.com/0xMiden/miden-client/pull/1275)).
16+
* Added `getMapEntries` method to `AccountStorage` in web client for iterating storage map entries ([#1323](https://github.com/0xMiden/miden-client/pull/1323)).
1617

1718
### Changes
1819

crates/web-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@demox-labs/miden-sdk",
3-
"version": "0.12.0-next.22",
3+
"version": "0.12.0-next.23",
44
"description": "Miden Wasm SDK",
55
"collaborators": [
66
"Miden",

crates/web-client/src/models/account_storage.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use miden_client::account::AccountStorage as NativeAccountStorage;
1+
use idxdb_store::account::JsStorageMapEntry;
2+
use miden_client::account::{AccountStorage as NativeAccountStorage, StorageSlot};
23
use wasm_bindgen::prelude::*;
34

45
use crate::models::word::Word;
@@ -22,6 +23,26 @@ impl AccountStorage {
2223
pub fn get_map_item(&self, index: u8, key: &Word) -> Option<Word> {
2324
self.0.get_map_item(index, key.into()).ok().map(Into::into)
2425
}
26+
27+
/// Get all key-value pairs from the map slot at `index`.
28+
/// Returns `undefined` if the slot isn't a map or `index` is out of bounds (0-255).
29+
/// Returns `[]` if the map exists but is empty.
30+
#[wasm_bindgen(js_name = "getMapEntries")]
31+
pub fn get_map_entries(&self, index: u8) -> Option<Vec<JsStorageMapEntry>> {
32+
let slots = self.0.slots();
33+
match slots.get(index as usize) {
34+
Some(StorageSlot::Map(map)) => Some(
35+
map.entries()
36+
.map(|(key, value)| JsStorageMapEntry {
37+
root: map.root().to_hex(),
38+
key: key.to_hex(),
39+
value: value.to_hex(),
40+
})
41+
.collect(),
42+
),
43+
_ => None,
44+
}
45+
}
2546
}
2647

2748
// CONVERSIONS

crates/web-client/test/new_account.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,45 @@ test.describe("new_faucet tests", () => {
187187
);
188188
});
189189
});
190+
191+
// AccountStorage.getMapEntries tests
192+
// =======================================================================================================
193+
194+
test.describe("AccountStorage.getMapEntries tests", () => {
195+
test("returns null for invalid indices and adds test for storage map", async ({
196+
page,
197+
}) => {
198+
const result = await page.evaluate(async () => {
199+
const client = window.client;
200+
201+
// Create a new wallet with private storage
202+
const account = await client.newWallet(
203+
window.AccountStorageMode.private(),
204+
true
205+
);
206+
207+
// Get the account to access its storage
208+
const accountRecord = await client.getAccount(account.id());
209+
if (!accountRecord) {
210+
throw new Error("Account not found");
211+
}
212+
213+
const storage = accountRecord.storage();
214+
215+
// Test non-map storage slot (slot 0 should be empty for a new account)
216+
const nonMapResult = storage.getMapEntries(0);
217+
218+
// Test out of bounds index
219+
const outOfBoundsResult = storage.getMapEntries(255);
220+
221+
return {
222+
nonMap: nonMapResult,
223+
outOfBounds: outOfBoundsResult,
224+
};
225+
});
226+
227+
// For invalid cases, getMapEntries should return undefined
228+
expect(result.nonMap).toBeUndefined();
229+
expect(result.outOfBounds).toBeUndefined();
230+
});
231+
});

crates/web-client/test/new_transactions.test.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,12 @@ export const testStorageMap = async (page: Page): Promise<any> => {
12071207
const client = window.client;
12081208
await client.syncState();
12091209

1210+
const normalizeHexWord = (hex) => {
1211+
if (!hex) return undefined;
1212+
const normalized = hex.replace(/^0x/, "").replace(/^0+|0+$/g, "");
1213+
return normalized;
1214+
};
1215+
12101216
// BUILD ACCOUNT WITH COMPONENT THAT MODIFIES STORAGE MAP
12111217
// --------------------------------------------------------------------------
12121218

@@ -1217,6 +1223,10 @@ export const testStorageMap = async (page: Page): Promise<any> => {
12171223

12181224
let storageMap = new window.StorageMap();
12191225
storageMap.insert(MAP_KEY, FPI_STORAGE_VALUE);
1226+
storageMap.insert(
1227+
new window.Word(new BigUint64Array([2n, 2n, 2n, 2n])),
1228+
new window.Word(new BigUint64Array([0n, 0n, 0n, 9n]))
1229+
);
12201230

12211231
const accountCode = `export.bump_map_item
12221232
# map key
@@ -1230,12 +1240,7 @@ export const testStorageMap = async (page: Page): Promise<any> => {
12301240
push.0
12311241
# => [index, KEY, BUMPED_VALUE]
12321242
exec.::miden::account::set_map_item
1233-
dropw
1234-
# => [OLD_VALUE]
1235-
dupw
1236-
push.0
1237-
# Set a new item each time as the value keeps changing
1238-
exec.::miden::account::set_map_item
1243+
# => [OLD_MAP_ROOT, OLD_VALUE]
12391244
dropw dropw
12401245
end
12411246
`;
@@ -1261,6 +1266,7 @@ export const testStorageMap = async (page: Page): Promise<any> => {
12611266

12621267
await client.addAccountSecretKeyToWebStore(secretKey);
12631268
await client.newAccount(bumpItemAccountBuilderResult.account, false);
1269+
await client.syncState();
12641270

12651271
let initialMapValue = (
12661272
await client.getAccount(bumpItemAccountBuilderResult.account.id())
@@ -1305,20 +1311,51 @@ export const testStorageMap = async (page: Page): Promise<any> => {
13051311
.getMapItem(1, MAP_KEY)
13061312
?.toHex();
13071313

1314+
// Test getMapEntries() functionality
1315+
let accountStorage = (
1316+
await client.getAccount(bumpItemAccountBuilderResult.account.id())
1317+
)?.storage();
1318+
let mapEntries = accountStorage?.getMapEntries(1);
1319+
1320+
// Verify we get the expected entries
1321+
let expectedKey = MAP_KEY.toHex();
1322+
let expectedValue = normalizeHexWord(finalMapValue);
1323+
1324+
let mapEntriesData = {
1325+
entriesCount: mapEntries?.length || 0,
1326+
hasExpectedEntry: false,
1327+
expectedKey: expectedKey,
1328+
expectedValue: expectedValue,
1329+
};
1330+
1331+
if (expectedValue && mapEntries && mapEntries.length > 0) {
1332+
mapEntriesData.hasExpectedEntry = mapEntries.some(
1333+
(entry) =>
1334+
entry.key === expectedKey &&
1335+
normalizeHexWord(entry.value) === expectedValue
1336+
);
1337+
}
1338+
13081339
return {
1309-
initialMapValue: initialMapValue
1310-
?.replace(/^0x/, "")
1311-
.replace(/^0+|0+$/g, ""),
1312-
finalMapValue: finalMapValue?.replace(/^0x/, "").replace(/^0+|0+$/g, ""),
1340+
initialMapValue: normalizeHexWord(initialMapValue),
1341+
finalMapValue: normalizeHexWord(finalMapValue),
1342+
mapEntries: mapEntriesData,
13131343
};
13141344
});
13151345
};
13161346

13171347
test.describe("storage map test", () => {
13181348
test.setTimeout(50000);
13191349
test("storage map is updated correctly in transaction", async ({ page }) => {
1320-
let { initialMapValue, finalMapValue } = await testStorageMap(page);
1350+
let { initialMapValue, finalMapValue, mapEntries } =
1351+
await testStorageMap(page);
13211352
expect(initialMapValue).toBe("1");
13221353
expect(finalMapValue).toBe("2");
1354+
1355+
// Test getMapEntries() functionality
1356+
expect(mapEntries.entriesCount).toBeGreaterThan(1);
1357+
expect(mapEntries.hasExpectedEntry).toBe(true);
1358+
expect(mapEntries.expectedKey).toBeDefined();
1359+
expect(mapEntries.expectedValue).toBe("2");
13231360
});
13241361
});

docs/src/web-client/api/classes/AccountStorage.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,26 @@
4444

4545
***
4646

47+
### getMapEntries()
48+
49+
> **getMapEntries**(`index`): [`JsStorageMapEntry`](JsStorageMapEntry.md)[]
50+
51+
Get all key-value pairs from the map slot at `index`.
52+
Returns `undefined` if the slot isn't a map or `index` is out of bounds (0-255).
53+
Returns `[]` if the map exists but is empty.
54+
55+
#### Parameters
56+
57+
##### index
58+
59+
`number`
60+
61+
#### Returns
62+
63+
[`JsStorageMapEntry`](JsStorageMapEntry.md)[]
64+
65+
***
66+
4767
### getMapItem()
4868

4969
> **getMapItem**(`index`, `key`): [`Word`](Word.md)

0 commit comments

Comments
 (0)