@cofhe/sdk 0.5.0 → 0.5.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/CHANGELOG.md +16 -0
- package/adapters/test/ethers5.test.ts +1 -1
- package/adapters/test/ethers6.test.ts +1 -1
- package/adapters/test/wagmi.test.ts +1 -1
- package/core/decrypt/decryptForTxBuilder.ts +21 -0
- package/core/decrypt/decryptForViewBuilder.ts +19 -0
- package/core/decrypt/submitRetry.ts +126 -0
- package/core/decrypt/tnDecryptV2.ts +48 -53
- package/core/decrypt/tnSealOutputV2.ts +48 -54
- package/core/test/decryptBuilders.test.ts +28 -0
- package/core/test/pollCallbacks.test.ts +226 -0
- package/dist/{chunk-S7OKGLFD.js → chunk-YDOK4BDL.js} +253 -147
- package/dist/{clientTypes-BSbwairE.d.cts → clientTypes-BJbFeeno.d.cts} +5 -0
- package/dist/{clientTypes-DDmcgZ0a.d.ts → clientTypes-CEno_BEf.d.ts} +5 -0
- package/dist/core.cjs +253 -147
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/node.cjs +208 -102
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/web.cjs +226 -109
- package/dist/web.d.cts +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +19 -7
- package/dist/zkProve.worker.cjs +69 -64
- package/dist/zkProve.worker.js +69 -64
- package/package.json +2 -2
- package/web/index.ts +22 -7
- package/web/test/tfheinit.web.test.ts +3 -3
- package/web/zkProve.worker.ts +93 -84
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @cofhe/sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2fbb918: Retry transient submit-time `404 Not Found` responses in the threshold-network decryption flows used by `decryptForTx` and `decryptForView`.
|
|
8
|
+
|
|
9
|
+
This adds a configurable `.set404RetryTimeout(timeoutMs)` builder option, defaults the retry window to 10 seconds, and keeps `decrypt` and `sealoutput` submit retry behavior aligned.
|
|
10
|
+
|
|
11
|
+
Update the decryption docs to explain submit-time `404` retries, empty `requestId` values during request creation, and when to increase the retry timeout for slower backends.
|
|
12
|
+
|
|
13
|
+
## 0.5.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 342fd0f: Fix SSR compatibility (`@cofhe/sdk/web` no longer crashes Next.js builds with `self is not defined`) by lazy-loading `tfhe`. Align `@cofhe/mock-contracts` with `@fhenixprotocol/cofhe-contracts@^0.1.3` (updated `TestBed.sol` to use current decrypt API, added missing `ITaskManager` batch methods to `MockTaskManager.sol`).
|
|
18
|
+
|
|
3
19
|
## 0.5.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -5,7 +5,7 @@ import { createMockEIP1193Provider } from '../test-utils.js';
|
|
|
5
5
|
import * as ethers5 from 'ethers5';
|
|
6
6
|
|
|
7
7
|
describe('Ethers5Adapter', () => {
|
|
8
|
-
const testRpcUrl = 'https://ethereum-sepolia
|
|
8
|
+
const testRpcUrl = 'https://ethereum-sepolia-rpc.publicnode.com';
|
|
9
9
|
const SEPOLIA_CHAIN_ID = 11155111;
|
|
10
10
|
let provider: ethers5.providers.JsonRpcProvider;
|
|
11
11
|
let wallet: ethers5.Wallet;
|
|
@@ -5,7 +5,7 @@ import { createMockEIP1193Provider } from '../test-utils.js';
|
|
|
5
5
|
import * as ethers6 from 'ethers6';
|
|
6
6
|
|
|
7
7
|
describe('Ethers6Adapter', () => {
|
|
8
|
-
const testRpcUrl = 'https://ethereum-sepolia
|
|
8
|
+
const testRpcUrl = 'https://ethereum-sepolia-rpc.publicnode.com';
|
|
9
9
|
const SEPOLIA_CHAIN_ID = 11155111;
|
|
10
10
|
let provider: ethers6.JsonRpcProvider;
|
|
11
11
|
let wallet: ethers6.Wallet;
|
|
@@ -4,7 +4,7 @@ import { privateKeyToAccount } from 'viem/accounts';
|
|
|
4
4
|
import { WagmiAdapter } from '../wagmi.js';
|
|
5
5
|
|
|
6
6
|
describe('WagmiAdapter', () => {
|
|
7
|
-
const testRpcUrl = 'https://ethereum-sepolia
|
|
7
|
+
const testRpcUrl = 'https://ethereum-sepolia-rpc.publicnode.com';
|
|
8
8
|
const SEPOLIA_CHAIN_ID = 11155111;
|
|
9
9
|
let account: ReturnType<typeof privateKeyToAccount>;
|
|
10
10
|
let publicClient: PublicClient;
|
|
@@ -12,6 +12,8 @@ import { getPublicClientChainID, sleep } from '../utils';
|
|
|
12
12
|
import { type DecryptPollCallbackFunction } from '../types';
|
|
13
13
|
import { tnDecryptV2 } from './tnDecryptV2';
|
|
14
14
|
|
|
15
|
+
const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* API
|
|
17
19
|
*
|
|
@@ -62,6 +64,7 @@ export class DecryptForTxBuilder extends BaseBuilder {
|
|
|
62
64
|
private permit?: Permit;
|
|
63
65
|
private permitSelection: DecryptForTxPermitSelection = 'unset';
|
|
64
66
|
private pollCallback?: DecryptPollCallbackFunction;
|
|
67
|
+
private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
|
|
65
68
|
|
|
66
69
|
constructor(params: DecryptForTxBuilderParams) {
|
|
67
70
|
super({
|
|
@@ -133,6 +136,23 @@ export class DecryptForTxBuilder extends BaseBuilder {
|
|
|
133
136
|
return this;
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
set404RetryTimeout(this: DecryptForTxBuilderUnset, timeoutMs: number): DecryptForTxBuilderUnset;
|
|
140
|
+
set404RetryTimeout(this: DecryptForTxBuilderSelected, timeoutMs: number): DecryptForTxBuilderSelected;
|
|
141
|
+
set404RetryTimeout(timeoutMs: number): DecryptForTxBuilder {
|
|
142
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
143
|
+
throw new CofheError({
|
|
144
|
+
code: CofheErrorCode.InternalError,
|
|
145
|
+
message: 'decryptForTx: set404RetryTimeout(timeoutMs) expects a finite number greater than or equal to 0.',
|
|
146
|
+
context: {
|
|
147
|
+
timeoutMs,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.retry404TimeoutMs = timeoutMs;
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
/**
|
|
137
157
|
* Select "use permit" mode.
|
|
138
158
|
*
|
|
@@ -305,6 +325,7 @@ export class DecryptForTxBuilder extends BaseBuilder {
|
|
|
305
325
|
chainId: this.chainId,
|
|
306
326
|
permission,
|
|
307
327
|
thresholdNetworkUrl,
|
|
328
|
+
retry404TimeoutMs: this.retry404TimeoutMs,
|
|
308
329
|
onPoll: this.pollCallback,
|
|
309
330
|
});
|
|
310
331
|
|
|
@@ -14,6 +14,8 @@ import { tnSealOutputV2 } from './tnSealOutputV2.js';
|
|
|
14
14
|
import { sleep } from '../utils.js';
|
|
15
15
|
import { type DecryptPollCallbackFunction } from '../types.js';
|
|
16
16
|
|
|
17
|
+
const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* API
|
|
19
21
|
*
|
|
@@ -48,6 +50,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
|
|
|
48
50
|
private permitHash?: string;
|
|
49
51
|
private permit?: Permit;
|
|
50
52
|
private pollCallback?: DecryptPollCallbackFunction;
|
|
53
|
+
private retry404TimeoutMs = DEFAULT_404_RETRY_TIMEOUT_MS;
|
|
51
54
|
|
|
52
55
|
constructor(params: DecryptForViewBuilderParams<U>) {
|
|
53
56
|
super({
|
|
@@ -116,6 +119,21 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
|
|
|
116
119
|
return this;
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
set404RetryTimeout(timeoutMs: number): DecryptForViewBuilder<U> {
|
|
123
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
124
|
+
throw new CofheError({
|
|
125
|
+
code: CofheErrorCode.InternalError,
|
|
126
|
+
message: 'decryptForView: set404RetryTimeout(timeoutMs) expects a finite number greater than or equal to 0.',
|
|
127
|
+
context: {
|
|
128
|
+
timeoutMs,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.retry404TimeoutMs = timeoutMs;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
/**
|
|
120
138
|
* Select "use permit" mode (optional).
|
|
121
139
|
*
|
|
@@ -276,6 +294,7 @@ export class DecryptForViewBuilder<U extends FheTypes> extends BaseBuilder {
|
|
|
276
294
|
chainId: this.chainId,
|
|
277
295
|
permission,
|
|
278
296
|
thresholdNetworkUrl,
|
|
297
|
+
retry404TimeoutMs: this.retry404TimeoutMs,
|
|
279
298
|
onPoll: this.pollCallback,
|
|
280
299
|
});
|
|
281
300
|
return PermitUtils.unseal(permit, sealed);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CofheError, CofheErrorCode } from '../error.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_404_RETRY_TIMEOUT_MS = 10_000;
|
|
4
|
+
|
|
5
|
+
export type SubmitRetryableStatus = 204 | 404;
|
|
6
|
+
|
|
7
|
+
export type SubmitResponseClassification =
|
|
8
|
+
| { kind: 'retryable'; status: SubmitRetryableStatus }
|
|
9
|
+
| { kind: 'parse-json' }
|
|
10
|
+
| { kind: 'fatal-http'; errorMessage: string };
|
|
11
|
+
|
|
12
|
+
export function isRetryableSubmitStatus(status: number): status is SubmitRetryableStatus {
|
|
13
|
+
return status === 204 || status === 404;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalize404RetryTimeoutMs(params: {
|
|
17
|
+
timeoutMs: number | undefined;
|
|
18
|
+
operationLabel: string;
|
|
19
|
+
errorCode: CofheErrorCode;
|
|
20
|
+
}): number {
|
|
21
|
+
const { timeoutMs, operationLabel, errorCode } = params;
|
|
22
|
+
|
|
23
|
+
if (timeoutMs === undefined) return DEFAULT_404_RETRY_TIMEOUT_MS;
|
|
24
|
+
|
|
25
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
26
|
+
throw new CofheError({
|
|
27
|
+
code: errorCode,
|
|
28
|
+
message: `${operationLabel} submit 404 retry timeout must be a finite number greater than or equal to 0`,
|
|
29
|
+
context: {
|
|
30
|
+
timeoutMs,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return timeoutMs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function classifySubmitResponse(params: {
|
|
39
|
+
response: Response;
|
|
40
|
+
extractErrorMessage?: (errorBody: unknown) => string | undefined;
|
|
41
|
+
}): Promise<SubmitResponseClassification> {
|
|
42
|
+
const { response, extractErrorMessage } = params;
|
|
43
|
+
|
|
44
|
+
if (isRetryableSubmitStatus(response.status)) {
|
|
45
|
+
return { kind: 'retryable', status: response.status };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
return { kind: 'parse-json' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
53
|
+
try {
|
|
54
|
+
const errorBody = (await response.json()) as unknown;
|
|
55
|
+
const maybeErrorMessage = extractErrorMessage?.(errorBody);
|
|
56
|
+
if (typeof maybeErrorMessage === 'string' && maybeErrorMessage.length > 0) {
|
|
57
|
+
errorMessage = maybeErrorMessage;
|
|
58
|
+
} else if (errorBody && typeof errorBody === 'object') {
|
|
59
|
+
const defaultMessage = (errorBody as Record<string, unknown>).error_message;
|
|
60
|
+
const fallbackMessage = (errorBody as Record<string, unknown>).message;
|
|
61
|
+
if (typeof defaultMessage === 'string' && defaultMessage.length > 0) {
|
|
62
|
+
errorMessage = defaultMessage;
|
|
63
|
+
} else if (typeof fallbackMessage === 'string' && fallbackMessage.length > 0) {
|
|
64
|
+
errorMessage = fallbackMessage;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
errorMessage = response.statusText || errorMessage;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { kind: 'fatal-http', errorMessage };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function throwIfSubmitRetryTimedOut(params: {
|
|
75
|
+
operationLabel: string;
|
|
76
|
+
errorCode: CofheErrorCode;
|
|
77
|
+
status: SubmitRetryableStatus;
|
|
78
|
+
elapsedMs: number;
|
|
79
|
+
retry404TimeoutMs: number;
|
|
80
|
+
overallTimeoutMs: number;
|
|
81
|
+
thresholdNetworkUrl: string;
|
|
82
|
+
body: unknown;
|
|
83
|
+
attemptIndex: number;
|
|
84
|
+
}): void {
|
|
85
|
+
const {
|
|
86
|
+
operationLabel,
|
|
87
|
+
errorCode,
|
|
88
|
+
status,
|
|
89
|
+
elapsedMs,
|
|
90
|
+
retry404TimeoutMs,
|
|
91
|
+
overallTimeoutMs,
|
|
92
|
+
thresholdNetworkUrl,
|
|
93
|
+
body,
|
|
94
|
+
attemptIndex,
|
|
95
|
+
} = params;
|
|
96
|
+
|
|
97
|
+
if (status === 404 && elapsedMs > retry404TimeoutMs) {
|
|
98
|
+
throw new CofheError({
|
|
99
|
+
code: errorCode,
|
|
100
|
+
message: `${operationLabel} submit retried 404 responses without receiving request_id for ${retry404TimeoutMs}ms`,
|
|
101
|
+
hint: 'The ciphertext may not be indexed yet. Increase set404RetryTimeout(...) if the backend is slow to index ciphertexts.',
|
|
102
|
+
context: {
|
|
103
|
+
thresholdNetworkUrl,
|
|
104
|
+
body,
|
|
105
|
+
attemptIndex,
|
|
106
|
+
timeoutMs: retry404TimeoutMs,
|
|
107
|
+
status,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (elapsedMs > overallTimeoutMs) {
|
|
113
|
+
throw new CofheError({
|
|
114
|
+
code: errorCode,
|
|
115
|
+
message: `${operationLabel} submit retried without receiving request_id for ${overallTimeoutMs}ms`,
|
|
116
|
+
hint: 'The ciphertext may still be propagating. Try again later.',
|
|
117
|
+
context: {
|
|
118
|
+
thresholdNetworkUrl,
|
|
119
|
+
body,
|
|
120
|
+
attemptIndex,
|
|
121
|
+
timeoutMs: overallTimeoutMs,
|
|
122
|
+
status,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -4,6 +4,7 @@ import { CofheError, CofheErrorCode } from '../error';
|
|
|
4
4
|
import { type DecryptPollCallbackFunction } from '../types';
|
|
5
5
|
import { normalizeTnSignature, parseDecryptedBytesToBigInt } from './tnDecryptUtils';
|
|
6
6
|
import { computeMinuteRampPollIntervalMs } from './polling.js';
|
|
7
|
+
import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
|
|
7
8
|
|
|
8
9
|
// Polling configuration
|
|
9
10
|
const POLL_INTERVAL_MS = 1000; // 1 second
|
|
@@ -178,6 +179,7 @@ async function submitDecryptRequestV2(
|
|
|
178
179
|
chainId: number,
|
|
179
180
|
permission: Permission | null,
|
|
180
181
|
overallStartTime: number,
|
|
182
|
+
retry404TimeoutMs: number,
|
|
181
183
|
onPoll?: DecryptPollCallbackFunction
|
|
182
184
|
): Promise<DecryptSubmitResultV2> {
|
|
183
185
|
const body: {
|
|
@@ -219,19 +221,12 @@ async function submitDecryptRequestV2(
|
|
|
219
221
|
});
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
let errorMessage = `HTTP ${response.status}`;
|
|
224
|
-
try {
|
|
225
|
-
const errorBody = (await response.json()) as Record<string, unknown>;
|
|
226
|
-
const maybeMessage = (errorBody.error_message || errorBody.message) as unknown;
|
|
227
|
-
if (typeof maybeMessage === 'string' && maybeMessage.length > 0) errorMessage = maybeMessage;
|
|
228
|
-
} catch {
|
|
229
|
-
errorMessage = response.statusText || errorMessage;
|
|
230
|
-
}
|
|
224
|
+
const responseClassification = await classifySubmitResponse({ response });
|
|
231
225
|
|
|
226
|
+
if (responseClassification.kind === 'fatal-http') {
|
|
232
227
|
throw new CofheError({
|
|
233
228
|
code: CofheErrorCode.DecryptFailed,
|
|
234
|
-
message: `decrypt request failed: ${errorMessage}`,
|
|
229
|
+
message: `decrypt request failed: ${responseClassification.errorMessage}`,
|
|
235
230
|
hint: 'Check the threshold network URL and request parameters.',
|
|
236
231
|
context: {
|
|
237
232
|
thresholdNetworkUrl,
|
|
@@ -243,8 +238,8 @@ async function submitDecryptRequestV2(
|
|
|
243
238
|
});
|
|
244
239
|
}
|
|
245
240
|
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
if (responseClassification.kind === 'parse-json') {
|
|
242
|
+
let submitResponse: DecryptSubmitResponseV2;
|
|
248
243
|
let rawJson: unknown;
|
|
249
244
|
try {
|
|
250
245
|
rawJson = (await response.json()) as unknown;
|
|
@@ -277,51 +272,44 @@ async function submitDecryptRequestV2(
|
|
|
277
272
|
if (submitResponse.request_id) {
|
|
278
273
|
return { kind: 'request_id', requestId: submitResponse.request_id };
|
|
279
274
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 204 means backend is aware of ct hash but didn't calculate it yet
|
|
283
|
-
if (response.status === 204) {
|
|
284
|
-
const elapsedMs = Date.now() - overallStartTime;
|
|
285
|
-
if (elapsedMs > DECRYPT_TIMEOUT_MS) {
|
|
286
|
-
throw new CofheError({
|
|
287
|
-
code: CofheErrorCode.DecryptFailed,
|
|
288
|
-
message: `decrypt submit retried without receiving request_id for ${DECRYPT_TIMEOUT_MS}ms`,
|
|
289
|
-
hint: 'The ciphertext may still be propagating. Try again later.',
|
|
290
|
-
context: {
|
|
291
|
-
thresholdNetworkUrl,
|
|
292
|
-
body,
|
|
293
|
-
attemptIndex,
|
|
294
|
-
timeoutMs: DECRYPT_TIMEOUT_MS,
|
|
295
|
-
submitResponse,
|
|
296
|
-
status: response.status,
|
|
297
|
-
},
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
275
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
276
|
+
throw new CofheError({
|
|
277
|
+
code: CofheErrorCode.DecryptFailed,
|
|
278
|
+
message: `decrypt submit response missing request_id`,
|
|
279
|
+
context: {
|
|
280
|
+
thresholdNetworkUrl,
|
|
281
|
+
body,
|
|
282
|
+
submitResponse,
|
|
283
|
+
attemptIndex,
|
|
284
|
+
},
|
|
308
285
|
});
|
|
309
|
-
|
|
310
|
-
await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
|
|
311
|
-
attemptIndex += 1;
|
|
312
|
-
continue;
|
|
313
286
|
}
|
|
314
287
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
288
|
+
const elapsedMs = Date.now() - overallStartTime;
|
|
289
|
+
|
|
290
|
+
throwIfSubmitRetryTimedOut({
|
|
291
|
+
operationLabel: 'decrypt',
|
|
292
|
+
errorCode: CofheErrorCode.DecryptFailed,
|
|
293
|
+
status: responseClassification.status,
|
|
294
|
+
elapsedMs,
|
|
295
|
+
retry404TimeoutMs,
|
|
296
|
+
overallTimeoutMs: DECRYPT_TIMEOUT_MS,
|
|
297
|
+
thresholdNetworkUrl,
|
|
298
|
+
body,
|
|
299
|
+
attemptIndex,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
onPoll?.({
|
|
303
|
+
operation: 'decrypt',
|
|
304
|
+
requestId: '',
|
|
305
|
+
attemptIndex,
|
|
306
|
+
elapsedMs,
|
|
307
|
+
intervalMs: SUBMIT_RETRY_INTERVAL_MS,
|
|
308
|
+
timeoutMs: DECRYPT_TIMEOUT_MS,
|
|
324
309
|
});
|
|
310
|
+
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
|
|
312
|
+
attemptIndex += 1;
|
|
325
313
|
}
|
|
326
314
|
}
|
|
327
315
|
|
|
@@ -462,9 +450,15 @@ export async function tnDecryptV2(params: {
|
|
|
462
450
|
chainId: number;
|
|
463
451
|
permission: Permission | null;
|
|
464
452
|
thresholdNetworkUrl: string;
|
|
453
|
+
retry404TimeoutMs?: number;
|
|
465
454
|
onPoll?: DecryptPollCallbackFunction;
|
|
466
455
|
}): Promise<{ decryptedValue: bigint; signature: `0x${string}` }> {
|
|
467
|
-
const { thresholdNetworkUrl, ctHash, chainId, permission, onPoll } = params;
|
|
456
|
+
const { thresholdNetworkUrl, ctHash, chainId, permission, retry404TimeoutMs, onPoll } = params;
|
|
457
|
+
const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
|
|
458
|
+
timeoutMs: retry404TimeoutMs,
|
|
459
|
+
operationLabel: 'decrypt',
|
|
460
|
+
errorCode: CofheErrorCode.DecryptFailed,
|
|
461
|
+
});
|
|
468
462
|
const overallStartTime = Date.now();
|
|
469
463
|
const submitResult = await submitDecryptRequestV2(
|
|
470
464
|
thresholdNetworkUrl,
|
|
@@ -472,6 +466,7 @@ export async function tnDecryptV2(params: {
|
|
|
472
466
|
chainId,
|
|
473
467
|
permission,
|
|
474
468
|
overallStartTime,
|
|
469
|
+
normalized404RetryTimeoutMs,
|
|
475
470
|
onPoll
|
|
476
471
|
);
|
|
477
472
|
|
|
@@ -3,6 +3,7 @@ import { type Permission, type EthEncryptedData } from '@/permits';
|
|
|
3
3
|
import { CofheError, CofheErrorCode } from '../error.js';
|
|
4
4
|
import { type DecryptPollCallbackFunction } from '../types.js';
|
|
5
5
|
import { computeMinuteRampPollIntervalMs } from './polling.js';
|
|
6
|
+
import { classifySubmitResponse, normalize404RetryTimeoutMs, throwIfSubmitRetryTimedOut } from './submitRetry.js';
|
|
6
7
|
|
|
7
8
|
// Polling configuration
|
|
8
9
|
const POLL_INTERVAL_MS = 1000; // 1 second
|
|
@@ -136,6 +137,7 @@ async function submitSealOutputRequest(
|
|
|
136
137
|
chainId: number,
|
|
137
138
|
permission: Permission,
|
|
138
139
|
overallStartTime: number,
|
|
140
|
+
retry404TimeoutMs: number,
|
|
139
141
|
onPoll?: DecryptPollCallbackFunction
|
|
140
142
|
): Promise<SealOutputSubmitResult> {
|
|
141
143
|
const body = {
|
|
@@ -169,20 +171,12 @@ async function submitSealOutputRequest(
|
|
|
169
171
|
});
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
let errorMessage = `HTTP ${response.status}`;
|
|
175
|
-
try {
|
|
176
|
-
const errorBody = await response.json();
|
|
177
|
-
|
|
178
|
-
errorMessage = errorBody.error_message || errorBody.message || errorMessage;
|
|
179
|
-
} catch {
|
|
180
|
-
errorMessage = response.statusText || errorMessage;
|
|
181
|
-
}
|
|
174
|
+
const responseClassification = await classifySubmitResponse({ response });
|
|
182
175
|
|
|
176
|
+
if (responseClassification.kind === 'fatal-http') {
|
|
183
177
|
throw new CofheError({
|
|
184
178
|
code: CofheErrorCode.SealOutputFailed,
|
|
185
|
-
message: `sealOutput request failed: ${errorMessage}`,
|
|
179
|
+
message: `sealOutput request failed: ${responseClassification.errorMessage}`,
|
|
186
180
|
hint: 'Check the threshold network URL and request parameters.',
|
|
187
181
|
context: {
|
|
188
182
|
thresholdNetworkUrl,
|
|
@@ -194,8 +188,8 @@ async function submitSealOutputRequest(
|
|
|
194
188
|
});
|
|
195
189
|
}
|
|
196
190
|
|
|
197
|
-
|
|
198
|
-
|
|
191
|
+
if (responseClassification.kind === 'parse-json') {
|
|
192
|
+
let submitResponse: SealOutputSubmitResponse;
|
|
199
193
|
try {
|
|
200
194
|
submitResponse = (await response.json()) as SealOutputSubmitResponse;
|
|
201
195
|
} catch (e) {
|
|
@@ -225,51 +219,44 @@ async function submitSealOutputRequest(
|
|
|
225
219
|
if (submitResponse.request_id) {
|
|
226
220
|
return { kind: 'request_id', requestId: submitResponse.request_id };
|
|
227
221
|
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 204 means backend is aware of ct hash but didn't calculate it yet
|
|
231
|
-
if (response.status === 204) {
|
|
232
|
-
const elapsedMs = Date.now() - overallStartTime;
|
|
233
|
-
if (elapsedMs > SEAL_OUTPUT_TIMEOUT_MS) {
|
|
234
|
-
throw new CofheError({
|
|
235
|
-
code: CofheErrorCode.SealOutputFailed,
|
|
236
|
-
message: `sealOutput submit retried without receiving request_id for ${SEAL_OUTPUT_TIMEOUT_MS}ms`,
|
|
237
|
-
hint: 'The ciphertext may still be propagating. Try again later.',
|
|
238
|
-
context: {
|
|
239
|
-
thresholdNetworkUrl,
|
|
240
|
-
body,
|
|
241
|
-
attemptIndex,
|
|
242
|
-
timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
243
|
-
submitResponse,
|
|
244
|
-
status: response.status,
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
222
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
223
|
+
throw new CofheError({
|
|
224
|
+
code: CofheErrorCode.SealOutputFailed,
|
|
225
|
+
message: `sealOutput submit response missing request_id`,
|
|
226
|
+
context: {
|
|
227
|
+
thresholdNetworkUrl,
|
|
228
|
+
body,
|
|
229
|
+
submitResponse,
|
|
230
|
+
attemptIndex,
|
|
231
|
+
},
|
|
256
232
|
});
|
|
257
|
-
|
|
258
|
-
await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
|
|
259
|
-
attemptIndex += 1;
|
|
260
|
-
continue;
|
|
261
233
|
}
|
|
262
234
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
235
|
+
const elapsedMs = Date.now() - overallStartTime;
|
|
236
|
+
|
|
237
|
+
throwIfSubmitRetryTimedOut({
|
|
238
|
+
operationLabel: 'sealOutput',
|
|
239
|
+
errorCode: CofheErrorCode.SealOutputFailed,
|
|
240
|
+
status: responseClassification.status,
|
|
241
|
+
elapsedMs,
|
|
242
|
+
retry404TimeoutMs,
|
|
243
|
+
overallTimeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
244
|
+
thresholdNetworkUrl,
|
|
245
|
+
body,
|
|
246
|
+
attemptIndex,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
onPoll?.({
|
|
250
|
+
operation: 'sealoutput',
|
|
251
|
+
requestId: '',
|
|
252
|
+
attemptIndex,
|
|
253
|
+
elapsedMs,
|
|
254
|
+
intervalMs: SUBMIT_RETRY_INTERVAL_MS,
|
|
255
|
+
timeoutMs: SEAL_OUTPUT_TIMEOUT_MS,
|
|
272
256
|
});
|
|
257
|
+
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, SUBMIT_RETRY_INTERVAL_MS));
|
|
259
|
+
attemptIndex += 1;
|
|
273
260
|
}
|
|
274
261
|
}
|
|
275
262
|
|
|
@@ -415,9 +402,15 @@ export async function tnSealOutputV2(params: {
|
|
|
415
402
|
chainId: number;
|
|
416
403
|
permission: Permission;
|
|
417
404
|
thresholdNetworkUrl: string;
|
|
405
|
+
retry404TimeoutMs?: number;
|
|
418
406
|
onPoll?: DecryptPollCallbackFunction;
|
|
419
407
|
}): Promise<EthEncryptedData> {
|
|
420
|
-
const { thresholdNetworkUrl, ctHash, chainId, permission, onPoll } = params;
|
|
408
|
+
const { thresholdNetworkUrl, ctHash, chainId, permission, retry404TimeoutMs, onPoll } = params;
|
|
409
|
+
const normalized404RetryTimeoutMs = normalize404RetryTimeoutMs({
|
|
410
|
+
timeoutMs: retry404TimeoutMs,
|
|
411
|
+
operationLabel: 'sealOutput',
|
|
412
|
+
errorCode: CofheErrorCode.SealOutputFailed,
|
|
413
|
+
});
|
|
421
414
|
const overallStartTime = Date.now();
|
|
422
415
|
|
|
423
416
|
// Step 1: Submit the request and get request_id
|
|
@@ -427,6 +420,7 @@ export async function tnSealOutputV2(params: {
|
|
|
427
420
|
chainId,
|
|
428
421
|
permission,
|
|
429
422
|
overallStartTime,
|
|
423
|
+
normalized404RetryTimeoutMs,
|
|
430
424
|
onPoll
|
|
431
425
|
);
|
|
432
426
|
|
|
@@ -197,6 +197,20 @@ describe('DecryptForTxBuilder', () => {
|
|
|
197
197
|
expect(selected.getChainId()).toBe(99);
|
|
198
198
|
expect(selected.getAccount()).toBe('0xabc');
|
|
199
199
|
});
|
|
200
|
+
|
|
201
|
+
it('should allow configuring 404 retry timeout', () => {
|
|
202
|
+
const builder = createTxBuilder();
|
|
203
|
+
const result = builder.set404RetryTimeout(15_000);
|
|
204
|
+
|
|
205
|
+
expect(result).toBe(builder);
|
|
206
|
+
expect((builder as any).retry404TimeoutMs).toBe(15_000);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should throw for invalid 404 retry timeout', () => {
|
|
210
|
+
const builder = createTxBuilder();
|
|
211
|
+
|
|
212
|
+
expect(() => builder.set404RetryTimeout(-1)).toThrow('set404RetryTimeout(timeoutMs) expects');
|
|
213
|
+
});
|
|
200
214
|
});
|
|
201
215
|
|
|
202
216
|
// --- execute error paths ---
|
|
@@ -326,6 +340,20 @@ describe('DecryptForViewBuilder', () => {
|
|
|
326
340
|
expect(result.getChainId()).toBe(TEST_CHAIN_ID);
|
|
327
341
|
expect(result.getAccount()).toBe(account.address);
|
|
328
342
|
});
|
|
343
|
+
|
|
344
|
+
it('should allow configuring 404 retry timeout', () => {
|
|
345
|
+
const builder = createViewBuilder(FheTypes.Uint32);
|
|
346
|
+
const result = builder.set404RetryTimeout(12_000);
|
|
347
|
+
|
|
348
|
+
expect(result).toBe(builder);
|
|
349
|
+
expect((builder as any).retry404TimeoutMs).toBe(12_000);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should throw for invalid 404 retry timeout', () => {
|
|
353
|
+
const builder = createViewBuilder(FheTypes.Uint32);
|
|
354
|
+
|
|
355
|
+
expect(() => builder.set404RetryTimeout(-1)).toThrow('set404RetryTimeout(timeoutMs) expects');
|
|
356
|
+
});
|
|
329
357
|
});
|
|
330
358
|
|
|
331
359
|
// --- execute error paths ---
|