@openzeppelin/miden-multisig-client 0.14.9-rc.0 → 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 +228 -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 +262 -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
@@ -44,6 +44,9 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
44
44
  toPreparedSignature: () => [1, 2, 3],
45
45
  }),
46
46
  },
47
+ TransactionRequest: {
48
+ deserialize: vi.fn().mockReturnValue({}),
49
+ },
47
50
  AdviceMap: vi.fn().mockImplementation(() => ({
48
51
  insert: vi.fn(),
49
52
  })),
@@ -1297,6 +1300,24 @@ describe('Multisig', () => {
1297
1300
  ok: true,
1298
1301
  json: async () => ({ commitment: newGuardianPubkey }),
1299
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
+ });
1300
1321
 
1301
1322
  const proposal = await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianPubkey);
1302
1323
 
@@ -1358,6 +1379,24 @@ describe('Multisig', () => {
1358
1379
  ok: true,
1359
1380
  json: async () => ({ commitment: newGuardianCommitment }),
1360
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
+ });
1361
1400
 
1362
1401
  await multisig.createSwitchGuardianProposal('http://new-guardian.com', newGuardianCommitment);
1363
1402
 
@@ -2191,11 +2230,9 @@ describe('Multisig', () => {
2191
2230
  const newGuardianPubkey = '0x' + '1'.repeat(64);
2192
2231
  const finalRequest = { kind: 'final-switch-guardian-request' };
2193
2232
 
2233
+ // switch_guardian is exempt from binding re-execution, so only the single
2234
+ // final-request build happens here.
2194
2235
  vi.mocked(buildUpdateGuardianTransactionRequest)
2195
- .mockResolvedValueOnce({
2196
- request: { kind: 'verify-switch-guardian-request' },
2197
- salt: { toHex: () => '0x' + 'd'.repeat(64) },
2198
- } as any)
2199
2236
  .mockResolvedValueOnce({
2200
2237
  request: finalRequest,
2201
2238
  salt: { toHex: () => '0x' + 'd'.repeat(64) },
@@ -2995,6 +3032,91 @@ describe('Multisig', () => {
2995
3032
  ok: true,
2996
3033
  json: async () => ({ commitment: newGuardianPubkey }),
2997
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'));
2998
3120
  mockWebClient.getAccount.mockResolvedValueOnce({
2999
3121
  serialize: () => new Uint8Array([1, 2, 3]),
3000
3122
  });
@@ -3144,6 +3266,142 @@ describe('Multisig', () => {
3144
3266
  });
3145
3267
  });
3146
3268
 
3269
+ describe('prepareCustomExecution', () => {
3270
+ const requestBytes = new Uint8Array([9, 8, 7]);
3271
+
3272
+ function customDelta(
3273
+ proposalType: string,
3274
+ cosignerSigs: any[],
3275
+ ): any {
3276
+ return {
3277
+ account_id: '0x' + 'a'.repeat(30),
3278
+ nonce: 1,
3279
+ prev_commitment: '0x' + 'b'.repeat(64),
3280
+ delta_payload: {
3281
+ tx_summary: { data: 'AQID' },
3282
+ signatures: [],
3283
+ metadata: {
3284
+ proposal_type: proposalType,
3285
+ description: '',
3286
+ },
3287
+ },
3288
+ status: {
3289
+ status: 'pending',
3290
+ timestamp: '2024-01-01T00:00:00Z',
3291
+ proposer_id: '0x' + 'c'.repeat(64),
3292
+ cosigner_sigs: cosignerSigs,
3293
+ },
3294
+ };
3295
+ }
3296
+
3297
+ function falconSig(signerId: string): any {
3298
+ return {
3299
+ signer_id: signerId,
3300
+ signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
3301
+ timestamp: '2024-01-01T00:00:00Z',
3302
+ };
3303
+ }
3304
+
3305
+ it('rejects a built-in proposal type', async () => {
3306
+ const config = {
3307
+ threshold: 1,
3308
+ signerCommitments: ['0x' + 'a'.repeat(64)],
3309
+ guardianCommitment: '0x' + 'c'.repeat(64),
3310
+ };
3311
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3312
+
3313
+ const builtinDelta = {
3314
+ ...customDelta('change_threshold', [falconSig('0x' + 'a'.repeat(64))]),
3315
+ };
3316
+ builtinDelta.delta_payload.metadata = {
3317
+ proposal_type: 'change_threshold',
3318
+ description: '',
3319
+ target_threshold: 1,
3320
+ signer_commitments: ['0x' + 'a'.repeat(64)],
3321
+ } as any;
3322
+
3323
+ mockFetch.mockResolvedValueOnce({
3324
+ ok: true,
3325
+ json: async () => builtinDelta,
3326
+ });
3327
+
3328
+ await expect(
3329
+ multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
3330
+ ).rejects.toThrow('prepareCustomExecution is only for custom proposals');
3331
+ });
3332
+
3333
+ it('rejects a proposal that is below its signature threshold', async () => {
3334
+ const config = {
3335
+ threshold: 2,
3336
+ signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
3337
+ guardianCommitment: '0x' + 'c'.repeat(64),
3338
+ };
3339
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3340
+
3341
+ mockFetch.mockResolvedValueOnce({
3342
+ ok: true,
3343
+ json: async () => customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]),
3344
+ });
3345
+
3346
+ await expect(
3347
+ multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
3348
+ ).rejects.toThrow('have 1 of 2 required signatures');
3349
+ });
3350
+
3351
+ it('rejects when the rebuilt request does not reproduce the signed commitment', async () => {
3352
+ const config = {
3353
+ threshold: 1,
3354
+ signerCommitments: ['0x' + 'a'.repeat(64)],
3355
+ guardianCommitment: '0x' + 'c'.repeat(64),
3356
+ };
3357
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3358
+
3359
+ // Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
3360
+ // Make the binding request derive a different commitment so the check fails.
3361
+ vi.mocked(executeForSummary).mockResolvedValueOnce({
3362
+ toCommitment: () => ({
3363
+ toHex: () => '0x' + '9'.repeat(64),
3364
+ }),
3365
+ serialize: () => new Uint8Array([1, 2, 3]),
3366
+ } as any);
3367
+
3368
+ mockFetch.mockResolvedValueOnce({
3369
+ ok: true,
3370
+ json: async () => customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]),
3371
+ });
3372
+
3373
+ await expect(
3374
+ multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
3375
+ ).rejects.toThrow('Custom proposal binding mismatch');
3376
+ });
3377
+
3378
+ it('fails when GUARDIAN does not return an acknowledgment signature', async () => {
3379
+ const config = {
3380
+ threshold: 1,
3381
+ signerCommitments: ['0x' + 'a'.repeat(64)],
3382
+ guardianCommitment: '0x' + 'c'.repeat(64),
3383
+ };
3384
+ const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
3385
+
3386
+ const ready = customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]);
3387
+
3388
+ // getDeltaProposal
3389
+ mockFetch.mockResolvedValueOnce({
3390
+ ok: true,
3391
+ json: async () => ready,
3392
+ });
3393
+ // pushDelta returns no ack_sig
3394
+ mockFetch.mockResolvedValueOnce({
3395
+ ok: true,
3396
+ json: async () => ({ ...ready, ack_sig: null }),
3397
+ });
3398
+
3399
+ await expect(
3400
+ multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
3401
+ ).rejects.toThrow('GUARDIAN did not return acknowledgment signature');
3402
+ });
3403
+ });
3404
+
3147
3405
  describe('proposal metadata preservation', () => {
3148
3406
  it('should preserve local metadata when syncing proposals', async () => {
3149
3407
  const config = {
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 {