@openzeppelin/miden-multisig-client 0.14.9 → 0.15.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/account/builder.d.ts.map +1 -1
  2. package/dist/account/builder.js +4 -3
  3. package/dist/account/builder.js.map +1 -1
  4. package/dist/account/builder.test.js +0 -3
  5. package/dist/account/builder.test.js.map +1 -1
  6. package/dist/account/masm/auth.d.ts +2 -2
  7. package/dist/account/masm/auth.d.ts.map +1 -1
  8. package/dist/account/masm/auth.js +22 -10
  9. package/dist/account/masm/auth.js.map +1 -1
  10. package/dist/account/masm/authLibraries.d.ts +6 -0
  11. package/dist/account/masm/authLibraries.d.ts.map +1 -0
  12. package/dist/account/masm/authLibraries.js +17 -0
  13. package/dist/account/masm/authLibraries.js.map +1 -0
  14. package/dist/lookupAuth.d.ts +4 -3
  15. package/dist/lookupAuth.d.ts.map +1 -1
  16. package/dist/lookupAuth.js +8 -7
  17. package/dist/lookupAuth.js.map +1 -1
  18. package/dist/lookupAuth.test.js +3 -2
  19. package/dist/lookupAuth.test.js.map +1 -1
  20. package/dist/multisig.d.ts.map +1 -1
  21. package/dist/multisig.js +34 -13
  22. package/dist/multisig.js.map +1 -1
  23. package/dist/multisig.test.js +118 -4
  24. package/dist/multisig.test.js.map +1 -1
  25. package/dist/procedures.d.ts +7 -7
  26. package/dist/procedures.js +7 -7
  27. package/dist/raw-client.d.ts.map +1 -1
  28. package/dist/raw-client.js +4 -4
  29. package/dist/raw-client.js.map +1 -1
  30. package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
  31. package/dist/transaction/updateProcedureThreshold.js +10 -4
  32. package/dist/transaction/updateProcedureThreshold.js.map +1 -1
  33. package/dist/transaction/updateSigners.d.ts.map +1 -1
  34. package/dist/transaction/updateSigners.js +9 -3
  35. package/dist/transaction/updateSigners.js.map +1 -1
  36. package/dist/utils/digest.d.ts.map +1 -1
  37. package/dist/utils/digest.js +4 -3
  38. package/dist/utils/digest.js.map +1 -1
  39. package/dist/utils/felt.d.ts +12 -0
  40. package/dist/utils/felt.d.ts.map +1 -0
  41. package/dist/utils/felt.js +19 -0
  42. package/dist/utils/felt.js.map +1 -0
  43. package/dist/utils/index.d.ts +1 -0
  44. package/dist/utils/index.d.ts.map +1 -1
  45. package/dist/utils/index.js +1 -0
  46. package/dist/utils/index.js.map +1 -1
  47. package/dist/utils/word.d.ts.map +1 -1
  48. package/dist/utils/word.js +7 -2
  49. package/dist/utils/word.js.map +1 -1
  50. package/masm/auth/multisig.masm +11 -5
  51. package/masm/auth/multisig_ecdsa.masm +11 -5
  52. package/package.json +3 -3
  53. package/src/account/builder.test.ts +0 -3
  54. package/src/account/builder.ts +3 -3
  55. package/src/account/masm/auth.ts +22 -10
  56. package/src/lookupAuth.test.ts +3 -2
  57. package/src/lookupAuth.ts +7 -6
  58. package/src/multisig.test.ts +123 -4
  59. package/src/multisig.ts +38 -14
  60. package/src/procedures.ts +7 -7
  61. package/src/raw-client.ts +4 -4
  62. package/src/transaction/updateProcedureThreshold.ts +16 -7
  63. package/src/transaction/updateSigners.ts +15 -6
  64. package/src/utils/digest.ts +4 -3
  65. package/src/utils/felt.ts +20 -0
  66. package/src/utils/index.ts +1 -0
  67. package/src/utils/word.ts +8 -2
@@ -1300,6 +1300,24 @@ describe('Multisig', () => {
1300
1300
  ok: true,
1301
1301
  json: async () => ({ commitment: newGuardianPubkey }),
1302
1302
  });
1303
+ mockFetch.mockResolvedValueOnce({
1304
+ ok: true,
1305
+ json: async () => ({
1306
+ delta: {
1307
+ account_id: multisig.accountId,
1308
+ nonce: 1,
1309
+ prev_commitment: '0x' + 'b'.repeat(64),
1310
+ delta_payload: { tx_summary: { data: 'AQID' }, signatures: [] },
1311
+ status: {
1312
+ status: 'pending',
1313
+ timestamp: '2024-01-01T00:00:00Z',
1314
+ proposer_id: '0x' + 'c'.repeat(64),
1315
+ cosigner_sigs: [],
1316
+ },
1317
+ },
1318
+ commitment: '0x' + 'c'.repeat(64),
1319
+ }),
1320
+ });
1303
1321
 
1304
1322
  const proposal = await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianPubkey);
1305
1323
 
@@ -1361,6 +1379,24 @@ describe('Multisig', () => {
1361
1379
  ok: true,
1362
1380
  json: async () => ({ commitment: newGuardianCommitment }),
1363
1381
  });
1382
+ mockFetch.mockResolvedValueOnce({
1383
+ ok: true,
1384
+ json: async () => ({
1385
+ delta: {
1386
+ account_id: multisig.accountId,
1387
+ nonce: 1,
1388
+ prev_commitment: '0x' + 'b'.repeat(64),
1389
+ delta_payload: { tx_summary: { data: 'AQID' }, signatures: [] },
1390
+ status: {
1391
+ status: 'pending',
1392
+ timestamp: '2024-01-01T00:00:00Z',
1393
+ proposer_id: '0x' + 'c'.repeat(64),
1394
+ cosigner_sigs: [],
1395
+ },
1396
+ },
1397
+ commitment: '0x' + 'c'.repeat(64),
1398
+ }),
1399
+ });
1364
1400
 
1365
1401
  await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianCommitment);
1366
1402
 
@@ -2194,11 +2230,9 @@ describe('Multisig', () => {
2194
2230
  const newGuardianPubkey = '0x' + '1'.repeat(64);
2195
2231
  const finalRequest = { kind: 'final-switch-guardian-request' };
2196
2232
 
2233
+ // switch_guardian is exempt from binding re-execution, so only the single
2234
+ // final-request build happens here.
2197
2235
  vi.mocked(buildUpdateGuardianTransactionRequest)
2198
- .mockResolvedValueOnce({
2199
- request: { kind: 'verify-switch-guardian-request' },
2200
- salt: { toHex: () => '0x' + 'd'.repeat(64) },
2201
- } as any)
2202
2236
  .mockResolvedValueOnce({
2203
2237
  request: finalRequest,
2204
2238
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
@@ -2998,6 +3032,91 @@ describe('Multisig', () => {
2998
3032
  ok: true,
2999
3033
  json: async () => ({ commitment: newGuardianPubkey }),
3000
3034
  });
3035
+ // Pre-switch canonicalization push: getDeltaProposal then pushDelta.
3036
+ mockFetch.mockResolvedValueOnce({
3037
+ ok: true,
3038
+ json: async () => ({
3039
+ account_id: multisig.accountId,
3040
+ nonce: 1,
3041
+ prev_commitment: '0x' + 'b'.repeat(64),
3042
+ delta_payload: {
3043
+ tx_summary: { data: 'AQID' },
3044
+ signatures: [],
3045
+ metadata: {
3046
+ proposal_type: 'switch_guardian',
3047
+ new_guardian_pubkey: newGuardianPubkey,
3048
+ new_guardian_endpoint: 'http://new-guardian.com',
3049
+ },
3050
+ },
3051
+ status: {
3052
+ status: 'pending',
3053
+ timestamp: '2024-01-01T00:00:00Z',
3054
+ proposer_id: '0x' + 'a'.repeat(64),
3055
+ cosigner_sigs: [],
3056
+ },
3057
+ }),
3058
+ });
3059
+ mockFetch.mockResolvedValueOnce({
3060
+ ok: true,
3061
+ json: async () => ({
3062
+ account_id: multisig.accountId,
3063
+ nonce: 1,
3064
+ ack_sig: '0x' + '6'.repeat(130),
3065
+ ack_pubkey: '0x' + 'f'.repeat(64),
3066
+ ack_scheme: 'falcon',
3067
+ }),
3068
+ });
3069
+ mockWebClient.getAccount.mockResolvedValueOnce({
3070
+ serialize: () => new Uint8Array([1, 2, 3]),
3071
+ });
3072
+ mockFetch.mockResolvedValueOnce({
3073
+ ok: true,
3074
+ json: async () => ({ success: true, message: 'ok', ack_pubkey: '0x' + 'f'.repeat(64) }),
3075
+ });
3076
+ mockWebClient.transactions.submit.mockResolvedValueOnce({});
3077
+
3078
+ await expect(multisig.executeProposal(proposalId)).resolves.toBeUndefined();
3079
+ expect(mockWebClient.transactions.submit).toHaveBeenCalledTimes(1);
3080
+ });
3081
+
3082
+ it('should still switch GUARDIAN when the pre-switch canonicalization push fails', async () => {
3083
+ const config = {
3084
+ threshold: 1,
3085
+ signerCommitments: ['0x' + 'a'.repeat(64)],
3086
+ guardianCommitment: '0x' + 'c'.repeat(64),
3087
+ };
3088
+
3089
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3090
+ const proposalId = '0x' + 'c'.repeat(64);
3091
+ const newGuardianPubkey = '0x' + '1'.repeat(64);
3092
+
3093
+ (multisig as any).proposals.set(proposalId, {
3094
+ id: proposalId,
3095
+ accountId: multisig.accountId,
3096
+ nonce: 1,
3097
+ status: 'ready',
3098
+ txSummary: 'AQID',
3099
+ signatures: [
3100
+ {
3101
+ signerId: '0x' + 'a'.repeat(64),
3102
+ signature: { scheme: 'falcon', signature: '0x' + 'b'.repeat(128) },
3103
+ timestamp: '2024-01-01T00:00:00Z',
3104
+ },
3105
+ ],
3106
+ metadata: {
3107
+ proposalType: 'switch_guardian',
3108
+ newGuardianPubkey,
3109
+ newGuardianEndpoint: 'http://new-guardian.com',
3110
+ description: '',
3111
+ },
3112
+ });
3113
+
3114
+ mockFetch.mockResolvedValueOnce({
3115
+ ok: true,
3116
+ json: async () => ({ commitment: newGuardianPubkey }),
3117
+ });
3118
+ // getDeltaProposal against the old GUARDIAN fails — must be swallowed.
3119
+ mockFetch.mockRejectedValueOnce(new Error('pre-switch GUARDIAN unreachable'));
3001
3120
  mockWebClient.getAccount.mockResolvedValueOnce({
3002
3121
  serialize: () => new Uint8Array([1, 2, 3]),
3003
3122
  });
package/src/multisig.ts CHANGED
@@ -739,19 +739,9 @@ export class Multisig {
739
739
  description: `Switch GUARDIAN to ${newGuardianEndpoint}`,
740
740
  };
741
741
 
742
- const proposalId = computeCommitmentFromTxSummary(summaryBase64);
743
- const proposal: Proposal = {
744
- id: proposalId,
745
- accountId: this._accountId,
746
- nonce: proposalNonce,
747
- status: 'pending',
748
- txSummary: summaryBase64,
749
- signatures: [],
750
- metadata,
751
- };
752
-
753
- this.proposals.set(proposal.id, proposal);
754
- return proposal;
742
+ // SwitchGuardian is a regular delta proposal; push it to GUARDIAN so
743
+ // sign/execute (which fetch from GUARDIAN) can find it.
744
+ return this.createProposal(proposalNonce, summaryBase64, metadata);
755
745
  }
756
746
 
757
747
  /**
@@ -879,7 +869,12 @@ export class Multisig {
879
869
  );
880
870
 
881
871
  if (canConsumeNow) {
882
- const noteId = inputNote.id().toString();
872
+ // Miden 0.15: InputNoteRecord.id() is `NoteId | undefined`; skip id-less records.
873
+ const id = inputNote.id();
874
+ if (id === undefined) {
875
+ continue;
876
+ }
877
+ const noteId = id.toString();
883
878
  const details = inputNote.details();
884
879
  const fungibleAssets = details.assets().fungibleAssets();
885
880
 
@@ -976,6 +971,24 @@ export class Multisig {
976
971
  throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
977
972
  }
978
973
 
974
+ // Canonicalize the executed delta on the pre-switch GUARDIAN (clears the
975
+ // pending proposal). Must run before `this.guardian` is repointed below.
976
+ // Best-effort: an unreachable old GUARDIAN must not block the switch, so
977
+ // errors are swallowed (mirrors the Rust execute path).
978
+ try {
979
+ const normalizedProposalId = normalizeHexWord(proposal.id);
980
+ const switchDelta = await this.guardian.getDeltaProposal(
981
+ this._accountId,
982
+ normalizedProposalId,
983
+ );
984
+ await this.guardian.pushDelta({
985
+ ...switchDelta,
986
+ deltaPayload: switchDelta.deltaPayload.txSummary,
987
+ });
988
+ } catch {
989
+ // best-effort; see above
990
+ }
991
+
979
992
  try {
980
993
  const webClient = await this.getRawClient();
981
994
  await webClient.syncState();
@@ -1556,6 +1569,17 @@ export class Multisig {
1556
1569
  return txSummaryCommitment;
1557
1570
  }
1558
1571
 
1572
+ if (proposal.metadata.proposalType === 'switch_guardian') {
1573
+ // Exempt from binding re-execution (mirrors the `custom` exemption above).
1574
+ // The WASM `executeForSummary` leaves the guardian-disabling side effect
1575
+ // applied to the in-session account, so re-execution reconstructs a smaller
1576
+ // delta and falsely rejects with "metadata does not match tx_summary". The
1577
+ // native Rust client does not mutate, so this is an intentional divergence.
1578
+ // The id ↔ tx_summary match above plus `verifyGuardianEndpointCommitment`
1579
+ // at propose/execute time still bind the proposal.
1580
+ return txSummaryCommitment;
1581
+ }
1582
+
1559
1583
  const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
1560
1584
  const salt = proposal.metadata.saltHex
1561
1585
  ? Word.fromHex(normalizeHexWord(proposal.metadata.saltHex))
package/src/procedures.ts CHANGED
@@ -11,13 +11,13 @@
11
11
  * human-readable encoding and should not be copied into this table.
12
12
  */
13
13
  export const PROCEDURE_ROOTS = {
14
- update_signers: '0x3d382ad461f9914c487c6fe908991d088eb54ecbd4aa8560ef79c66c3746bf19',
15
- update_procedure_threshold: '0x1f43e9d56ceff5d547ffdcb89896fb38cae0be1b74d9235ed2b4aa525df85f8d',
16
- auth_tx: '0x415530d7169f849d7219e810065f9119bba9af2c55070de0bf4f082a1c0aea5c',
17
- update_guardian: '0xc8ea876f1837e5cd1d6031becdbd40ce262ecd55930d65400f6890a37149d80c',
18
- verify_guardian: '0x9bc6e7b25c8dbaa29d6ad41e354a545dd0a4bac7f3a521bb5195ba101f0213cc',
19
- send_asset: '0x6d30df4312a2c44ec842db1bee227cc045396ca91e2c47d756dcb607f2bf5f89',
20
- receive_asset: '0x75f638c65584d058542bcf4674b066ae394183021bc9b44dc2fdd97d52f9bcfb',
14
+ update_signers: '0x34963b067dbba634e57b416bc2f2a9a8d4ac24147f40b2900148c9ba44774274',
15
+ update_procedure_threshold: '0xec74c4b96ce593c11017ae54dec9c0ae5e0d242e8b3074eb3908d961300aed67',
16
+ auth_tx: '0x0708020dce7b91b61116e3eb27e5d686e129a83df3c540e0a7693b4523814e72',
17
+ update_guardian: '0xeceb1f2c2d7d20312dbaf091e9a27a2b63f9fcba120948043069793a5715bc96',
18
+ verify_guardian: '0xe6a8a62d37117f55a79b5345aa3d263ab16e973d486bac9a1612663dfdecf82d',
19
+ send_asset: '0xfb1c73d10de1954e9e8948964e3e77cf4e33759d2e012cb00eb10c50f2974eb4',
20
+ receive_asset: '0x6170fd6d682d91777b551fd866258f43cc657f1291f8f071500f4e56e9c153da',
21
21
  } as const;
22
22
 
23
23
  /**
package/src/raw-client.ts CHANGED
@@ -24,12 +24,12 @@ function isPublicMidenClient(client: RawClientSource): client is MidenClient {
24
24
  return 'accounts' in client && 'sync' in client;
25
25
  }
26
26
 
27
- export function getRawMidenClient(
27
+ export async function getRawMidenClient(
28
28
  client: RawClientSource,
29
29
  rpcUrl?: string,
30
30
  ): Promise<WasmWebClient> {
31
31
  if (!isPublicMidenClient(client)) {
32
- return Promise.resolve(client);
32
+ return client;
33
33
  }
34
34
 
35
35
  const cached = rawClientCache.get(client);
@@ -41,7 +41,7 @@ export function getRawMidenClient(
41
41
  resolveMidenRpcEndpoint(rpcUrl),
42
42
  undefined,
43
43
  undefined,
44
- client.storeIdentifier(),
44
+ await client.storeIdentifier(),
45
45
  );
46
46
  rawClientCache.set(client, rawClient);
47
47
  return rawClient;
@@ -62,7 +62,7 @@ export async function compileTxScript(
62
62
  }
63
63
 
64
64
  const rawClient = await getRawMidenClient(client, rpcUrl);
65
- const builder = rawClient.createCodeBuilder();
65
+ const builder = await rawClient.createCodeBuilder();
66
66
  for (const library of libraries) {
67
67
  const builtLibrary = builder.buildLibrary(library.namespace, library.code);
68
68
  if (library.linking === 'static') {
@@ -21,19 +21,28 @@ import { randomWord } from '../utils/random.js';
21
21
  import type { SignatureOptions } from './options.js';
22
22
  import type { SignatureScheme } from '../types.js';
23
23
 
24
- function buildProcedureThresholdAdvice(
25
- procedure: ProcedureName,
26
- threshold: number,
27
- ): { configHash: Word; payload: FeltArray } {
24
+ function buildProcedureThresholdFelts(procedure: ProcedureName, threshold: number): Felt[] {
28
25
  const procedureRoot = WordType.fromHex(normalizeHexWord(getProcedureRoot(procedure)));
29
- const payload = new FeltArray([
26
+ return [
30
27
  ...procedureRoot.toFelts(),
31
28
  new Felt(BigInt(threshold)),
32
29
  new Felt(0n),
33
30
  new Felt(0n),
34
31
  new Felt(0n),
35
- ]);
36
- const configHash = Poseidon2.hashElements(payload);
32
+ ];
33
+ }
34
+
35
+ function buildProcedureThresholdAdvice(
36
+ procedure: ProcedureName,
37
+ threshold: number,
38
+ ): { configHash: Word; payload: FeltArray } {
39
+ // `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the
40
+ // advice payload must be a freshly built one — reusing the hashed array
41
+ // surfaces as "null pointer passed to rust" at the later `advice.insert`.
42
+ const configHash = Poseidon2.hashElements(
43
+ new FeltArray(buildProcedureThresholdFelts(procedure, threshold)),
44
+ );
45
+ const payload = new FeltArray(buildProcedureThresholdFelts(procedure, threshold));
37
46
  return { configHash, payload };
38
47
  }
39
48
 
@@ -21,10 +21,7 @@ import { randomWord } from '../utils/random.js';
21
21
  import type { SignatureOptions } from './options.js';
22
22
  import type { SignatureScheme } from '../types.js';
23
23
 
24
- function buildMultisigConfigAdvice(
25
- threshold: number,
26
- signerCommitments: string[],
27
- ): { configHash: Word; payload: FeltArray } {
24
+ function buildMultisigConfigFelts(threshold: number, signerCommitments: string[]): Felt[] {
28
25
  const numApprovers = signerCommitments.length;
29
26
  const felts: Felt[] = [
30
27
  new Felt(BigInt(threshold)),
@@ -36,8 +33,20 @@ function buildMultisigConfigAdvice(
36
33
  const word = WordType.fromHex(normalizeHexWord(commitment));
37
34
  felts.push(...word.toFelts());
38
35
  }
39
- const payload = new FeltArray(felts);
40
- const configHash = Poseidon2.hashElements(payload);
36
+ return felts;
37
+ }
38
+
39
+ function buildMultisigConfigAdvice(
40
+ threshold: number,
41
+ signerCommitments: string[],
42
+ ): { configHash: Word; payload: FeltArray } {
43
+ // `Poseidon2.hashElements` consumes (frees) its `FeltArray` by value, so the
44
+ // advice payload must be a freshly built one — reusing the hashed array
45
+ // surfaces as "null pointer passed to rust" at the later `advice.insert`.
46
+ const configHash = Poseidon2.hashElements(
47
+ new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments)),
48
+ );
49
+ const payload = new FeltArray(buildMultisigConfigFelts(threshold, signerCommitments));
41
50
  return { configHash, payload };
42
51
  }
43
52
 
@@ -1,5 +1,6 @@
1
1
  import type { RequestAuthPayload } from '@openzeppelin/guardian-client';
2
2
  import { AccountId, Felt, FeltArray, Rpo256, Word } from '@miden-sdk/miden-sdk';
3
+ import { feltFromU64Reduced } from './felt.js';
3
4
 
4
5
  export class AuthDigest {
5
6
  static fromAccountIdWithTimestamp(accountId: string, timestamp: number): Word {
@@ -11,7 +12,7 @@ export class AuthDigest {
11
12
  const feltArray = new FeltArray([
12
13
  prefix,
13
14
  suffix,
14
- new Felt(BigInt(timestamp)),
15
+ feltFromU64Reduced(BigInt(timestamp)),
15
16
  new Felt(0n),
16
17
  ]);
17
18
 
@@ -39,7 +40,7 @@ export class AuthDigest {
39
40
  const feltArray = new FeltArray([
40
41
  prefix,
41
42
  suffix,
42
- new Felt(BigInt(timestamp)),
43
+ feltFromU64Reduced(BigInt(timestamp)),
43
44
  ...payloadWord.toFelts(),
44
45
  ]);
45
46
 
@@ -67,7 +68,7 @@ export class AuthDigest {
67
68
  for (let j = 0; j < 8 && i + j < bytes.length; j += 1) {
68
69
  packed |= BigInt(bytes[i + j]) << (8n * BigInt(j));
69
70
  }
70
- payloadElements.push(new Felt(packed));
71
+ payloadElements.push(feltFromU64Reduced(packed));
71
72
  }
72
73
 
73
74
  return Rpo256.hashElements(new FeltArray(payloadElements));
@@ -0,0 +1,20 @@
1
+ import { Felt } from '@miden-sdk/miden-sdk';
2
+
3
+ /**
4
+ * Goldilocks field order (`2^64 - 2^32 + 1`), matching `Felt::ORDER` in
5
+ * `miden-protocol`.
6
+ */
7
+ const FELT_ORDER = 18446744069414584321n;
8
+
9
+ /**
10
+ * Maps an arbitrary `u64` onto a canonical field element by reducing modulo the
11
+ * field order, mirroring `guardian_shared::felt::felt_from_u64_reduced`.
12
+ *
13
+ * Miden 0.15's `Felt` constructor rejects non-canonical inputs (values
14
+ * `>= FELT_ORDER`), whereas 0.14 reduced silently. Byte-packed digest inputs are
15
+ * arbitrary `u64`s, so reducing here preserves the original digest layout, keeps
16
+ * construction infallible, and stays byte-identical to server-side signing.
17
+ */
18
+ export function feltFromU64Reduced(value: bigint): Felt {
19
+ return new Felt(BigInt.asUintN(64, value) % FELT_ORDER);
20
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './encoding.js';
2
+ export * from './felt.js';
2
3
  export * from './random.js';
3
4
  export * from './signature.js';
4
5
  export * from './word.js';
package/src/utils/word.ts CHANGED
@@ -5,8 +5,14 @@ export function wordToHex(word: Word): string {
5
5
  }
6
6
 
7
7
  export function wordElementToBigInt(word: Word, index: number): bigint {
8
- const elements = word.toU64s();
9
- return index >= 0 && index < elements.length ? elements[index] : 0n;
8
+ if (index < 0 || index > 3) {
9
+ return 0n;
10
+ }
11
+ // The wallet-embedded 0.15 SDK lacks `toU64s()` on storage-read Words; fall
12
+ // back to `toFelts()`, which has the same element order.
13
+ const elements: BigUint64Array | bigint[] =
14
+ typeof word.toU64s === 'function' ? word.toU64s() : word.toFelts().map(f => f.asInt());
15
+ return index < elements.length ? elements[index] : 0n;
10
16
  }
11
17
 
12
18
  export function wordToBytes(word: { toFelts: () => Array<{ asInt: () => bigint }> }): Uint8Array {