@invisible-labs/sdk 0.6.0-devnet.1
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 +210 -0
- package/dist/chunk-7DSVN3MV.js +1145 -0
- package/dist/chunk-7DSVN3MV.js.map +1 -0
- package/dist/chunk-JNNVPZEU.js +599 -0
- package/dist/chunk-JNNVPZEU.js.map +1 -0
- package/dist/chunk-JRMKCE3S.js +50 -0
- package/dist/chunk-JRMKCE3S.js.map +1 -0
- package/dist/chunk-LBC7ALYO.js +3783 -0
- package/dist/chunk-LBC7ALYO.js.map +1 -0
- package/dist/chunk-NCY4FCYU.js +1911 -0
- package/dist/chunk-NCY4FCYU.js.map +1 -0
- package/dist/chunk-OHIM2YWU.js +126 -0
- package/dist/chunk-OHIM2YWU.js.map +1 -0
- package/dist/chunk-SZAYO2L5.js +123 -0
- package/dist/chunk-SZAYO2L5.js.map +1 -0
- package/dist/chunk-TQNNTV5F.js +17 -0
- package/dist/chunk-TQNNTV5F.js.map +1 -0
- package/dist/chunk-VR6T6OJS.js +24 -0
- package/dist/chunk-VR6T6OJS.js.map +1 -0
- package/dist/chunk-XUWBGET5.js +221 -0
- package/dist/chunk-XUWBGET5.js.map +1 -0
- package/dist/coordinator-7Y45MCCZ.js +4 -0
- package/dist/coordinator-7Y45MCCZ.js.map +1 -0
- package/dist/createSession-D3ym-Ira.d.ts +177 -0
- package/dist/dkgWorker.d.ts +2 -0
- package/dist/dkgWorker.js +61 -0
- package/dist/dkgWorker.js.map +1 -0
- package/dist/events.d.ts +54 -0
- package/dist/events.js +143 -0
- package/dist/events.js.map +1 -0
- package/dist/frostRuntime-JESDHN6O.js +4 -0
- package/dist/frostRuntime-JESDHN6O.js.map +1 -0
- package/dist/index.d.ts +499 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/lp.d.ts +388 -0
- package/dist/lp.js +1711 -0
- package/dist/lp.js.map +1 -0
- package/dist/presets.d.ts +43 -0
- package/dist/presets.js +70 -0
- package/dist/presets.js.map +1 -0
- package/dist/session-DFuy54C-.d.ts +31 -0
- package/dist/stats.d.ts +47 -0
- package/dist/stats.js +21 -0
- package/dist/stats.js.map +1 -0
- package/dist/storage.d.ts +82 -0
- package/dist/storage.js +246 -0
- package/dist/storage.js.map +1 -0
- package/dist/types-ZhV7TIQY.d.ts +76 -0
- package/dist/types.generated-CHGSbmLp.d.ts +67 -0
- package/dist/user.d.ts +377 -0
- package/dist/user.js +1225 -0
- package/dist/user.js.map +1 -0
- package/package.json +82 -0
package/dist/user.js
ADDED
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
import { scheduledPayoutWindowSeconds } from './chunk-LBC7ALYO.js';
|
|
2
|
+
import { createSession, createExponentialBackoff, validatePayoutPolicyShape, formatAjvErrors } from './chunk-7DSVN3MV.js';
|
|
3
|
+
import { PRODUCT_MAX_ENTRY_LAMPORTS, PRODUCT_MIN_ENTRY_LAMPORTS } from './chunk-TQNNTV5F.js';
|
|
4
|
+
import { solanaAddressFromPublicKeyBytes, publicKey, assertAttested } from './chunk-SZAYO2L5.js';
|
|
5
|
+
import { CoordinatorMessageType, onEnvelope, closeProtocolRuntime, EnvelopeKind, requestEnvelope } from './chunk-XUWBGET5.js';
|
|
6
|
+
import { getSessionState } from './chunk-JRMKCE3S.js';
|
|
7
|
+
import './chunk-NCY4FCYU.js';
|
|
8
|
+
import { TransportError, CommandError, InvalidDepositAmountError, normalizeError, PolicyValidationError, NotImplementedError, RoutingError, AttestationError } from './chunk-OHIM2YWU.js';
|
|
9
|
+
export { InvalidDepositAmountError } from './chunk-OHIM2YWU.js';
|
|
10
|
+
import './chunk-VR6T6OJS.js';
|
|
11
|
+
|
|
12
|
+
// src/validation/payoutPolicy.ts
|
|
13
|
+
var PAYOUT_WINDOW_MILLISECONDS_PER_SECOND = 1e3;
|
|
14
|
+
var INSTANT_PAYOUT_WINDOW_MS = 0;
|
|
15
|
+
var SCHEDULED_PAYOUT_WINDOW_DURATIONS_MS = Object.freeze(
|
|
16
|
+
scheduledPayoutWindowSeconds().map(
|
|
17
|
+
(windowSeconds) => windowSeconds * PAYOUT_WINDOW_MILLISECONDS_PER_SECOND
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
var SUPPORTED_TOTAL_DEADLINE_MS = Object.freeze([
|
|
21
|
+
INSTANT_PAYOUT_WINDOW_MS,
|
|
22
|
+
...SCHEDULED_PAYOUT_WINDOW_DURATIONS_MS
|
|
23
|
+
]);
|
|
24
|
+
var SUPPORTED_TOTAL_DEADLINE_MS_SET = new Set(SUPPORTED_TOTAL_DEADLINE_MS);
|
|
25
|
+
Math.max(...SUPPORTED_TOTAL_DEADLINE_MS);
|
|
26
|
+
var SHARE_SUM_TOLERANCE = 0.01;
|
|
27
|
+
var CURRENT_USER_PAYOUT_DESTINATION_LIMIT = 1;
|
|
28
|
+
var CURRENT_USER_PAYOUT_DESTINATION_LIMIT_MESSAGE = "current V0 normal-user SDK and coordinator support exactly one payout destination; multi-address receive is not supported yet";
|
|
29
|
+
var CURRENT_USER_BATCH_EXCLUSION_ID_UNSUPPORTED_MESSAGE = "batchExclusionId is not supported in V0 normal-user payout requests; V1 defines it as campaign-scoped batch exclusion, never a stable actor identity";
|
|
30
|
+
var CURRENT_USER_MIN_DELAY_UNSUPPORTED_MESSAGE = "destinations[*].minDelayMs is not supported in V0 normal-user payout requests";
|
|
31
|
+
var CURRENT_USER_MAX_DELAY_UNSUPPORTED_MESSAGE = "destinations[*].maxDelayMs is not supported in V0 normal-user payout requests";
|
|
32
|
+
var CURRENT_USER_PAYOUT_FRAGMENT_LIMIT = 1;
|
|
33
|
+
var CURRENT_USER_PAYOUT_FRAGMENT_LIMIT_MESSAGE = "current SDK and coordinator support exactly one scheduled payout fragment; multi-fragment receive is not supported yet";
|
|
34
|
+
function validatePayoutPolicy(input, ctx = {}) {
|
|
35
|
+
if (!validatePayoutPolicyShape(input)) {
|
|
36
|
+
throw new PolicyValidationError(
|
|
37
|
+
"INVALID_PAYOUT_PLAN",
|
|
38
|
+
`payout policy shape invalid: ${formatAjvErrors(validatePayoutPolicyShape.errors)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const policy = input;
|
|
42
|
+
const { destinations } = policy;
|
|
43
|
+
assertCurrentUserUnsupportedPayoutFeatures(policy);
|
|
44
|
+
assertCurrentUserPayoutDestinationLimit(destinations.length);
|
|
45
|
+
for (const { address } of destinations) {
|
|
46
|
+
publicKey(address);
|
|
47
|
+
}
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const { address } of destinations) {
|
|
50
|
+
if (seen.has(address)) {
|
|
51
|
+
throw new PolicyValidationError(
|
|
52
|
+
"INVALID_PAYOUT_PLAN",
|
|
53
|
+
`duplicate payout destination: ${address}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
seen.add(address);
|
|
57
|
+
}
|
|
58
|
+
if (ctx.originAddress !== void 0 && seen.has(ctx.originAddress)) {
|
|
59
|
+
throw new PolicyValidationError(
|
|
60
|
+
"INVALID_PAYOUT_PLAN",
|
|
61
|
+
"a payout destination must not be the deposit origin"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const shareSum = destinations.reduce((acc, d) => acc + d.sharePercent, 0);
|
|
65
|
+
if (Math.abs(shareSum - 100) > SHARE_SUM_TOLERANCE) {
|
|
66
|
+
throw new PolicyValidationError(
|
|
67
|
+
"INVALID_PAYOUT_PLAN",
|
|
68
|
+
`destination shares must sum to 100, got ${shareSum}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
for (const d of destinations) {
|
|
72
|
+
if (d.minLamports !== void 0 && ctx.totalLamports !== void 0) {
|
|
73
|
+
const shareFloor = Math.floor(ctx.totalLamports * d.sharePercent / 100);
|
|
74
|
+
if (d.minLamports > shareFloor) {
|
|
75
|
+
throw new PolicyValidationError(
|
|
76
|
+
"INVALID_AMOUNT",
|
|
77
|
+
`minLamports (${d.minLamports}) exceeds the implied share floor (${shareFloor}) for ${d.address}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (policy.totalDeadlineMs !== void 0) {
|
|
83
|
+
if (!SUPPORTED_TOTAL_DEADLINE_MS_SET.has(policy.totalDeadlineMs)) {
|
|
84
|
+
throw new PolicyValidationError(
|
|
85
|
+
"INVALID_PAYOUT_PLAN",
|
|
86
|
+
`totalDeadlineMs (${policy.totalDeadlineMs}) must be one of [${SUPPORTED_TOTAL_DEADLINE_MS.join(", ")}] milliseconds; 0 selects instant payout`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return policy;
|
|
91
|
+
}
|
|
92
|
+
function assertCurrentUserUnsupportedPayoutFeatures(policy) {
|
|
93
|
+
if (policy.batchExclusionId !== void 0) {
|
|
94
|
+
throw new PolicyValidationError(
|
|
95
|
+
"INVALID_PAYOUT_PLAN",
|
|
96
|
+
CURRENT_USER_BATCH_EXCLUSION_ID_UNSUPPORTED_MESSAGE
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
for (const destination of policy.destinations) {
|
|
100
|
+
if (destination.minDelayMs !== void 0) {
|
|
101
|
+
throw new PolicyValidationError(
|
|
102
|
+
"INVALID_PAYOUT_PLAN",
|
|
103
|
+
CURRENT_USER_MIN_DELAY_UNSUPPORTED_MESSAGE
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (destination.maxDelayMs !== void 0) {
|
|
107
|
+
throw new PolicyValidationError(
|
|
108
|
+
"INVALID_PAYOUT_PLAN",
|
|
109
|
+
CURRENT_USER_MAX_DELAY_UNSUPPORTED_MESSAGE
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function assertCurrentUserPayoutDestinationLimit(destinationCount) {
|
|
115
|
+
if (destinationCount !== CURRENT_USER_PAYOUT_DESTINATION_LIMIT) {
|
|
116
|
+
throw new PolicyValidationError(
|
|
117
|
+
"INVALID_PAYOUT_PLAN",
|
|
118
|
+
`${CURRENT_USER_PAYOUT_DESTINATION_LIMIT_MESSAGE}; got ${destinationCount}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function assertCurrentUserPayoutFragmentLimit(fragmentCount) {
|
|
123
|
+
if (fragmentCount !== CURRENT_USER_PAYOUT_FRAGMENT_LIMIT) {
|
|
124
|
+
throw new PolicyValidationError(
|
|
125
|
+
"INVALID_PAYOUT_PLAN",
|
|
126
|
+
`${CURRENT_USER_PAYOUT_FRAGMENT_LIMIT_MESSAGE}; got ${fragmentCount}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/user/recoveryCode.ts
|
|
132
|
+
var RECOVERY_CODE_BYTE_LENGTH = 32;
|
|
133
|
+
function assertRecoveryCode(code) {
|
|
134
|
+
if (code.byteLength !== RECOVERY_CODE_BYTE_LENGTH) {
|
|
135
|
+
throw new PolicyValidationError(
|
|
136
|
+
"INVALID_WITHDRAWAL_PLAN",
|
|
137
|
+
`recoveryCode must be ${RECOVERY_CODE_BYTE_LENGTH} bytes`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function generateRecoveryCode() {
|
|
142
|
+
const code = crypto.getRandomValues(new Uint8Array(RECOVERY_CODE_BYTE_LENGTH));
|
|
143
|
+
const commitment = await commitmentFromCode(code);
|
|
144
|
+
return { code, commitment };
|
|
145
|
+
}
|
|
146
|
+
async function commitmentFromCode(code) {
|
|
147
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", new Uint8Array(code));
|
|
148
|
+
return new Uint8Array(hashBuffer);
|
|
149
|
+
}
|
|
150
|
+
function recoveryCodeToHex(code) {
|
|
151
|
+
return Array.from(code).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
152
|
+
}
|
|
153
|
+
function recoveryCodeFromHex(hex) {
|
|
154
|
+
const expectedHexLength = RECOVERY_CODE_BYTE_LENGTH * 2;
|
|
155
|
+
if (!new RegExp(`^[0-9a-fA-F]{${expectedHexLength}}$`).test(hex)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`recovery code must be exactly ${expectedHexLength} hex characters (${RECOVERY_CODE_BYTE_LENGTH} bytes)`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const bytes = new Uint8Array(RECOVERY_CODE_BYTE_LENGTH);
|
|
161
|
+
for (let i = 0; i < RECOVERY_CODE_BYTE_LENGTH; i += 1) {
|
|
162
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
163
|
+
}
|
|
164
|
+
return bytes;
|
|
165
|
+
}
|
|
166
|
+
function recoveryCodeFromUserInput(input) {
|
|
167
|
+
return recoveryCodeFromHex(input.replace(/\s+/g, ""));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/user/coordinatorSession.ts
|
|
171
|
+
var protocolPromise = null;
|
|
172
|
+
var frostRuntimePromise = null;
|
|
173
|
+
function loadProtocol() {
|
|
174
|
+
protocolPromise ??= import('./coordinator-7Y45MCCZ.js');
|
|
175
|
+
return protocolPromise;
|
|
176
|
+
}
|
|
177
|
+
function loadFrostRuntime() {
|
|
178
|
+
frostRuntimePromise ??= import('./frostRuntime-JESDHN6O.js');
|
|
179
|
+
return frostRuntimePromise;
|
|
180
|
+
}
|
|
181
|
+
function loadFrost() {
|
|
182
|
+
return loadFrostRuntime().then((runtime) => runtime.loadFrost());
|
|
183
|
+
}
|
|
184
|
+
var BENIGN_TRANSPORT_DROP = /* @__PURE__ */ Symbol("benignTransportDrop");
|
|
185
|
+
var ALICE_ID = 1;
|
|
186
|
+
var COORDINATOR_ID = 2;
|
|
187
|
+
var SYNC_SECRET_BYTE_LENGTH = 32;
|
|
188
|
+
var ACTIVE_CONTRACT_REQUEST_MESSAGE = "coordinator session already has an active contract";
|
|
189
|
+
var DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS = 5e3;
|
|
190
|
+
var MAX_SWAP_STATUS_RETRY_DELAY_MS = 3e4;
|
|
191
|
+
var SWAP_STATUS_RETRY_MULTIPLIER = 2;
|
|
192
|
+
var SWAP_STATUS_RETRY_JITTER_RATIO = 0.2;
|
|
193
|
+
var DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS = 5 * 60 * 1e3;
|
|
194
|
+
var REFUND_OUTCOME_POLL_INTERVAL_MS = 3e3;
|
|
195
|
+
var REFUND_OUTCOME_PER_POLL_TIMEOUT_MS = 5e3;
|
|
196
|
+
var REQUEST_TIMEOUT_MESSAGE = "coordinator request timed out";
|
|
197
|
+
var CURRENT_SCHEDULED_PAYOUT_WINDOW_SECONDS = new Set(scheduledPayoutWindowSeconds());
|
|
198
|
+
var CURRENT_SCHEDULED_PAYOUT_WINDOW_MESSAGE = `scheduled payout_spec.window_seconds must be one of [${scheduledPayoutWindowSeconds().join(", ")}]`;
|
|
199
|
+
var TERMINAL_SWAP_STATES = /* @__PURE__ */ new Set(["completed", "refunded", "failed"]);
|
|
200
|
+
var activeFunctionalContractRequests = /* @__PURE__ */ new WeakSet();
|
|
201
|
+
function createCoordinatorSession(options) {
|
|
202
|
+
return new UserCoordinatorSession(options);
|
|
203
|
+
}
|
|
204
|
+
function isBenignTransportDrop(err) {
|
|
205
|
+
return typeof err === "object" && err !== null && err[BENIGN_TRANSPORT_DROP] === true;
|
|
206
|
+
}
|
|
207
|
+
function readDkgComplete(protocol, envelope) {
|
|
208
|
+
const payload = protocol.asDkgCompletePayload(envelope);
|
|
209
|
+
return {
|
|
210
|
+
jointPublicKey: payload.joint_pubkey,
|
|
211
|
+
depositExpiresAtMs: payload.deposit_expires_at_ms,
|
|
212
|
+
coordinatorTimestampMs: envelope.timestamp_ms
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function tagBenignTransportDrop(err) {
|
|
216
|
+
err[BENIGN_TRANSPORT_DROP] = true;
|
|
217
|
+
return err;
|
|
218
|
+
}
|
|
219
|
+
function isBenignTransportDropCandidate(err) {
|
|
220
|
+
return err instanceof TransportError && err.code === "CONNECTION_LOST";
|
|
221
|
+
}
|
|
222
|
+
var UserCoordinatorSession = class {
|
|
223
|
+
constructor(options, closeSessionAfterRefund = true) {
|
|
224
|
+
this.protocol = null;
|
|
225
|
+
this.session = null;
|
|
226
|
+
this.unsubscribeEnvelope = null;
|
|
227
|
+
this.unsubscribeTransportState = null;
|
|
228
|
+
this.swapState = "new";
|
|
229
|
+
this.sessionStatus = "idle";
|
|
230
|
+
this.swapId = null;
|
|
231
|
+
this.syncSecret = null;
|
|
232
|
+
this.attested = false;
|
|
233
|
+
this.pushListeners = /* @__PURE__ */ new Map();
|
|
234
|
+
this.stateListeners = /* @__PURE__ */ new Set();
|
|
235
|
+
this.logListeners = /* @__PURE__ */ new Set();
|
|
236
|
+
this.payoutExecutedListeners = /* @__PURE__ */ new Set();
|
|
237
|
+
this.refundExecutedListeners = /* @__PURE__ */ new Set();
|
|
238
|
+
this.normalUserActorSyncListeners = /* @__PURE__ */ new Set();
|
|
239
|
+
this.coordinatorErrorListeners = /* @__PURE__ */ new Set();
|
|
240
|
+
this.emittedPayoutKeys = /* @__PURE__ */ new Set();
|
|
241
|
+
this.emittedRefundKeys = /* @__PURE__ */ new Set();
|
|
242
|
+
this.stopStatusReconciliation = null;
|
|
243
|
+
this.bufferDepositConfirmedUntilDelegateAck = false;
|
|
244
|
+
this.bufferedDepositConfirmed = null;
|
|
245
|
+
this.coordinator = options.coordinator;
|
|
246
|
+
this.createSessionImpl = options.createSession ?? createSession;
|
|
247
|
+
this.closeSessionAfterRefund = closeSessionAfterRefund;
|
|
248
|
+
}
|
|
249
|
+
onStateChange(listener) {
|
|
250
|
+
this.stateListeners.add(listener);
|
|
251
|
+
return () => this.stateListeners.delete(listener);
|
|
252
|
+
}
|
|
253
|
+
onLog(listener) {
|
|
254
|
+
this.logListeners.add(listener);
|
|
255
|
+
return () => this.logListeners.delete(listener);
|
|
256
|
+
}
|
|
257
|
+
onPayoutExecuted(listener) {
|
|
258
|
+
this.payoutExecutedListeners.add(listener);
|
|
259
|
+
return () => this.payoutExecutedListeners.delete(listener);
|
|
260
|
+
}
|
|
261
|
+
onRefundExecuted(listener) {
|
|
262
|
+
this.refundExecutedListeners.add(listener);
|
|
263
|
+
return () => this.refundExecutedListeners.delete(listener);
|
|
264
|
+
}
|
|
265
|
+
onNormalUserActorSync(listener) {
|
|
266
|
+
this.normalUserActorSyncListeners.add(listener);
|
|
267
|
+
return () => this.normalUserActorSyncListeners.delete(listener);
|
|
268
|
+
}
|
|
269
|
+
onCoordinatorError(listener) {
|
|
270
|
+
this.coordinatorErrorListeners.add(listener);
|
|
271
|
+
return () => this.coordinatorErrorListeners.delete(listener);
|
|
272
|
+
}
|
|
273
|
+
get currentSwapState() {
|
|
274
|
+
return this.swapState;
|
|
275
|
+
}
|
|
276
|
+
get currentSessionStatus() {
|
|
277
|
+
return this.sessionStatus;
|
|
278
|
+
}
|
|
279
|
+
/** @internal The resolved contract id used as the V0 refund request id. */
|
|
280
|
+
get currentSwapId() {
|
|
281
|
+
return this.swapId;
|
|
282
|
+
}
|
|
283
|
+
async runSwap(params) {
|
|
284
|
+
assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
|
|
285
|
+
this.assertNoActiveContract();
|
|
286
|
+
this.setSessionStatus("connecting");
|
|
287
|
+
let completed = false;
|
|
288
|
+
try {
|
|
289
|
+
await ensureFrostWasm();
|
|
290
|
+
await this.openSession();
|
|
291
|
+
this.setSessionStatus("running");
|
|
292
|
+
const result = await this.runProtocol(params);
|
|
293
|
+
completed = true;
|
|
294
|
+
this.setSessionStatus("done");
|
|
295
|
+
return result;
|
|
296
|
+
} catch (err) {
|
|
297
|
+
const error = this.normalizeSessionError(err);
|
|
298
|
+
if (!isBenignTransportDrop(error)) {
|
|
299
|
+
this.setSessionStatus("failed");
|
|
300
|
+
}
|
|
301
|
+
throw error;
|
|
302
|
+
} finally {
|
|
303
|
+
if (!completed) {
|
|
304
|
+
this.close();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async contractRequest(params) {
|
|
309
|
+
assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
|
|
310
|
+
this.assertNoActiveContract();
|
|
311
|
+
this.setSessionStatus("connecting");
|
|
312
|
+
try {
|
|
313
|
+
await ensureFrostWasm();
|
|
314
|
+
await this.openSession();
|
|
315
|
+
this.setSessionStatus("running");
|
|
316
|
+
const prepared = await this.prepareContract(params);
|
|
317
|
+
return toContractRequestResult(prepared);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const error = this.normalizeSessionError(err);
|
|
320
|
+
this.setSessionStatus("failed");
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** @internal Used by the functional Session API without closing its caller-owned session. */
|
|
325
|
+
releaseWithoutClosing() {
|
|
326
|
+
this.stopStatusReconciliation?.();
|
|
327
|
+
this.stopStatusReconciliation = null;
|
|
328
|
+
this.detachSession(
|
|
329
|
+
new TransportError("CONNECTION_LOST", "coordinator session operation released"),
|
|
330
|
+
false,
|
|
331
|
+
false
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
close() {
|
|
335
|
+
this.stopStatusReconciliation?.();
|
|
336
|
+
this.stopStatusReconciliation = null;
|
|
337
|
+
this.detachSession(new TransportError("CONNECTION_LOST", "coordinator session closed"), true);
|
|
338
|
+
}
|
|
339
|
+
restoreSwapContext(params) {
|
|
340
|
+
this.swapId = params.swapId;
|
|
341
|
+
if (params.syncSecret !== void 0) {
|
|
342
|
+
if (params.syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
|
|
343
|
+
throw new CommandError(
|
|
344
|
+
"REJECTED_GUARD",
|
|
345
|
+
`syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
this.syncSecret = new Uint8Array(params.syncSecret);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async requestSwapStatus(timeoutMs) {
|
|
352
|
+
if (!this.swapId) {
|
|
353
|
+
throw new CommandError("REJECTED_STATE", "Sync requires swapId");
|
|
354
|
+
}
|
|
355
|
+
await this.openSession();
|
|
356
|
+
const env = await this.request(
|
|
357
|
+
CoordinatorMessageType.Sync,
|
|
358
|
+
{
|
|
359
|
+
actor: "user",
|
|
360
|
+
...this.syncSecret ? { sync_secret: this.syncSecret } : {}
|
|
361
|
+
},
|
|
362
|
+
timeoutMs
|
|
363
|
+
);
|
|
364
|
+
const status = this.requireProtocol().asSyncUserResponsePayload(env);
|
|
365
|
+
this.applySwapStatus(status, env.timestamp_ms);
|
|
366
|
+
return status;
|
|
367
|
+
}
|
|
368
|
+
startSwapStatusReconciliation(intervalMs = DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS) {
|
|
369
|
+
this.stopStatusReconciliation?.();
|
|
370
|
+
let stopped = false;
|
|
371
|
+
let reconciling = false;
|
|
372
|
+
let timer = null;
|
|
373
|
+
const browserWindow = typeof window === "undefined" ? null : window;
|
|
374
|
+
const browserDocument = typeof document === "undefined" ? null : document;
|
|
375
|
+
const retryBackoff = createExponentialBackoff({
|
|
376
|
+
initialDelayMs: intervalMs,
|
|
377
|
+
maxDelayMs: Math.max(MAX_SWAP_STATUS_RETRY_DELAY_MS, intervalMs),
|
|
378
|
+
multiplier: SWAP_STATUS_RETRY_MULTIPLIER,
|
|
379
|
+
jitterRatio: SWAP_STATUS_RETRY_JITTER_RATIO
|
|
380
|
+
});
|
|
381
|
+
const isVisible = () => browserDocument === null || browserDocument.visibilityState === "visible";
|
|
382
|
+
const schedule = (delayMs) => {
|
|
383
|
+
if (stopped || !isVisible()) return;
|
|
384
|
+
if (timer) clearTimeout(timer);
|
|
385
|
+
timer = setTimeout(() => {
|
|
386
|
+
timer = null;
|
|
387
|
+
void reconcile();
|
|
388
|
+
}, delayMs);
|
|
389
|
+
};
|
|
390
|
+
const reconcile = async () => {
|
|
391
|
+
if (stopped || reconciling || !isVisible()) return;
|
|
392
|
+
reconciling = true;
|
|
393
|
+
try {
|
|
394
|
+
const status = await this.requestSwapStatus();
|
|
395
|
+
retryBackoff.reset();
|
|
396
|
+
if (status.state === "completed" || status.state === "refunded" || status.state === "failed") {
|
|
397
|
+
stopped = true;
|
|
398
|
+
if (timer) clearTimeout(timer);
|
|
399
|
+
} else {
|
|
400
|
+
schedule(intervalMs);
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
this.log(`SwapStatus reconcile failed: ${normalizeError(err)}`, "info");
|
|
404
|
+
if (!isRetryableSwapStatusReconciliationError(err)) {
|
|
405
|
+
stopped = true;
|
|
406
|
+
if (timer) clearTimeout(timer);
|
|
407
|
+
this.close();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
schedule(retryBackoff.nextDelayMs());
|
|
411
|
+
} finally {
|
|
412
|
+
reconciling = false;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const reconcileWhenVisible = () => {
|
|
416
|
+
if (!isVisible()) return;
|
|
417
|
+
if (timer) clearTimeout(timer);
|
|
418
|
+
timer = null;
|
|
419
|
+
void reconcile();
|
|
420
|
+
};
|
|
421
|
+
const pauseWhileInactive = () => {
|
|
422
|
+
if (timer) clearTimeout(timer);
|
|
423
|
+
timer = null;
|
|
424
|
+
};
|
|
425
|
+
const handleVisibilityChange = () => {
|
|
426
|
+
if (!isVisible()) {
|
|
427
|
+
pauseWhileInactive();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
reconcileWhenVisible();
|
|
431
|
+
};
|
|
432
|
+
const handleFreeze = () => {
|
|
433
|
+
pauseWhileInactive();
|
|
434
|
+
this.detachSession(new TransportError("CONNECTION_LOST", "coordinator page frozen"), true);
|
|
435
|
+
};
|
|
436
|
+
browserWindow?.addEventListener("focus", reconcileWhenVisible);
|
|
437
|
+
browserWindow?.addEventListener("online", reconcileWhenVisible);
|
|
438
|
+
browserWindow?.addEventListener("pageshow", reconcileWhenVisible);
|
|
439
|
+
browserDocument?.addEventListener("visibilitychange", handleVisibilityChange);
|
|
440
|
+
browserDocument?.addEventListener("freeze", handleFreeze);
|
|
441
|
+
browserDocument?.addEventListener("resume", reconcileWhenVisible);
|
|
442
|
+
reconcileWhenVisible();
|
|
443
|
+
this.stopStatusReconciliation = () => {
|
|
444
|
+
stopped = true;
|
|
445
|
+
if (timer) clearTimeout(timer);
|
|
446
|
+
browserWindow?.removeEventListener("focus", reconcileWhenVisible);
|
|
447
|
+
browserWindow?.removeEventListener("online", reconcileWhenVisible);
|
|
448
|
+
browserWindow?.removeEventListener("pageshow", reconcileWhenVisible);
|
|
449
|
+
browserDocument?.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
450
|
+
browserDocument?.removeEventListener("freeze", handleFreeze);
|
|
451
|
+
browserDocument?.removeEventListener("resume", reconcileWhenVisible);
|
|
452
|
+
};
|
|
453
|
+
return this.stopStatusReconciliation;
|
|
454
|
+
}
|
|
455
|
+
async requestRefund(params) {
|
|
456
|
+
const result = await this.requestRefundIntentInternal(params, { waitForFinality: true });
|
|
457
|
+
if (result.kind === "executed") return result.refund;
|
|
458
|
+
throw new CommandError("REJECTED_STALE_JOB", "refund accepted without final execution");
|
|
459
|
+
}
|
|
460
|
+
requestRefundIntent(params) {
|
|
461
|
+
return this.requestRefundIntentInternal(params, { waitForFinality: false });
|
|
462
|
+
}
|
|
463
|
+
async requestRefundIntentInternal(params, options) {
|
|
464
|
+
assertRecoveryCode(params.recoveryCode);
|
|
465
|
+
const recoveryCode = new Uint8Array(params.recoveryCode);
|
|
466
|
+
const ackTimeout = params.timeoutMs ?? 3e4;
|
|
467
|
+
let ackTimer;
|
|
468
|
+
let openedForRefund = false;
|
|
469
|
+
try {
|
|
470
|
+
if (params.syncSecret !== void 0) {
|
|
471
|
+
const swapId = params.swapId ?? this.swapId;
|
|
472
|
+
if (!swapId) {
|
|
473
|
+
throw new CommandError("REJECTED_STATE", "syncSecret requires swapId");
|
|
474
|
+
}
|
|
475
|
+
this.restoreSwapContext({ swapId, syncSecret: params.syncSecret });
|
|
476
|
+
} else if (params.swapId) {
|
|
477
|
+
const swapId = params.swapId;
|
|
478
|
+
this.restoreSwapContext({ swapId });
|
|
479
|
+
}
|
|
480
|
+
openedForRefund = !this.hasUsableSession();
|
|
481
|
+
await this.openSession();
|
|
482
|
+
const env = await Promise.race([
|
|
483
|
+
this.request(CoordinatorMessageType.WithdrawRequest, {
|
|
484
|
+
recovery_code: recoveryCode
|
|
485
|
+
}),
|
|
486
|
+
new Promise((_, reject) => {
|
|
487
|
+
ackTimer = setTimeout(
|
|
488
|
+
() => reject(
|
|
489
|
+
new CommandError(
|
|
490
|
+
"REJECTED_STALE_JOB",
|
|
491
|
+
`WithdrawRequest acknowledgement timed out after ${ackTimeout}ms`
|
|
492
|
+
)
|
|
493
|
+
),
|
|
494
|
+
ackTimeout
|
|
495
|
+
);
|
|
496
|
+
})
|
|
497
|
+
]);
|
|
498
|
+
if (ackTimer) clearTimeout(ackTimer);
|
|
499
|
+
if (env.type === CoordinatorMessageType.WithdrawExecuted) {
|
|
500
|
+
if (env.swap_id && !this.swapId) {
|
|
501
|
+
this.swapId = env.swap_id;
|
|
502
|
+
}
|
|
503
|
+
return { kind: "executed", refund: this.requireProtocol().asWithdrawExecutedPayload(env) };
|
|
504
|
+
}
|
|
505
|
+
if (env.type !== CoordinatorMessageType.WithdrawAccepted) {
|
|
506
|
+
throw new CommandError(
|
|
507
|
+
"REJECTED_GUARD",
|
|
508
|
+
`Expected WithdrawAccepted or WithdrawExecuted response, got ${env.type}`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (env.swap_id && !this.swapId) {
|
|
512
|
+
this.swapId = env.swap_id;
|
|
513
|
+
}
|
|
514
|
+
const accepted = this.requireProtocol().asWithdrawAcceptedPayload(env);
|
|
515
|
+
if (!options.waitForFinality) {
|
|
516
|
+
return { kind: "accepted", accepted };
|
|
517
|
+
}
|
|
518
|
+
if (!this.swapId) {
|
|
519
|
+
throw new CommandError(
|
|
520
|
+
"REJECTED_STALE_JOB",
|
|
521
|
+
"refund accepted and pending finalization; check status again shortly"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
return { kind: "executed", refund: await this.awaitRefundOutcomeFromSwapStatus() };
|
|
525
|
+
} finally {
|
|
526
|
+
if (ackTimer) clearTimeout(ackTimer);
|
|
527
|
+
recoveryCode.fill(0);
|
|
528
|
+
if (openedForRefund && this.closeSessionAfterRefund) {
|
|
529
|
+
this.close();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async awaitRefundOutcomeFromSwapStatus() {
|
|
534
|
+
const deadline = Date.now() + DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS;
|
|
535
|
+
for (; ; ) {
|
|
536
|
+
const perPollTimeout = Math.min(
|
|
537
|
+
REFUND_OUTCOME_PER_POLL_TIMEOUT_MS,
|
|
538
|
+
Math.max(0, deadline - Date.now())
|
|
539
|
+
);
|
|
540
|
+
try {
|
|
541
|
+
const status = await this.requestSwapStatus(perPollTimeout);
|
|
542
|
+
if (status.withdraw_execution) {
|
|
543
|
+
return status.withdraw_execution;
|
|
544
|
+
}
|
|
545
|
+
} catch (err) {
|
|
546
|
+
if (!isRequestTimeoutError(err)) {
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (Date.now() >= deadline) {
|
|
551
|
+
throw new CommandError(
|
|
552
|
+
"REJECTED_STALE_JOB",
|
|
553
|
+
"refund accepted and pending finalization; check status again shortly"
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
await delay(REFUND_OUTCOME_POLL_INTERVAL_MS);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async runProtocol(params) {
|
|
560
|
+
const protocol = this.requireProtocol();
|
|
561
|
+
const prepared = await this.prepareContract(params);
|
|
562
|
+
const depositEnv = await prepared.depositConfirmedPromise;
|
|
563
|
+
const depositPayload = protocol.asDepositConfirmedPayload(depositEnv);
|
|
564
|
+
if (depositPayload.amount_lamports !== params.amountLamports) {
|
|
565
|
+
throw new InvalidDepositAmountError(
|
|
566
|
+
BigInt(depositPayload.amount_lamports),
|
|
567
|
+
BigInt(params.amountLamports)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
this.log(
|
|
571
|
+
`deposit confirmed tx=${depositPayload.tx_signature} amount=${depositPayload.amount_lamports}`,
|
|
572
|
+
"in"
|
|
573
|
+
);
|
|
574
|
+
await params.onDepositConfirmed?.({ originAddress: depositPayload.origin_address });
|
|
575
|
+
await this.requestSwapStatus();
|
|
576
|
+
return {
|
|
577
|
+
swapId: prepared.swapId,
|
|
578
|
+
jointPublicKey: prepared.jointPublicKey,
|
|
579
|
+
depositTxSignature: depositPayload.tx_signature,
|
|
580
|
+
depositObservedAtMs: depositPayload.observed_at_ms,
|
|
581
|
+
delegatedAtMs: prepared.delegatedAtMs,
|
|
582
|
+
recoveryCodeHex: prepared.recoveryCodeHex,
|
|
583
|
+
policySnapshot: prepared.policySnapshot,
|
|
584
|
+
delegatePolicySnapshot: prepared.delegatePolicySnapshot
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async prepareContract(params) {
|
|
588
|
+
const protocol = this.requireProtocol();
|
|
589
|
+
const requestedPayoutWindowMs = params.payoutWindowMs ?? payoutWindowMsFromSpec(params.payoutSpec);
|
|
590
|
+
const payoutDeadlineMs = requestedPayoutWindowMs !== void 0 ? Date.now() + requestedPayoutWindowMs : void 0;
|
|
591
|
+
let generatedSyncSecret = null;
|
|
592
|
+
const syncSecret = params.syncSecret ? new Uint8Array(params.syncSecret) : (() => {
|
|
593
|
+
generatedSyncSecret = randomSecret32();
|
|
594
|
+
return generatedSyncSecret;
|
|
595
|
+
})();
|
|
596
|
+
if (syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
|
|
597
|
+
throw new CommandError(
|
|
598
|
+
"REJECTED_GUARD",
|
|
599
|
+
`syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
this.syncSecret = new Uint8Array(syncSecret);
|
|
603
|
+
const syncSecretHex = bytesToHex(syncSecret);
|
|
604
|
+
const syncCommitment = await sha256Commitment(syncSecret);
|
|
605
|
+
if (generatedSyncSecret) {
|
|
606
|
+
await params.onSyncSecretReady?.({ secretHex: syncSecretHex });
|
|
607
|
+
}
|
|
608
|
+
const swapResp = await this.request(CoordinatorMessageType.ContractRequest, {
|
|
609
|
+
actor: "user",
|
|
610
|
+
amount_lamports: params.amountLamports,
|
|
611
|
+
payout_spec: params.payoutSpec,
|
|
612
|
+
sync_commitment: syncCommitment,
|
|
613
|
+
...(params.payoutDeadlineMs !== void 0 || payoutDeadlineMs !== void 0) && {
|
|
614
|
+
payout_deadline_ms: params.payoutDeadlineMs ?? payoutDeadlineMs
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
const swapPayload = protocol.asSwapRequestResponsePayload(swapResp);
|
|
618
|
+
const swapId = swapPayload.swap_id;
|
|
619
|
+
this.swapId = swapId;
|
|
620
|
+
params.onPolicySnapshot?.(swapPayload.policy_snapshot);
|
|
621
|
+
this.log(`swap accepted - id=${swapId}`, "in");
|
|
622
|
+
this.bufferDepositConfirmedUntilDelegateAck = true;
|
|
623
|
+
this.bufferedDepositConfirmed = null;
|
|
624
|
+
const depositConfirmedPromise = this.awaitPush(CoordinatorMessageType.DepositConfirmed, null);
|
|
625
|
+
void depositConfirmedPromise.catch(() => void 0);
|
|
626
|
+
const { jointPublicKey, keyHandle, depositExpiresAtMs, coordinatorTimestampMs } = await this.runDkg();
|
|
627
|
+
let recoveryCommitment = params.recoveryCommitment;
|
|
628
|
+
let recoveryCodeHex;
|
|
629
|
+
if (!recoveryCommitment) {
|
|
630
|
+
const generated = await generateRecoveryCode();
|
|
631
|
+
recoveryCommitment = generated.commitment;
|
|
632
|
+
try {
|
|
633
|
+
recoveryCodeHex = recoveryCodeToHex(generated.code);
|
|
634
|
+
await params.onRecoveryCodeReady?.({ codeHex: recoveryCodeHex });
|
|
635
|
+
} finally {
|
|
636
|
+
generated.code.fill(0);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck);
|
|
640
|
+
const share = params.delegateShare ?? keyHandle.secretShareBytes();
|
|
641
|
+
let delegateResp;
|
|
642
|
+
try {
|
|
643
|
+
delegateResp = await this.request(CoordinatorMessageType.DelegateShare, {
|
|
644
|
+
share,
|
|
645
|
+
recovery_commitment: recoveryCommitment
|
|
646
|
+
});
|
|
647
|
+
} catch (err) {
|
|
648
|
+
void delegateAckPromise.catch(() => void 0);
|
|
649
|
+
throw err;
|
|
650
|
+
}
|
|
651
|
+
const delegatePayload = protocol.asDelegateShareResponsePayload(delegateResp);
|
|
652
|
+
const delegateAck = protocol.asDelegateAckPayload(await delegateAckPromise);
|
|
653
|
+
if (delegateAck.delegated_at_ms !== delegatePayload.delegated_at_ms) {
|
|
654
|
+
throw new CommandError(
|
|
655
|
+
"REJECTED_GUARD",
|
|
656
|
+
"DelegateAck delegated_at_ms does not match DelegateShare response"
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (!spendingPoliciesEqual(delegateAck.spending_policy, swapPayload.policy_snapshot)) {
|
|
660
|
+
throw new CommandError(
|
|
661
|
+
"REJECTED_GUARD",
|
|
662
|
+
"DelegateAck spending_policy does not match ContractRequest policy_snapshot"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
|
|
666
|
+
this.releaseDepositConfirmedGate();
|
|
667
|
+
await params.onDepositAddressReady?.({
|
|
668
|
+
swapId,
|
|
669
|
+
jointPublicKey,
|
|
670
|
+
amountLamports: params.amountLamports,
|
|
671
|
+
depositExpiresAtMs,
|
|
672
|
+
coordinatorTimestampMs
|
|
673
|
+
});
|
|
674
|
+
return {
|
|
675
|
+
requestId: swapId,
|
|
676
|
+
swapId,
|
|
677
|
+
amountLamports: params.amountLamports,
|
|
678
|
+
jointPublicKey,
|
|
679
|
+
depositExpiresAtMs,
|
|
680
|
+
coordinatorTimestampMs,
|
|
681
|
+
delegatedAtMs: delegateAck.delegated_at_ms,
|
|
682
|
+
syncSecret,
|
|
683
|
+
syncSecretHex,
|
|
684
|
+
recoveryCodeHex,
|
|
685
|
+
policySnapshot: swapPayload.policy_snapshot,
|
|
686
|
+
delegatePolicySnapshot: delegateAck.spending_policy,
|
|
687
|
+
depositConfirmedPromise
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async runDkg() {
|
|
691
|
+
await ensureFrostWasm();
|
|
692
|
+
const protocol = this.requireProtocol();
|
|
693
|
+
const { dkgRound1 } = await loadFrost();
|
|
694
|
+
await this.request(CoordinatorMessageType.DkgInit, {});
|
|
695
|
+
this.log("DKG initialised", "info");
|
|
696
|
+
const aliceRound1State = dkgRound1(ALICE_ID);
|
|
697
|
+
const aliceRound1Bytes = aliceRound1State.round1PackageBytes();
|
|
698
|
+
const round1Resp = await this.request(CoordinatorMessageType.DkgRound1, {
|
|
699
|
+
package: aliceRound1Bytes
|
|
700
|
+
});
|
|
701
|
+
const round1Payload = protocol.asDkgRound1ResponsePayload(round1Resp);
|
|
702
|
+
this.log(`DKG round1 complete - coord pkg ${round1Payload.package.byteLength}B`, "in");
|
|
703
|
+
const aliceRound2State = aliceRound1State.intoRound2(COORDINATOR_ID, round1Payload.package);
|
|
704
|
+
const aliceRound2Bytes = aliceRound2State.round2PackageBytesFor(COORDINATOR_ID);
|
|
705
|
+
let aliceKeyHandle;
|
|
706
|
+
try {
|
|
707
|
+
aliceKeyHandle = aliceRound2State.intoRound3(
|
|
708
|
+
COORDINATOR_ID,
|
|
709
|
+
round1Payload.package,
|
|
710
|
+
round1Payload.round2_for_alice
|
|
711
|
+
);
|
|
712
|
+
} catch (err) {
|
|
713
|
+
throw new CommandError("REJECTED_GUARD", `Alice DKG finalize failed: ${normalizeError(err)}`);
|
|
714
|
+
}
|
|
715
|
+
const dkgCompletePromise = this.awaitPush(CoordinatorMessageType.DkgComplete);
|
|
716
|
+
await this.request(CoordinatorMessageType.DkgRound2, {
|
|
717
|
+
package: aliceRound2Bytes
|
|
718
|
+
});
|
|
719
|
+
const completeEnvelope = await dkgCompletePromise;
|
|
720
|
+
const complete = readDkgComplete(protocol, completeEnvelope);
|
|
721
|
+
const alicePubkey = aliceKeyHandle.groupPublicKeyBytes();
|
|
722
|
+
if (!bytesEqual(alicePubkey, complete.jointPublicKey)) {
|
|
723
|
+
throw new CommandError(
|
|
724
|
+
"REJECTED_GUARD",
|
|
725
|
+
"DKG joint public key mismatch between Alice and coordinator"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const pubkeyHexPrefix = Array.from(alicePubkey.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
729
|
+
this.log(`DKG complete - joint pubkey ${pubkeyHexPrefix}`, "info");
|
|
730
|
+
return {
|
|
731
|
+
jointPublicKey: alicePubkey,
|
|
732
|
+
keyHandle: aliceKeyHandle,
|
|
733
|
+
depositExpiresAtMs: complete.depositExpiresAtMs,
|
|
734
|
+
coordinatorTimestampMs: complete.coordinatorTimestampMs
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
async openSession() {
|
|
738
|
+
if (this.hasUsableSession()) return;
|
|
739
|
+
if (this.session !== null) {
|
|
740
|
+
this.detachSession(
|
|
741
|
+
new TransportError("CONNECTION_LOST", "cached coordinator session is not usable"),
|
|
742
|
+
true
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
this.protocol = await loadProtocol();
|
|
746
|
+
const session = await this.createSessionImpl({ coordinator: this.coordinator });
|
|
747
|
+
this.session = session;
|
|
748
|
+
this.attested = true;
|
|
749
|
+
this.unsubscribeEnvelope = onEnvelope(session, (envelope) => this.handleEnvelope(envelope));
|
|
750
|
+
this.unsubscribeTransportState = getSessionState(session).transport.onStateChange((state) => {
|
|
751
|
+
if (this.session !== session) return;
|
|
752
|
+
if (state === "closing" || state === "closed") {
|
|
753
|
+
const fatalError = getSessionState(session).lastFatalError;
|
|
754
|
+
this.detachSession(
|
|
755
|
+
fatalError ?? new TransportError("CONNECTION_LOST", "coordinator transport closed"),
|
|
756
|
+
false
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
this.log(`connected to ${this.coordinator.endpoints[0]?.wsUrl ?? "coordinator"}`, "info");
|
|
761
|
+
this.log("attestation verified", "info");
|
|
762
|
+
}
|
|
763
|
+
assertNoActiveContract() {
|
|
764
|
+
if (this.swapId !== null || this.sessionStatus === "connecting" || this.sessionStatus === "running") {
|
|
765
|
+
throw new CommandError("REJECTED_STATE", ACTIVE_CONTRACT_REQUEST_MESSAGE);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
hasUsableSession() {
|
|
769
|
+
if (this.session === null) return false;
|
|
770
|
+
const state = getSessionState(this.session);
|
|
771
|
+
return state.attested && state.channel !== null && state.transport.state() === "open";
|
|
772
|
+
}
|
|
773
|
+
detachSession(error, closeTransport, closeProtocolRuntime2 = true) {
|
|
774
|
+
const rejectionError = this.attested && !closeTransport && isBenignTransportDropCandidate(error) ? tagBenignTransportDrop(error) : error;
|
|
775
|
+
this.unsubscribeEnvelope?.();
|
|
776
|
+
this.unsubscribeEnvelope = null;
|
|
777
|
+
this.unsubscribeTransportState?.();
|
|
778
|
+
this.unsubscribeTransportState = null;
|
|
779
|
+
const session = this.session;
|
|
780
|
+
this.session = null;
|
|
781
|
+
this.attested = false;
|
|
782
|
+
if (session !== null) {
|
|
783
|
+
if (closeProtocolRuntime2) closeProtocolRuntime(session);
|
|
784
|
+
if (closeTransport) {
|
|
785
|
+
getSessionState(session).close();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
this.rejectAllPushListeners(rejectionError);
|
|
789
|
+
}
|
|
790
|
+
handleEnvelope(env) {
|
|
791
|
+
const protocol = this.requireProtocol();
|
|
792
|
+
this.log(`<- ${env.type} kind=${env.kind}`, "in");
|
|
793
|
+
const payoutExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.PayoutExecuted ? protocol.asPayoutExecutedPayload(env) : null;
|
|
794
|
+
const payoutMatchesCurrentSwap = payoutExecuted !== null && (env.swap_id === null || env.swap_id === this.swapId);
|
|
795
|
+
const refundExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.WithdrawExecuted ? protocol.asWithdrawExecutedPayload(env) : null;
|
|
796
|
+
if (this.shouldBufferDepositConfirmed(env)) {
|
|
797
|
+
this.bufferDepositConfirmed(env);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const foreignPayout = payoutExecuted !== null && !payoutMatchesCurrentSwap;
|
|
801
|
+
const next = foreignPayout ? null : protocol.nextState(
|
|
802
|
+
this.swapState,
|
|
803
|
+
env.kind,
|
|
804
|
+
env.type,
|
|
805
|
+
payoutExecuted ? { payoutFinal: payoutExecuted.final } : void 0
|
|
806
|
+
);
|
|
807
|
+
if (env.kind === EnvelopeKind.PUSH && next === null) {
|
|
808
|
+
this.log(
|
|
809
|
+
foreignPayout ? `ignored ${env.type} for swap ${env.swap_id ?? "null"} while current swap is ${this.swapId ?? "none"}` : `ignored invalid push ${env.type} in state ${this.swapState}`,
|
|
810
|
+
"info"
|
|
811
|
+
);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (next !== null) {
|
|
815
|
+
this.swapState = next;
|
|
816
|
+
this.emitStateChange();
|
|
817
|
+
}
|
|
818
|
+
if (env.kind === EnvelopeKind.ERROR) {
|
|
819
|
+
const coordinatorError = coordinatorErrorFromEnvelope(protocol, env);
|
|
820
|
+
if (env.req_id === 0) {
|
|
821
|
+
for (const listener of this.coordinatorErrorListeners) {
|
|
822
|
+
listener(coordinatorError);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (env.kind === EnvelopeKind.PUSH) {
|
|
828
|
+
if (payoutExecuted && payoutMatchesCurrentSwap) {
|
|
829
|
+
this.emitPayoutExecuted(payoutExecuted, env.swap_id ?? this.swapId);
|
|
830
|
+
}
|
|
831
|
+
if (refundExecuted) {
|
|
832
|
+
this.emitRefundExecuted(refundExecuted, env.swap_id ?? this.swapId);
|
|
833
|
+
}
|
|
834
|
+
this.resolvePushListener(env);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
applySwapStatus(status, coordinatorTimestampMs) {
|
|
838
|
+
const keepTerminalState = TERMINAL_SWAP_STATES.has(this.swapState) && status.state !== this.swapState;
|
|
839
|
+
if (!keepTerminalState && this.swapState !== status.state) {
|
|
840
|
+
this.swapState = status.state;
|
|
841
|
+
this.emitStateChange();
|
|
842
|
+
}
|
|
843
|
+
for (const fragment of status.fragments_executed) {
|
|
844
|
+
this.emitPayoutExecuted(fragment, this.swapId);
|
|
845
|
+
}
|
|
846
|
+
if (status.withdraw_execution) {
|
|
847
|
+
this.emitRefundExecuted(status.withdraw_execution, this.swapId);
|
|
848
|
+
}
|
|
849
|
+
if (status.actor_sync !== void 0) {
|
|
850
|
+
for (const listener of this.normalUserActorSyncListeners) {
|
|
851
|
+
listener(status.actor_sync, coordinatorTimestampMs);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
emitPayoutExecuted(payload, swapId) {
|
|
856
|
+
if (!swapId) return;
|
|
857
|
+
const key = `${swapId}:${payload.index}:${payload.tx_signature}`;
|
|
858
|
+
if (this.emittedPayoutKeys.has(key)) return;
|
|
859
|
+
this.emittedPayoutKeys.add(key);
|
|
860
|
+
this.log(
|
|
861
|
+
`payout executed tx=${payload.tx_signature} amount=${payload.amount_lamports} final=${payload.final}`,
|
|
862
|
+
"in"
|
|
863
|
+
);
|
|
864
|
+
for (const listener of this.payoutExecutedListeners) {
|
|
865
|
+
listener(payload, swapId);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
emitRefundExecuted(payload, swapId) {
|
|
869
|
+
if (!swapId) return;
|
|
870
|
+
const key = `${swapId}:${payload.tx_signature}`;
|
|
871
|
+
if (this.emittedRefundKeys.has(key)) return;
|
|
872
|
+
this.emittedRefundKeys.add(key);
|
|
873
|
+
this.log(`refund executed tx=${payload.tx_signature} amount=${payload.amount_lamports}`, "in");
|
|
874
|
+
for (const listener of this.refundExecutedListeners) {
|
|
875
|
+
listener(payload, swapId);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
request(type, payload, timeoutMs) {
|
|
879
|
+
if (this.session === null) {
|
|
880
|
+
throw new TransportError("CONNECTION_LOST", "coordinator session is not connected");
|
|
881
|
+
}
|
|
882
|
+
return requestEnvelope(this.session, type, payload, {
|
|
883
|
+
swapId: this.swapId,
|
|
884
|
+
timeoutMs,
|
|
885
|
+
onLog: (message, direction) => this.log(message, direction)
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
awaitPush(type, timeoutMs = 3e4) {
|
|
889
|
+
return new Promise((resolve, reject) => {
|
|
890
|
+
const timer = timeoutMs === null ? null : setTimeout(() => {
|
|
891
|
+
this.pushListeners.delete(type);
|
|
892
|
+
reject(new CommandError("REJECTED_STALE_JOB", `timed out waiting for ${type} push`));
|
|
893
|
+
}, timeoutMs);
|
|
894
|
+
this.pushListeners.set(type, { resolve, reject, timer });
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
rejectAllPushListeners(err) {
|
|
898
|
+
for (const listener of this.pushListeners.values()) {
|
|
899
|
+
if (listener.timer) clearTimeout(listener.timer);
|
|
900
|
+
listener.reject(err);
|
|
901
|
+
}
|
|
902
|
+
this.pushListeners.clear();
|
|
903
|
+
this.bufferDepositConfirmedUntilDelegateAck = false;
|
|
904
|
+
this.bufferedDepositConfirmed = null;
|
|
905
|
+
}
|
|
906
|
+
shouldBufferDepositConfirmed(env) {
|
|
907
|
+
return env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.DepositConfirmed && this.bufferDepositConfirmedUntilDelegateAck;
|
|
908
|
+
}
|
|
909
|
+
bufferDepositConfirmed(env) {
|
|
910
|
+
if (!this.bufferedDepositConfirmed) {
|
|
911
|
+
this.bufferedDepositConfirmed = env;
|
|
912
|
+
this.clearPushListenerTimer(CoordinatorMessageType.DepositConfirmed);
|
|
913
|
+
this.log("buffered DepositConfirmed until DelegateAck is verified", "info");
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
this.log("ignored duplicate DepositConfirmed while waiting for DelegateAck", "info");
|
|
917
|
+
}
|
|
918
|
+
releaseDepositConfirmedGate() {
|
|
919
|
+
const protocol = this.requireProtocol();
|
|
920
|
+
this.bufferDepositConfirmedUntilDelegateAck = false;
|
|
921
|
+
const buffered = this.bufferedDepositConfirmed;
|
|
922
|
+
if (!buffered) return;
|
|
923
|
+
this.bufferedDepositConfirmed = null;
|
|
924
|
+
const next = protocol.nextState(this.swapState, buffered.kind, buffered.type);
|
|
925
|
+
if (next === null) {
|
|
926
|
+
this.log(`ignored buffered DepositConfirmed in state ${this.swapState}`, "info");
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.swapState = next;
|
|
930
|
+
this.emitStateChange();
|
|
931
|
+
this.resolvePushListener(buffered);
|
|
932
|
+
}
|
|
933
|
+
resolvePushListener(env) {
|
|
934
|
+
const listener = this.pushListeners.get(env.type);
|
|
935
|
+
if (!listener) return;
|
|
936
|
+
this.pushListeners.delete(env.type);
|
|
937
|
+
if (listener.timer) clearTimeout(listener.timer);
|
|
938
|
+
listener.resolve(env);
|
|
939
|
+
}
|
|
940
|
+
clearPushListenerTimer(type) {
|
|
941
|
+
const listener = this.pushListeners.get(type);
|
|
942
|
+
if (!listener?.timer) return;
|
|
943
|
+
clearTimeout(listener.timer);
|
|
944
|
+
listener.timer = null;
|
|
945
|
+
}
|
|
946
|
+
setSessionStatus(status) {
|
|
947
|
+
this.sessionStatus = status;
|
|
948
|
+
this.emitStateChange();
|
|
949
|
+
}
|
|
950
|
+
emitStateChange() {
|
|
951
|
+
for (const listener of this.stateListeners) {
|
|
952
|
+
listener(this.swapState, this.sessionStatus);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
log(msg, direction) {
|
|
956
|
+
for (const listener of this.logListeners) {
|
|
957
|
+
listener(msg, direction);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
normalizeSessionError(err) {
|
|
961
|
+
if (err instanceof Error) {
|
|
962
|
+
if (this.attested && isBenignTransportDropCandidate(err)) return tagBenignTransportDrop(err);
|
|
963
|
+
return err;
|
|
964
|
+
}
|
|
965
|
+
return new Error(String(err));
|
|
966
|
+
}
|
|
967
|
+
requireProtocol() {
|
|
968
|
+
if (this.protocol === null) {
|
|
969
|
+
throw new CommandError("REJECTED_STATE", "coordinator protocol is not loaded");
|
|
970
|
+
}
|
|
971
|
+
return this.protocol;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
function toContractRequestResult(prepared) {
|
|
975
|
+
return {
|
|
976
|
+
accepted: true,
|
|
977
|
+
requestId: prepared.requestId,
|
|
978
|
+
state: "awaiting_deposit",
|
|
979
|
+
swapId: prepared.swapId,
|
|
980
|
+
depositAddress: solanaAddressFromPublicKeyBytes(prepared.jointPublicKey),
|
|
981
|
+
jointPublicKey: new Uint8Array(prepared.jointPublicKey),
|
|
982
|
+
amountLamports: prepared.amountLamports,
|
|
983
|
+
depositExpiresAtMs: prepared.depositExpiresAtMs,
|
|
984
|
+
coordinatorTimestampMs: prepared.coordinatorTimestampMs,
|
|
985
|
+
delegatedAtMs: prepared.delegatedAtMs,
|
|
986
|
+
syncSecret: new Uint8Array(prepared.syncSecret),
|
|
987
|
+
syncSecretHex: prepared.syncSecretHex,
|
|
988
|
+
recoveryCodeHex: prepared.recoveryCodeHex,
|
|
989
|
+
policySnapshot: prepared.policySnapshot,
|
|
990
|
+
delegatePolicySnapshot: prepared.delegatePolicySnapshot
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
async function contractRequestOnSession(session, params) {
|
|
994
|
+
if (activeFunctionalContractRequests.has(session)) {
|
|
995
|
+
throw new CommandError("REJECTED_STATE", ACTIVE_CONTRACT_REQUEST_MESSAGE);
|
|
996
|
+
}
|
|
997
|
+
activeFunctionalContractRequests.add(session);
|
|
998
|
+
let client = null;
|
|
999
|
+
try {
|
|
1000
|
+
client = new UserCoordinatorSession({
|
|
1001
|
+
coordinator: getSessionState(session).pool,
|
|
1002
|
+
createSession: async () => session
|
|
1003
|
+
});
|
|
1004
|
+
return await client.contractRequest(params);
|
|
1005
|
+
} finally {
|
|
1006
|
+
activeFunctionalContractRequests.delete(session);
|
|
1007
|
+
client?.releaseWithoutClosing();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function requestRefundOnSession(session, params) {
|
|
1011
|
+
const client = new UserCoordinatorSession(
|
|
1012
|
+
{
|
|
1013
|
+
coordinator: getSessionState(session).pool,
|
|
1014
|
+
createSession: async () => session
|
|
1015
|
+
},
|
|
1016
|
+
false
|
|
1017
|
+
);
|
|
1018
|
+
try {
|
|
1019
|
+
await client.requestRefundIntent(params);
|
|
1020
|
+
const requestId = client.currentSwapId;
|
|
1021
|
+
if (!requestId) {
|
|
1022
|
+
throw new CommandError(
|
|
1023
|
+
"REJECTED_STALE_JOB",
|
|
1024
|
+
"refund response did not include the resolved contract id"
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
return { accepted: true, requestId };
|
|
1028
|
+
} finally {
|
|
1029
|
+
client.releaseWithoutClosing();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function requestUserSync(session, swapId, syncSecret, timeoutMs) {
|
|
1033
|
+
if (swapId.trim().length === 0) {
|
|
1034
|
+
throw new CommandError("REJECTED_STATE", "sync requires a non-empty swapId");
|
|
1035
|
+
}
|
|
1036
|
+
if (syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
|
|
1037
|
+
throw new CommandError("REJECTED_GUARD", `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`);
|
|
1038
|
+
}
|
|
1039
|
+
const env = await requestEnvelope(
|
|
1040
|
+
session,
|
|
1041
|
+
CoordinatorMessageType.Sync,
|
|
1042
|
+
{
|
|
1043
|
+
actor: "user",
|
|
1044
|
+
sync_secret: new Uint8Array(syncSecret)
|
|
1045
|
+
},
|
|
1046
|
+
{ swapId, timeoutMs }
|
|
1047
|
+
);
|
|
1048
|
+
return (await loadProtocol()).asSyncUserResponsePayload(env);
|
|
1049
|
+
}
|
|
1050
|
+
async function ensureFrostWasm() {
|
|
1051
|
+
await (await loadFrostRuntime()).ensureFrostWasm();
|
|
1052
|
+
}
|
|
1053
|
+
function randomSecret32() {
|
|
1054
|
+
return crypto.getRandomValues(new Uint8Array(SYNC_SECRET_BYTE_LENGTH));
|
|
1055
|
+
}
|
|
1056
|
+
async function sha256Commitment(secret) {
|
|
1057
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", new Uint8Array(secret));
|
|
1058
|
+
return new Uint8Array(hashBuffer);
|
|
1059
|
+
}
|
|
1060
|
+
function bytesToHex(bytes) {
|
|
1061
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1062
|
+
}
|
|
1063
|
+
function delay(ms) {
|
|
1064
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1065
|
+
}
|
|
1066
|
+
function payoutWindowMsFromSpec(payoutSpec) {
|
|
1067
|
+
if (payoutSpec.mode === "instant") return void 0;
|
|
1068
|
+
return payoutSpec.window_seconds * 1e3;
|
|
1069
|
+
}
|
|
1070
|
+
function assertCurrentUserPayoutSpecRuntimeLimits(payoutSpec) {
|
|
1071
|
+
if (payoutSpec.mode === "instant") {
|
|
1072
|
+
if (typeof payoutSpec.destination_address !== "string") {
|
|
1073
|
+
throw new PolicyValidationError(
|
|
1074
|
+
"INVALID_PAYOUT_PLAN",
|
|
1075
|
+
"instant payout_spec.destination_address must be a string"
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
publicKey(payoutSpec.destination_address);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (!Array.isArray(payoutSpec.destination_addresses)) {
|
|
1082
|
+
throw new PolicyValidationError(
|
|
1083
|
+
"INVALID_PAYOUT_PLAN",
|
|
1084
|
+
"scheduled payout_spec.destination_addresses must be an array"
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
assertCurrentUserPayoutDestinationLimit(payoutSpec.destination_addresses.length);
|
|
1088
|
+
for (const address of payoutSpec.destination_addresses) {
|
|
1089
|
+
publicKey(address);
|
|
1090
|
+
}
|
|
1091
|
+
assertCurrentUserPayoutFragmentLimit(payoutSpec.num_fragments);
|
|
1092
|
+
if (!CURRENT_SCHEDULED_PAYOUT_WINDOW_SECONDS.has(payoutSpec.window_seconds)) {
|
|
1093
|
+
throw new PolicyValidationError(
|
|
1094
|
+
"INVALID_PAYOUT_PLAN",
|
|
1095
|
+
`${CURRENT_SCHEDULED_PAYOUT_WINDOW_MESSAGE}; got ${payoutSpec.window_seconds}`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function bytesEqual(a, b) {
|
|
1100
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
1101
|
+
let diff = 0;
|
|
1102
|
+
for (let index = 0; index < a.byteLength; index += 1) {
|
|
1103
|
+
diff |= a[index] ^ b[index];
|
|
1104
|
+
}
|
|
1105
|
+
return diff === 0;
|
|
1106
|
+
}
|
|
1107
|
+
function spendingPoliciesEqual(a, b) {
|
|
1108
|
+
return a.asset === b.asset && a.entry_amount_lamports === b.entry_amount_lamports && a.total_committed_lamports === b.total_committed_lamports && a.payout_deadline_ms === b.payout_deadline_ms && a.payout_window_ms === b.payout_window_ms && a.fragment_count === b.fragment_count && a.payout_mode === b.payout_mode && a.matching_mode === b.matching_mode && a.fragmentation_allowed === b.fragmentation_allowed && a.min_fee_bps === b.min_fee_bps && a.fee_bps === b.fee_bps && a.fee_lamports === b.fee_lamports && a.net_payout_lamports === b.net_payout_lamports && payoutSchedulesEqual(a.payout_schedule, b.payout_schedule);
|
|
1109
|
+
}
|
|
1110
|
+
function payoutSchedulesEqual(a, b) {
|
|
1111
|
+
if (a.length !== b.length) return false;
|
|
1112
|
+
return a.every((entry, index) => entry.destination_address === b[index]?.destination_address);
|
|
1113
|
+
}
|
|
1114
|
+
function isRequestTimeoutError(err) {
|
|
1115
|
+
return err instanceof CommandError && err.message.includes(REQUEST_TIMEOUT_MESSAGE);
|
|
1116
|
+
}
|
|
1117
|
+
function isRetryableSwapStatusReconciliationError(err) {
|
|
1118
|
+
if (err instanceof TransportError || err instanceof RoutingError) return true;
|
|
1119
|
+
if (err instanceof AttestationError) return false;
|
|
1120
|
+
return err instanceof CommandError && err.code === "REJECTED_STALE_JOB";
|
|
1121
|
+
}
|
|
1122
|
+
function coordinatorErrorFromEnvelope(protocol, env) {
|
|
1123
|
+
try {
|
|
1124
|
+
const payload = protocol.asErrorPayload(env);
|
|
1125
|
+
return coordinatorProtocolError(payload.code, payload.message, payload.failed_req_id);
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
return coordinatorProtocolError(
|
|
1128
|
+
"ERR_INTERNAL",
|
|
1129
|
+
err instanceof Error ? err.message : "coordinator sent an invalid error payload"
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function coordinatorProtocolError(code, message, failedReqId) {
|
|
1134
|
+
const error = new Error(message);
|
|
1135
|
+
error.name = "CoordinatorProtocolError";
|
|
1136
|
+
error.code = code;
|
|
1137
|
+
if (failedReqId !== void 0) error.failedReqId = failedReqId;
|
|
1138
|
+
return error;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/user/index.ts
|
|
1142
|
+
var MIN_ENTRY_AMOUNT_LAMPORTS = PRODUCT_MIN_ENTRY_LAMPORTS;
|
|
1143
|
+
var MAX_ENTRY_AMOUNT_LAMPORTS = PRODUCT_MAX_ENTRY_LAMPORTS;
|
|
1144
|
+
var CURRENT_USER_MIN_LAMPORTS_UNSUPPORTED_MESSAGE = "destinations[*].minLamports is not supported in the current normal-user wire contract";
|
|
1145
|
+
async function contractRequest(session, args) {
|
|
1146
|
+
const payoutPolicy = validatePayoutPolicy(args.payoutPolicy, {
|
|
1147
|
+
totalLamports: args.amountLamports
|
|
1148
|
+
});
|
|
1149
|
+
assertEntryAmount(args.amountLamports);
|
|
1150
|
+
assertCurrentUserContractPolicy(payoutPolicy);
|
|
1151
|
+
assertAttested(session);
|
|
1152
|
+
const destination = payoutPolicy.destinations[0];
|
|
1153
|
+
const payoutSpec = payoutSpecFromPolicy(payoutPolicy, destination.address);
|
|
1154
|
+
return contractRequestOnSession(session, {
|
|
1155
|
+
amountLamports: args.amountLamports,
|
|
1156
|
+
payoutSpec,
|
|
1157
|
+
...args.syncSecret !== void 0 && { syncSecret: args.syncSecret },
|
|
1158
|
+
...args.recoveryCommitment !== void 0 && {
|
|
1159
|
+
recoveryCommitment: args.recoveryCommitment
|
|
1160
|
+
},
|
|
1161
|
+
...args.onSyncSecretReady !== void 0 && {
|
|
1162
|
+
onSyncSecretReady: args.onSyncSecretReady
|
|
1163
|
+
},
|
|
1164
|
+
...args.onRecoveryCodeReady !== void 0 && {
|
|
1165
|
+
onRecoveryCodeReady: args.onRecoveryCodeReady
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
async function sync(session, contractId, syncSecret, timeoutMs) {
|
|
1170
|
+
assertAttested(session);
|
|
1171
|
+
return requestUserSync(session, contractId, syncSecret, timeoutMs);
|
|
1172
|
+
}
|
|
1173
|
+
function assertEntryAmount(amountLamports) {
|
|
1174
|
+
if (!Number.isSafeInteger(amountLamports) || amountLamports < MIN_ENTRY_AMOUNT_LAMPORTS || amountLamports > MAX_ENTRY_AMOUNT_LAMPORTS) {
|
|
1175
|
+
throw new PolicyValidationError(
|
|
1176
|
+
"INVALID_AMOUNT",
|
|
1177
|
+
`amountLamports must be an integer between ${MIN_ENTRY_AMOUNT_LAMPORTS} and ${MAX_ENTRY_AMOUNT_LAMPORTS} lamports`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function assertCurrentUserContractPolicy(policy) {
|
|
1182
|
+
for (const destination of policy.destinations) {
|
|
1183
|
+
if (destination.minLamports !== void 0) {
|
|
1184
|
+
throw new PolicyValidationError(
|
|
1185
|
+
"INVALID_PAYOUT_PLAN",
|
|
1186
|
+
CURRENT_USER_MIN_LAMPORTS_UNSUPPORTED_MESSAGE
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function payoutSpecFromPolicy(policy, destinationAddress) {
|
|
1192
|
+
const totalDeadlineMs = policy.totalDeadlineMs;
|
|
1193
|
+
if (totalDeadlineMs === void 0 || totalDeadlineMs === INSTANT_PAYOUT_WINDOW_MS) {
|
|
1194
|
+
return {
|
|
1195
|
+
mode: "instant",
|
|
1196
|
+
destination_address: destinationAddress
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
mode: "scheduled",
|
|
1201
|
+
num_fragments: CURRENT_USER_PAYOUT_FRAGMENT_LIMIT,
|
|
1202
|
+
window_seconds: totalDeadlineMs / PAYOUT_WINDOW_MILLISECONDS_PER_SECOND,
|
|
1203
|
+
destination_addresses: [destinationAddress]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
async function requestRefund(session, args) {
|
|
1207
|
+
assertAttested(session);
|
|
1208
|
+
assertRecoveryCode(args.recoveryCode);
|
|
1209
|
+
return requestRefundOnSession(session, {
|
|
1210
|
+
recoveryCode: new Uint8Array(args.recoveryCode)
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
async function getDepositAddress(_session, _requestId, _syncSecret) {
|
|
1214
|
+
throw new NotImplementedError("user.getDepositAddress");
|
|
1215
|
+
}
|
|
1216
|
+
function subscribe(_session, _requestId, _syncSecret, _handler) {
|
|
1217
|
+
throw new NotImplementedError("user.subscribe");
|
|
1218
|
+
}
|
|
1219
|
+
async function replayFromStorage(_session, _storage) {
|
|
1220
|
+
throw new NotImplementedError("user.replayFromStorage");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
export { INSTANT_PAYOUT_WINDOW_MS, MAX_ENTRY_AMOUNT_LAMPORTS, MIN_ENTRY_AMOUNT_LAMPORTS, PAYOUT_WINDOW_MILLISECONDS_PER_SECOND, RECOVERY_CODE_BYTE_LENGTH, SUPPORTED_TOTAL_DEADLINE_MS, commitmentFromCode, contractRequest, createCoordinatorSession, generateRecoveryCode, getDepositAddress, isBenignTransportDrop, recoveryCodeFromHex, recoveryCodeFromUserInput, recoveryCodeToHex, replayFromStorage, requestRefund, subscribe, sync };
|
|
1224
|
+
//# sourceMappingURL=user.js.map
|
|
1225
|
+
//# sourceMappingURL=user.js.map
|