@openzeppelin/miden-multisig-client 0.16.0 → 0.16.2
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.
- package/README.md +58 -2
- package/dist/client.d.ts +12 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -2
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +7 -2
- package/dist/client.test.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/multisig.d.ts +65 -2
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +148 -15
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +217 -13
- package/dist/multisig.test.js.map +1 -1
- package/dist/proposal/factory.d.ts.map +1 -1
- package/dist/proposal/factory.js +10 -0
- package/dist/proposal/factory.js.map +1 -1
- package/dist/proposal/factory.test.d.ts +2 -0
- package/dist/proposal/factory.test.d.ts.map +1 -0
- package/dist/proposal/factory.test.js +32 -0
- package/dist/proposal/factory.test.js.map +1 -0
- package/dist/prover/config.d.ts +16 -0
- package/dist/prover/config.d.ts.map +1 -0
- package/dist/prover/config.js +54 -0
- package/dist/prover/config.js.map +1 -0
- package/dist/prover/config.test.d.ts +2 -0
- package/dist/prover/config.test.d.ts.map +1 -0
- package/dist/prover/config.test.js +54 -0
- package/dist/prover/config.test.js.map +1 -0
- package/dist/prover/errors.d.ts +7 -0
- package/dist/prover/errors.d.ts.map +1 -0
- package/dist/prover/errors.js +10 -0
- package/dist/prover/errors.js.map +1 -0
- package/dist/prover/errors.test.d.ts +2 -0
- package/dist/prover/errors.test.d.ts.map +1 -0
- package/dist/prover/errors.test.js +29 -0
- package/dist/prover/errors.test.js.map +1 -0
- package/dist/prover/retry.d.ts +5 -0
- package/dist/prover/retry.d.ts.map +1 -0
- package/dist/prover/retry.js +9 -0
- package/dist/prover/retry.js.map +1 -0
- package/dist/prover/retry.test.d.ts +2 -0
- package/dist/prover/retry.test.d.ts.map +1 -0
- package/dist/prover/retry.test.js +20 -0
- package/dist/prover/retry.test.js.map +1 -0
- package/dist/prover/workflow.d.ts +11 -0
- package/dist/prover/workflow.d.ts.map +1 -0
- package/dist/prover/workflow.js +18 -0
- package/dist/prover/workflow.js.map +1 -0
- package/dist/prover/workflow.test.d.ts +2 -0
- package/dist/prover/workflow.test.d.ts.map +1 -0
- package/dist/prover/workflow.test.js +103 -0
- package/dist/prover/workflow.test.js.map +1 -0
- package/dist/retry/classify.d.ts +16 -0
- package/dist/retry/classify.d.ts.map +1 -0
- package/dist/retry/classify.js +187 -0
- package/dist/retry/classify.js.map +1 -0
- package/dist/retry/runtime.d.ts +13 -0
- package/dist/retry/runtime.d.ts.map +1 -0
- package/dist/retry/runtime.js +33 -0
- package/dist/retry/runtime.js.map +1 -0
- package/dist/rpc/config.d.ts +11 -0
- package/dist/rpc/config.d.ts.map +1 -0
- package/dist/rpc/config.js +17 -0
- package/dist/rpc/config.js.map +1 -0
- package/dist/rpc/config.test.d.ts +2 -0
- package/dist/rpc/config.test.d.ts.map +1 -0
- package/dist/rpc/config.test.js +24 -0
- package/dist/rpc/config.test.js.map +1 -0
- package/dist/rpc/errors.d.ts +2 -0
- package/dist/rpc/errors.d.ts.map +1 -0
- package/dist/rpc/errors.js +11 -0
- package/dist/rpc/errors.js.map +1 -0
- package/dist/rpc/errors.test.d.ts +2 -0
- package/dist/rpc/errors.test.d.ts.map +1 -0
- package/dist/rpc/errors.test.js +34 -0
- package/dist/rpc/errors.test.js.map +1 -0
- package/dist/rpc/retry.d.ts +4 -0
- package/dist/rpc/retry.d.ts.map +1 -0
- package/dist/rpc/retry.js +6 -0
- package/dist/rpc/retry.js.map +1 -0
- package/dist/rpc/retry.test.d.ts +2 -0
- package/dist/rpc/retry.test.d.ts.map +1 -0
- package/dist/rpc/retry.test.js +98 -0
- package/dist/rpc/retry.test.js.map +1 -0
- package/dist/transaction/p2id.d.ts +8 -1
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +12 -3
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/package.json +2 -2
- package/src/client.test.ts +8 -2
- package/src/client.ts +28 -2
- package/src/index.ts +6 -0
- package/src/multisig.test.ts +263 -17
- package/src/multisig.ts +201 -14
- package/src/proposal/factory.test.ts +40 -0
- package/src/proposal/factory.ts +11 -1
- package/src/prover/config.test.ts +90 -0
- package/src/prover/config.ts +78 -0
- package/src/prover/errors.test.ts +61 -0
- package/src/prover/errors.ts +10 -0
- package/src/prover/retry.test.ts +38 -0
- package/src/prover/retry.ts +21 -0
- package/src/prover/test-node.d.ts +6 -0
- package/src/prover/workflow.test.ts +140 -0
- package/src/prover/workflow.ts +19 -0
- package/src/retry/classify.ts +220 -0
- package/src/retry/runtime.ts +45 -0
- package/src/rpc/config.test.ts +45 -0
- package/src/rpc/config.ts +30 -0
- package/src/rpc/errors.test.ts +69 -0
- package/src/rpc/errors.ts +12 -0
- package/src/rpc/retry.test.ts +144 -0
- package/src/rpc/retry.ts +12 -0
- package/src/transaction/p2id.ts +29 -10
- package/src/transaction.ts +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { resolveRpcConfig } from './config.js';
|
|
4
|
+
import { retryRpcRead } from './retry.js';
|
|
5
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
6
|
+
import { retryDelay } from '../retry/runtime.js';
|
|
7
|
+
|
|
8
|
+
interface Fixtures {
|
|
9
|
+
delays: Array<{ retryIndex: number; unitRandom: number; delayMs: number }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fixtures(): Fixtures {
|
|
13
|
+
return JSON.parse(
|
|
14
|
+
readFileSync(
|
|
15
|
+
new URL(
|
|
16
|
+
'../../../../fixtures/miden-multisig-client/rpc-policy-fixtures.json',
|
|
17
|
+
import.meta.url,
|
|
18
|
+
),
|
|
19
|
+
'utf8',
|
|
20
|
+
),
|
|
21
|
+
) as Fixtures;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function recordingRuntime(): RetryRuntime & { sleeps: number[] } {
|
|
25
|
+
const sleeps: number[] = [];
|
|
26
|
+
return {
|
|
27
|
+
sleeps,
|
|
28
|
+
sleep: (delayMs) => {
|
|
29
|
+
sleeps.push(delayMs);
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
},
|
|
32
|
+
unitRandom: () => 0.5,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rateLimitError(): Error {
|
|
37
|
+
return Object.assign(new Error('Too Many Requests!'), {
|
|
38
|
+
code: 'ResourceExhausted',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('retryRpcRead', () => {
|
|
43
|
+
it('matches every shared delay vector', () => {
|
|
44
|
+
for (const fixture of fixtures().delays) {
|
|
45
|
+
expect(retryDelay(fixture.retryIndex, fixture.unitRandom)).toBe(fixture.delayMs);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('waits out rate limiting and returns the eventual result', async () => {
|
|
50
|
+
const runtime = recordingRuntime();
|
|
51
|
+
const operation = vi
|
|
52
|
+
.fn()
|
|
53
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
54
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
55
|
+
.mockResolvedValueOnce('synced');
|
|
56
|
+
|
|
57
|
+
const result = await retryRpcRead(
|
|
58
|
+
operation,
|
|
59
|
+
resolveRpcConfig({ retry: { maxAttempts: 3 } }),
|
|
60
|
+
runtime,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(result).toBe('synced');
|
|
64
|
+
expect(operation).toHaveBeenCalledTimes(3);
|
|
65
|
+
expect(runtime.sleeps).toEqual([retryDelay(0, 0.5), retryDelay(1, 0.5)]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws a permanent error immediately without sleeping', async () => {
|
|
69
|
+
const runtime = recordingRuntime();
|
|
70
|
+
const permanent = Object.assign(new Error('malformed account id'), {
|
|
71
|
+
code: 'InvalidArgument',
|
|
72
|
+
});
|
|
73
|
+
const operation = vi.fn().mockRejectedValue(permanent);
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 5 } }), runtime),
|
|
77
|
+
).rejects.toBe(permanent);
|
|
78
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(runtime.sleeps).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rethrows the final upstream error unchanged after budget exhaustion', async () => {
|
|
83
|
+
const runtime = recordingRuntime();
|
|
84
|
+
const final = Object.assign(new Error('still rate limited'), {
|
|
85
|
+
code: 'ResourceExhausted',
|
|
86
|
+
});
|
|
87
|
+
const operation = vi
|
|
88
|
+
.fn()
|
|
89
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
90
|
+
.mockRejectedValueOnce(final);
|
|
91
|
+
|
|
92
|
+
await expect(
|
|
93
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 2 } }), runtime),
|
|
94
|
+
).rejects.toBe(final);
|
|
95
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
96
|
+
expect(runtime.sleeps).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('retries a transient read once under the default config', async () => {
|
|
100
|
+
const runtime = recordingRuntime();
|
|
101
|
+
const operation = vi
|
|
102
|
+
.fn()
|
|
103
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
104
|
+
.mockResolvedValueOnce('synced');
|
|
105
|
+
|
|
106
|
+
await expect(retryRpcRead(operation, resolveRpcConfig(undefined), runtime)).resolves.toBe(
|
|
107
|
+
'synced',
|
|
108
|
+
);
|
|
109
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(runtime.sleeps).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('never retries under an explicit single-attempt config', async () => {
|
|
114
|
+
const runtime = recordingRuntime();
|
|
115
|
+
const operation = vi.fn().mockRejectedValue(rateLimitError());
|
|
116
|
+
|
|
117
|
+
await expect(
|
|
118
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 1 } }), runtime),
|
|
119
|
+
).rejects.toThrow('Too Many Requests!');
|
|
120
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(runtime.sleeps).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('completes 64 concurrent reads against a rate-limited node', async () => {
|
|
125
|
+
const runtime = recordingRuntime();
|
|
126
|
+
let rejected = 0;
|
|
127
|
+
const operation = () => {
|
|
128
|
+
if (rejected < 40) {
|
|
129
|
+
rejected += 1;
|
|
130
|
+
return Promise.reject(rateLimitError());
|
|
131
|
+
}
|
|
132
|
+
return Promise.resolve('synced');
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const results = await Promise.all(
|
|
136
|
+
Array.from({ length: 64 }, () =>
|
|
137
|
+
retryRpcRead(operation, resolveRpcConfig({ retry: { maxAttempts: 4 } }), runtime),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(results).toEqual(Array.from({ length: 64 }, () => 'synced'));
|
|
142
|
+
expect(rejected).toBe(40);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/rpc/retry.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedRpcConfig } from './config.js';
|
|
2
|
+
import { isTransientRpcError } from './errors.js';
|
|
3
|
+
import type { RetryRuntime } from '../retry/runtime.js';
|
|
4
|
+
import { productionRetryRuntime, retryTransient } from '../retry/runtime.js';
|
|
5
|
+
|
|
6
|
+
export async function retryRpcRead<T>(
|
|
7
|
+
operation: () => Promise<T>,
|
|
8
|
+
config: ResolvedRpcConfig,
|
|
9
|
+
runtime: RetryRuntime = productionRetryRuntime,
|
|
10
|
+
): Promise<T> {
|
|
11
|
+
return retryTransient(operation, config.maxAttempts, isTransientRpcError, runtime);
|
|
12
|
+
}
|
package/src/transaction/p2id.ts
CHANGED
|
@@ -114,28 +114,47 @@ function buildP2idNote(
|
|
|
114
114
|
return new Note(noteAssets, noteMetadata, noteRecipient);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Rebuilds the P2ID note a proposal creates, from its metadata fields. The
|
|
119
|
+
* note is deterministic in the salt, so the resulting ID matches the note the
|
|
120
|
+
* proposal produces on execution (issue #356). The asset is derived from the
|
|
121
|
+
* account's current vault state, so call this before the transfer executes.
|
|
122
|
+
*/
|
|
123
|
+
export function buildP2idNoteFromMetadata(
|
|
118
124
|
senderId: string,
|
|
119
125
|
recipientId: string,
|
|
120
126
|
faucetId: string,
|
|
121
127
|
amount: bigint,
|
|
122
128
|
account: Account,
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
noteType: NoteType,
|
|
130
|
+
saltHex: string,
|
|
131
|
+
): Note {
|
|
125
132
|
const sender = AccountId.fromHex(senderId);
|
|
126
133
|
const recipient = AccountId.fromHex(recipientId);
|
|
127
134
|
const faucet = AccountId.fromHex(faucetId);
|
|
128
135
|
|
|
129
|
-
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
130
|
-
|
|
131
136
|
const asset = resolveFungibleAssetFromVault(account, faucet, amount);
|
|
132
|
-
|
|
133
137
|
const noteAssets = new NoteAssets([asset]);
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
return buildP2idNote(sender, recipient, noteAssets, noteType, saltHex);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildP2idTransactionRequest(
|
|
143
|
+
senderId: string,
|
|
144
|
+
recipientId: string,
|
|
145
|
+
faucetId: string,
|
|
146
|
+
amount: bigint,
|
|
147
|
+
account: Account,
|
|
148
|
+
options: P2idTransactionOptions = {},
|
|
149
|
+
): { request: TransactionRequest; salt: Word } {
|
|
150
|
+
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
151
|
+
|
|
152
|
+
const note = buildP2idNoteFromMetadata(
|
|
153
|
+
senderId,
|
|
154
|
+
recipientId,
|
|
155
|
+
faucetId,
|
|
156
|
+
amount,
|
|
157
|
+
account,
|
|
139
158
|
options.noteType ?? NoteType.Public,
|
|
140
159
|
authSaltHex,
|
|
141
160
|
);
|