@capxul/sdk 1.0.0-alpha.9 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -6
- package/dist/{InMemoryAuthCacheAdapter-v5W-XB5M.mjs → InMemoryAuthCacheAdapter-Dr1sEd9y.mjs} +36 -16
- package/dist/InMemoryAuthCacheAdapter-Dr1sEd9y.mjs.map +1 -0
- package/dist/create-capxul-client-C7H5b68l.d.mts +2764 -0
- package/dist/create-capxul-client-C7H5b68l.d.mts.map +1 -0
- package/dist/create-capxul-client-DiqVIxsV.mjs +6980 -0
- package/dist/create-capxul-client-DiqVIxsV.mjs.map +1 -0
- package/dist/index.d.mts +119 -2020
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2464 -6308
- package/dist/index.mjs.map +1 -1
- package/dist/node/index.d.mts +3 -6
- package/dist/node/index.d.mts.map +1 -1
- package/dist/node/index.mjs +27 -27
- package/dist/node/index.mjs.map +1 -1
- package/dist/signer-CJT0pPiO.d.mts +301 -0
- package/dist/signer-CJT0pPiO.d.mts.map +1 -0
- package/dist/testing/index.d.mts +54 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1068 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +17 -18
- package/dist/InMemoryAuthCacheAdapter-v5W-XB5M.mjs.map +0 -1
- package/dist/index-CTXgQ_xR.d.mts +0 -158
- package/dist/index-CTXgQ_xR.d.mts.map +0 -1
- package/dist/ports/safe-deployment.d.mts +0 -2
- package/dist/ports/safe-deployment.mjs +0 -38
- package/dist/ports/safe-deployment.mjs.map +0 -1
- package/dist/safe-deployment-D3k9yndM.d.mts +0 -136
- package/dist/safe-deployment-D3k9yndM.d.mts.map +0 -1
- package/dist/signer-DqDtJU1l.d.mts +0 -145
- package/dist/signer-DqDtJU1l.d.mts.map +0 -1
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import { C as convexCallErrorFromCapxul, D as bootstrapErrorFromCapxul, H as deriveCapxulSafeAddress, O as authClientPortFromPromiseAdapter, _ as identityErrorFromCapxul, c as smartAccountErrorFromCapxul, d as fromWei, h as wireChainId, i as redactTelemetryEvent, m as toWei, p as accountReadErrorFromCapxul, t as assembleCapxulClient, u as subAccountErrorFromCapxul } from "../create-capxul-client-DiqVIxsV.mjs";
|
|
2
|
+
import { C as toJwtToken, E as toPublishableKey, N as Errors, O as toSessionToken, S as toEpochSeconds, _ as toCountryCode, b as toEmail, d as toAccountId, f as toAddress, g as toChainId, h as toAuthUserId, j as CapxulError, k as toSubAccountId, m as toAppId, p as toAllowedOrigin, t as InMemoryAuthCacheAdapter, v as toCurrencyCode, w as toKycTier, x as toEpochMs, y as toDurationMs } from "../InMemoryAuthCacheAdapter-Dr1sEd9y.mjs";
|
|
3
|
+
import { keccak256 } from "viem";
|
|
4
|
+
import { Effect, Result, Semaphore } from "effect";
|
|
5
|
+
import { getFunctionName } from "convex/server";
|
|
6
|
+
//#region src/testing/account/InMemoryAccountReadAdapter.ts
|
|
7
|
+
var InMemoryAccountReadAdapter = class {
|
|
8
|
+
#deps;
|
|
9
|
+
#rawBalance;
|
|
10
|
+
constructor(deps = {}) {
|
|
11
|
+
this.#deps = deps;
|
|
12
|
+
this.#rawBalance = deps.rawBalance ?? "0";
|
|
13
|
+
}
|
|
14
|
+
readBalance(_input) {
|
|
15
|
+
const scriptedFailure = this.#deps.failures?.readBalance;
|
|
16
|
+
if (scriptedFailure !== void 0) return Effect.fail(accountReadErrorFromCapxul("readBalance", scriptedFailure));
|
|
17
|
+
if (this.#deps.account !== void 0) return Effect.succeed(cloneAccount(this.#deps.account));
|
|
18
|
+
const decimals = this.#deps.decimals ?? 6;
|
|
19
|
+
const currency = this.#deps.currency ?? "USD";
|
|
20
|
+
const money = fromWei(this.#rawBalance, decimals, currency);
|
|
21
|
+
return Effect.succeed({
|
|
22
|
+
id: toAccountId(this.#deps.accountId ?? "account_01TEST000000000000000000"),
|
|
23
|
+
balance: money,
|
|
24
|
+
available: money
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
fundFromFaucet(input) {
|
|
28
|
+
const scriptedFailure = this.#deps.failures?.fundFromFaucet;
|
|
29
|
+
if (scriptedFailure !== void 0) return Effect.fail(accountReadErrorFromCapxul("fundFromFaucet", scriptedFailure));
|
|
30
|
+
const expectedDecimals = this.#deps.decimals ?? 6;
|
|
31
|
+
const expectedCurrency = this.#deps.currency ?? "USD";
|
|
32
|
+
if (input.amount.decimals !== expectedDecimals || String(input.amount.currency) !== expectedCurrency) return Effect.fail(accountReadErrorFromCapxul("fundFromFaucet", Errors.invalidInput("amount", "decimals/currency must match adapter config")));
|
|
33
|
+
const minted = BigInt(toWei(input.amount));
|
|
34
|
+
this.#rawBalance = (BigInt(this.#rawBalance) + minted).toString();
|
|
35
|
+
return Effect.succeed({ txHash: "0x" + "f".repeat(64) });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function cloneAccount(account) {
|
|
39
|
+
return {
|
|
40
|
+
id: account.id,
|
|
41
|
+
balance: { ...account.balance },
|
|
42
|
+
available: { ...account.available }
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/adapters/_shared/clock.ts
|
|
47
|
+
async function readClockNow(clock) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await Effect.runPromise(Effect.result(clock.now));
|
|
50
|
+
if (Result.isFailure(result)) return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: Errors.providerError("clock", "now", result.failure)
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
value: result.success
|
|
57
|
+
};
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: Errors.providerError("clock", "now", cause)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/testing/clock/ManualClockAdapter.ts
|
|
67
|
+
/**
|
|
68
|
+
* Event-discipline test clock. Time moves only when a release primitive is
|
|
69
|
+
* invoked. `tickOne()` releases EXACTLY ONE pending sleep (the earliest
|
|
70
|
+
* deadline, FIFO at ties) and snaps `now()` to that deadline. `advance(ms)`
|
|
71
|
+
* composes repeated `tickOne()` calls over the resulting window — used to
|
|
72
|
+
* keep the cross-adapter conformance harness uniform with the distance model
|
|
73
|
+
* of `VirtualClockAdapter`. See `packages/sdk/docs/architecture.md` and the
|
|
74
|
+
* slice spec `docs/slices/clock-adapters.md`.
|
|
75
|
+
*/
|
|
76
|
+
var ManualClockAdapter = class ManualClockAdapter {
|
|
77
|
+
currentTime = 0;
|
|
78
|
+
queue = [];
|
|
79
|
+
seqCounter = 0;
|
|
80
|
+
constructor(deps) {}
|
|
81
|
+
now = Effect.sync(() => toEpochMs(this.currentTime));
|
|
82
|
+
sleep(duration) {
|
|
83
|
+
return Effect.promise(() => new Promise((resolve) => {
|
|
84
|
+
this.queue.push({
|
|
85
|
+
deadline: this.currentTime + duration,
|
|
86
|
+
resolve,
|
|
87
|
+
seq: this.seqCounter++
|
|
88
|
+
});
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Release exactly one pending sleep — the earliest deadline, FIFO at ties.
|
|
93
|
+
* Snaps `now()` forward to that sleep's deadline. No-op if the queue is
|
|
94
|
+
* empty (preserves the empty-tick safety property).
|
|
95
|
+
*/
|
|
96
|
+
tickOne() {
|
|
97
|
+
return Effect.runPromise(this.tickOneEffect());
|
|
98
|
+
}
|
|
99
|
+
tickOneEffect() {
|
|
100
|
+
const self = this;
|
|
101
|
+
return Effect.gen(function* () {
|
|
102
|
+
if (self.queue.length === 0) return;
|
|
103
|
+
const nextIndex = self.findEarliestIndex();
|
|
104
|
+
const next = self.queue.splice(nextIndex, 1)[0];
|
|
105
|
+
if (next.deadline > self.currentTime) self.currentTime = next.deadline;
|
|
106
|
+
next.resolve();
|
|
107
|
+
yield* Effect.yieldNow;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Cross-adapter harness affordance. Releases every sleep whose deadline
|
|
112
|
+
* falls inside `[currentTime, currentTime + ms]` via repeated `tickOne()`,
|
|
113
|
+
* then snaps `now()` to `currentTime + ms`. The composition is the seam
|
|
114
|
+
* that lets the harness drive Manual and Virtual identically.
|
|
115
|
+
*/
|
|
116
|
+
advance(ms) {
|
|
117
|
+
return Effect.runPromise(this.advanceEffect(toDurationMs(ms)));
|
|
118
|
+
}
|
|
119
|
+
advanceEffect(duration) {
|
|
120
|
+
const self = this;
|
|
121
|
+
return Effect.gen(function* () {
|
|
122
|
+
yield* Effect.yieldNow;
|
|
123
|
+
const target = self.currentTime + duration;
|
|
124
|
+
while (self.queue.length > 0 && self.earliestDeadline() <= target) yield* self.tickOneEffect();
|
|
125
|
+
self.currentTime = target;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
findEarliestIndex() {
|
|
129
|
+
let bestIndex = 0;
|
|
130
|
+
let best = this.queue[0];
|
|
131
|
+
for (let i = 1; i < this.queue.length; i++) {
|
|
132
|
+
const candidate = this.queue[i];
|
|
133
|
+
if (candidate.deadline < best.deadline || candidate.deadline === best.deadline && candidate.seq < best.seq) {
|
|
134
|
+
best = candidate;
|
|
135
|
+
bestIndex = i;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return bestIndex;
|
|
139
|
+
}
|
|
140
|
+
earliestDeadline() {
|
|
141
|
+
return this.queue[this.findEarliestIndex()].deadline;
|
|
142
|
+
}
|
|
143
|
+
static forConformance() {
|
|
144
|
+
return new ManualClockUnderTest(new ManualClockAdapter({}));
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var ManualClockUnderTest = class {
|
|
148
|
+
clock;
|
|
149
|
+
constructor(clock) {
|
|
150
|
+
this.clock = clock;
|
|
151
|
+
}
|
|
152
|
+
advance(ms) {
|
|
153
|
+
return this.clock.advance(ms);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/testing/auth-client/InMemoryAuthClientAdapter.ts
|
|
158
|
+
const DEFAULT_OTP_TTL_MS = toDurationMs(300 * 1e3);
|
|
159
|
+
const DEFAULT_SESSION_TTL_MS = toDurationMs(1440 * 60 * 1e3);
|
|
160
|
+
const DEFAULT_JWT_TTL_MS = toDurationMs(900 * 1e3);
|
|
161
|
+
/**
|
|
162
|
+
* Hermetic OTP + session state machine implementing `AuthClientPort`. Wires
|
|
163
|
+
* through `AuthCachePort` for persistence (clause A11) and `ClockPort`
|
|
164
|
+
* for TTL + expiry math. OTPs are deterministic per-email counters (`"000000"`,
|
|
165
|
+
* `"000001"`, …) so test assertions remain stable. Session tokens are
|
|
166
|
+
* deterministic per-adapter counters (`"tok_0"`, `"tok_1"`, …). Returning users
|
|
167
|
+
* (same email post-signOut) get the same `AuthUserId`.
|
|
168
|
+
*
|
|
169
|
+
* Never throws — every failure path returns `{ ok: false, error: CapxulError }`.
|
|
170
|
+
* Honors `options.signal` via a pre-check (clause A10): an aborted controller
|
|
171
|
+
* before method entry yields `{ ok: false, error: Errors.cancelled({ operation }) }`
|
|
172
|
+
* without any state change.
|
|
173
|
+
*/
|
|
174
|
+
var InMemoryAuthClientAdapter = class {
|
|
175
|
+
#pendingOtps = /* @__PURE__ */ new Map();
|
|
176
|
+
#otpCountersByEmail = /* @__PURE__ */ new Map();
|
|
177
|
+
#authUserIdByEmail = /* @__PURE__ */ new Map();
|
|
178
|
+
#allocatedAuthUserIds = /* @__PURE__ */ new Set();
|
|
179
|
+
#authCache;
|
|
180
|
+
#clock;
|
|
181
|
+
#otpTtlMs;
|
|
182
|
+
#failures;
|
|
183
|
+
#authUserIdCounter = 0;
|
|
184
|
+
#tokenCounter = 0;
|
|
185
|
+
#jwtCounter = 0;
|
|
186
|
+
constructor(deps) {
|
|
187
|
+
this.#authCache = deps.authCache;
|
|
188
|
+
this.#clock = deps.clock;
|
|
189
|
+
this.#otpTtlMs = deps.otpTtlMs ?? DEFAULT_OTP_TTL_MS;
|
|
190
|
+
this.#failures = deps.failures ?? {};
|
|
191
|
+
}
|
|
192
|
+
/** Test affordance: peek the OTP for an email, simulating "user reads email." */
|
|
193
|
+
peekOtpForTesting(email) {
|
|
194
|
+
return this.#pendingOtps.get(email)?.otp;
|
|
195
|
+
}
|
|
196
|
+
/** Test affordance: make a seeded identity own the session minted for its email. */
|
|
197
|
+
bindAuthUserIdForTesting(email, authUserId) {
|
|
198
|
+
this.#authUserIdByEmail.set(email, authUserId);
|
|
199
|
+
this.#allocatedAuthUserIds.add(authUserId);
|
|
200
|
+
}
|
|
201
|
+
async canSendOtp(input, options) {
|
|
202
|
+
if (options?.signal?.aborted) return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: Errors.cancelled({ operation: "canSendOtp" })
|
|
205
|
+
};
|
|
206
|
+
if (this.#failures.canSendOtp !== void 0) return {
|
|
207
|
+
ok: false,
|
|
208
|
+
error: this.#failures.canSendOtp
|
|
209
|
+
};
|
|
210
|
+
const pending = this.#pendingOtps.get(input.email);
|
|
211
|
+
if (pending === void 0) return {
|
|
212
|
+
ok: true,
|
|
213
|
+
value: {
|
|
214
|
+
allowed: true,
|
|
215
|
+
cooldownMs: toDurationMs(0)
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const nowResult = await readClockNow(this.#clock);
|
|
219
|
+
if (!nowResult.ok) return nowResult;
|
|
220
|
+
const elapsedMs = nowResult.value - pending.issuedAt;
|
|
221
|
+
const cooldownMs = Math.max(0, this.#otpTtlMs - elapsedMs);
|
|
222
|
+
if (cooldownMs === 0) {
|
|
223
|
+
this.#pendingOtps.delete(input.email);
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
value: {
|
|
227
|
+
allowed: true,
|
|
228
|
+
cooldownMs: toDurationMs(0)
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
ok: true,
|
|
234
|
+
value: {
|
|
235
|
+
allowed: false,
|
|
236
|
+
cooldownMs: toDurationMs(cooldownMs)
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async sendOtp(input, options) {
|
|
241
|
+
if (options?.signal?.aborted) return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: Errors.cancelled({ operation: "sendOtp" })
|
|
244
|
+
};
|
|
245
|
+
if (this.#failures.sendOtp !== void 0) return {
|
|
246
|
+
ok: false,
|
|
247
|
+
error: this.#failures.sendOtp
|
|
248
|
+
};
|
|
249
|
+
const issuedAt = await readClockNow(this.#clock);
|
|
250
|
+
if (!issuedAt.ok) return issuedAt;
|
|
251
|
+
const counter = this.#otpCountersByEmail.get(input.email) ?? 0;
|
|
252
|
+
this.#otpCountersByEmail.set(input.email, counter + 1);
|
|
253
|
+
const otp = counter.toString().padStart(6, "0");
|
|
254
|
+
this.#pendingOtps.set(input.email, {
|
|
255
|
+
otp,
|
|
256
|
+
issuedAt: issuedAt.value
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
value: void 0
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async verifyOtp(input, options) {
|
|
264
|
+
if (options?.signal?.aborted) return {
|
|
265
|
+
ok: false,
|
|
266
|
+
error: Errors.cancelled({ operation: "verifyOtp" })
|
|
267
|
+
};
|
|
268
|
+
if (this.#failures.verifyOtp !== void 0) return {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: this.#failures.verifyOtp
|
|
271
|
+
};
|
|
272
|
+
const pending = this.#pendingOtps.get(input.email);
|
|
273
|
+
if (pending === void 0) return {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: Errors.invalidInput("email", "no OTP issued for this email")
|
|
276
|
+
};
|
|
277
|
+
const nowResult = await readClockNow(this.#clock);
|
|
278
|
+
if (!nowResult.ok) return nowResult;
|
|
279
|
+
const now = nowResult.value;
|
|
280
|
+
if (now - pending.issuedAt > this.#otpTtlMs) {
|
|
281
|
+
this.#pendingOtps.delete(input.email);
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: Errors.otpExpired()
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (input.otp !== pending.otp) return {
|
|
288
|
+
ok: false,
|
|
289
|
+
error: Errors.invalidInput("otp", "incorrect OTP")
|
|
290
|
+
};
|
|
291
|
+
const authUserId = this.#getOrIssueAuthUserId(input.email);
|
|
292
|
+
const token = toSessionToken(`tok_${this.#tokenCounter++}`);
|
|
293
|
+
const expiresAt = toEpochMs(now + DEFAULT_SESSION_TTL_MS);
|
|
294
|
+
const session = {
|
|
295
|
+
authUserId,
|
|
296
|
+
email: input.email,
|
|
297
|
+
token,
|
|
298
|
+
expiresAt
|
|
299
|
+
};
|
|
300
|
+
const cacheWrite = await runAuthCacheEffect(this.#authCache.setSession(session), "setSession");
|
|
301
|
+
if (!cacheWrite.ok) return cacheWrite;
|
|
302
|
+
this.#pendingOtps.delete(input.email);
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
value: session
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async getSession(options) {
|
|
309
|
+
if (options?.signal?.aborted) return {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: Errors.cancelled({ operation: "getSession" })
|
|
312
|
+
};
|
|
313
|
+
if (this.#failures.getSession !== void 0) return {
|
|
314
|
+
ok: false,
|
|
315
|
+
error: this.#failures.getSession
|
|
316
|
+
};
|
|
317
|
+
const cacheRead = await runAuthCacheEffect(this.#authCache.getSession, "getSession");
|
|
318
|
+
if (!cacheRead.ok) return cacheRead;
|
|
319
|
+
const persisted = cacheRead.value;
|
|
320
|
+
if (persisted === null) return {
|
|
321
|
+
ok: true,
|
|
322
|
+
value: null
|
|
323
|
+
};
|
|
324
|
+
const nowResult = await readClockNow(this.#clock);
|
|
325
|
+
if (!nowResult.ok) return nowResult;
|
|
326
|
+
const now = nowResult.value;
|
|
327
|
+
if (persisted.expiresAt <= now) {
|
|
328
|
+
const cacheClear = await runAuthCacheEffect(this.#authCache.clearSession, "clearSession");
|
|
329
|
+
if (!cacheClear.ok) return cacheClear;
|
|
330
|
+
return {
|
|
331
|
+
ok: true,
|
|
332
|
+
value: null
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
value: persisted
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async signOut(options) {
|
|
341
|
+
if (options?.signal?.aborted) return {
|
|
342
|
+
ok: false,
|
|
343
|
+
error: Errors.cancelled({ operation: "signOut" })
|
|
344
|
+
};
|
|
345
|
+
if (this.#failures.signOut !== void 0) return {
|
|
346
|
+
ok: false,
|
|
347
|
+
error: this.#failures.signOut
|
|
348
|
+
};
|
|
349
|
+
return runAuthCacheEffect(this.#authCache.clearSession, "clearSession");
|
|
350
|
+
}
|
|
351
|
+
async getConvexJwt(options) {
|
|
352
|
+
if (options?.signal?.aborted) return {
|
|
353
|
+
ok: false,
|
|
354
|
+
error: Errors.cancelled({ operation: "getConvexJwt" })
|
|
355
|
+
};
|
|
356
|
+
if (this.#failures.getConvexJwt !== void 0) return {
|
|
357
|
+
ok: false,
|
|
358
|
+
error: this.#failures.getConvexJwt
|
|
359
|
+
};
|
|
360
|
+
if (options?.forceRefresh !== true) {
|
|
361
|
+
const cacheRead = await runAuthCacheEffect(this.#authCache.getJwt, "getJwt");
|
|
362
|
+
if (!cacheRead.ok) return cacheRead;
|
|
363
|
+
if (cacheRead.value !== null) return {
|
|
364
|
+
ok: true,
|
|
365
|
+
value: cacheRead.value
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const now = await readClockNow(this.#clock);
|
|
369
|
+
if (!now.ok) return now;
|
|
370
|
+
const jwt = {
|
|
371
|
+
token: toJwtToken(`in-memory-jwt-${this.#jwtCounter++}`),
|
|
372
|
+
expEpochSeconds: toEpochSeconds(Math.floor(now.value / 1e3 + DEFAULT_JWT_TTL_MS / 1e3))
|
|
373
|
+
};
|
|
374
|
+
const cacheWrite = await runAuthCacheEffect(this.#authCache.setJwt(jwt), "setJwt");
|
|
375
|
+
if (!cacheWrite.ok) return cacheWrite;
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
value: jwt
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
#getOrIssueAuthUserId(email) {
|
|
382
|
+
const existing = this.#authUserIdByEmail.get(email);
|
|
383
|
+
if (existing !== void 0) return existing;
|
|
384
|
+
let fresh;
|
|
385
|
+
do
|
|
386
|
+
fresh = toAuthUserId(`user_${this.#authUserIdCounter++}`);
|
|
387
|
+
while (this.#allocatedAuthUserIds.has(fresh));
|
|
388
|
+
this.#authUserIdByEmail.set(email, fresh);
|
|
389
|
+
this.#allocatedAuthUserIds.add(fresh);
|
|
390
|
+
return fresh;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
async function runAuthCacheEffect(effect, operation) {
|
|
394
|
+
const result = await Effect.runPromise(Effect.result(effect));
|
|
395
|
+
if (Result.isFailure(result)) return {
|
|
396
|
+
ok: false,
|
|
397
|
+
error: Errors.providerError("auth-cache", operation, result.failure)
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
value: result.success
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/testing/bootstrap/BootstrapStubAdapter.ts
|
|
406
|
+
var BootstrapStubAdapter = class {
|
|
407
|
+
#script;
|
|
408
|
+
constructor(deps) {
|
|
409
|
+
this.#script = deps.script;
|
|
410
|
+
}
|
|
411
|
+
resolve(input) {
|
|
412
|
+
const key = bootstrapStubScriptKey(input);
|
|
413
|
+
const failure = this.#script.failures?.get(key);
|
|
414
|
+
if (failure !== void 0) return fail$2(kindFromCapxulError(failure), failure);
|
|
415
|
+
const resolution = this.#script.resolutions?.get(key);
|
|
416
|
+
if (resolution !== void 0) return Effect.succeed(resolution);
|
|
417
|
+
const defaultFailure = this.#script.defaultFailure ?? Errors.notAuthenticated();
|
|
418
|
+
return fail$2(kindFromCapxulError(defaultFailure), defaultFailure);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
function bootstrapStubScriptKey(input) {
|
|
422
|
+
return `${String(input.publishableKey)}::${input.origin ?? ""}`;
|
|
423
|
+
}
|
|
424
|
+
function kindFromCapxulError(error) {
|
|
425
|
+
if (error.code === "NOT_AUTHENTICATED") return "notAuthenticated";
|
|
426
|
+
if (error.code === "NETWORK_ERROR") return "network";
|
|
427
|
+
if (error.code === "RATE_LIMITED") return "rateLimited";
|
|
428
|
+
if (error.code === "INVALID_INPUT") return "invalidInput";
|
|
429
|
+
return "provider";
|
|
430
|
+
}
|
|
431
|
+
function fail$2(kind, error) {
|
|
432
|
+
return Effect.fail(bootstrapErrorFromCapxul(kind, error));
|
|
433
|
+
}
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/testing/convex-call/ConvexCallStubAdapter.ts
|
|
436
|
+
var ConvexCallStubAdapter = class {
|
|
437
|
+
#script;
|
|
438
|
+
#subscribers = /* @__PURE__ */ new Map();
|
|
439
|
+
constructor(deps) {
|
|
440
|
+
this.#script = deps.script;
|
|
441
|
+
}
|
|
442
|
+
query(fn, _args) {
|
|
443
|
+
return this.#resolve(getFunctionName(fn));
|
|
444
|
+
}
|
|
445
|
+
mutation(fn, _args) {
|
|
446
|
+
return this.#resolve(getFunctionName(fn));
|
|
447
|
+
}
|
|
448
|
+
action(fn, _args) {
|
|
449
|
+
return this.#resolve(getFunctionName(fn));
|
|
450
|
+
}
|
|
451
|
+
subscribe(fn, _args, callback) {
|
|
452
|
+
return Effect.sync(() => {
|
|
453
|
+
const path = getFunctionName(fn);
|
|
454
|
+
const cb = callback;
|
|
455
|
+
cb({ status: "loading" });
|
|
456
|
+
const sequence = this.#script.subscriptions?.get(path);
|
|
457
|
+
const subscriber = {
|
|
458
|
+
callback: cb,
|
|
459
|
+
cursor: 0
|
|
460
|
+
};
|
|
461
|
+
let subs = this.#subscribers.get(path);
|
|
462
|
+
if (subs === void 0) {
|
|
463
|
+
subs = /* @__PURE__ */ new Set();
|
|
464
|
+
this.#subscribers.set(path, subs);
|
|
465
|
+
}
|
|
466
|
+
subs.add(subscriber);
|
|
467
|
+
if (sequence !== void 0 && sequence.length > 0) {
|
|
468
|
+
cb(sequence[0]);
|
|
469
|
+
subscriber.cursor = 1;
|
|
470
|
+
}
|
|
471
|
+
let active = true;
|
|
472
|
+
return () => {
|
|
473
|
+
if (!active) return;
|
|
474
|
+
active = false;
|
|
475
|
+
const liveSubs = this.#subscribers.get(path);
|
|
476
|
+
if (liveSubs === void 0) return;
|
|
477
|
+
liveSubs.delete(subscriber);
|
|
478
|
+
if (liveSubs.size === 0) this.#subscribers.delete(path);
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
advanceSubscription(path) {
|
|
483
|
+
const sequence = this.#script.subscriptions?.get(path);
|
|
484
|
+
if (sequence === void 0) return;
|
|
485
|
+
const subs = this.#subscribers.get(path);
|
|
486
|
+
if (subs === void 0) return;
|
|
487
|
+
for (const sub of subs) {
|
|
488
|
+
if (sub.cursor >= sequence.length) continue;
|
|
489
|
+
const next = sequence[sub.cursor];
|
|
490
|
+
sub.cursor += 1;
|
|
491
|
+
sub.callback(next);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
#resolve(path) {
|
|
495
|
+
const failure = this.#script.failures?.get(path);
|
|
496
|
+
if (failure !== void 0) return Effect.fail(mapFailure(path, failure));
|
|
497
|
+
if (this.#script.responses?.has(path)) return Effect.succeed(this.#script.responses.get(path));
|
|
498
|
+
return Effect.fail(convexCallErrorFromCapxul(path, Errors.providerError("convex", path, "unknown function")));
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function mapFailure(operation, failure) {
|
|
502
|
+
if (failure instanceof CapxulError) return convexCallErrorFromCapxul(operation, failure);
|
|
503
|
+
if (failure instanceof Error) return convexCallErrorFromCapxul(operation, Errors.providerError("convex", operation, failure));
|
|
504
|
+
return convexCallErrorFromCapxul(operation, Errors.providerError("convex", operation, new Error(String(failure))));
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/testing/identity/InMemoryIdentityAdapter.ts
|
|
508
|
+
var InMemoryIdentityAdapter = class {
|
|
509
|
+
#identities = /* @__PURE__ */ new Map();
|
|
510
|
+
#clock;
|
|
511
|
+
constructor(deps) {
|
|
512
|
+
this.#clock = deps.clock;
|
|
513
|
+
}
|
|
514
|
+
loadByAuthUserId(authUserId) {
|
|
515
|
+
return Effect.sync(() => cloneProfileOrNull(this.#identities.get(authUserId) ?? null));
|
|
516
|
+
}
|
|
517
|
+
create(input) {
|
|
518
|
+
const self = this;
|
|
519
|
+
return Effect.gen(function* () {
|
|
520
|
+
if (self.#identities.has(input.authUserId)) return yield* fail$1("create", Errors.invalidInput("authUserId", "already exists"));
|
|
521
|
+
const now = yield* self.#now("create.clock");
|
|
522
|
+
const profile = {
|
|
523
|
+
authUserId: input.authUserId,
|
|
524
|
+
email: input.email,
|
|
525
|
+
displayName: input.displayName ?? null,
|
|
526
|
+
country: input.country ?? null,
|
|
527
|
+
onboarded: false,
|
|
528
|
+
withdrawalAddress: null,
|
|
529
|
+
username: null,
|
|
530
|
+
imageUrl: null,
|
|
531
|
+
kycTier: toKycTier(0),
|
|
532
|
+
createdAt: now,
|
|
533
|
+
updatedAt: now
|
|
534
|
+
};
|
|
535
|
+
self.#identities.set(input.authUserId, cloneProfile(profile));
|
|
536
|
+
return cloneProfile(profile);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
update(input) {
|
|
540
|
+
const self = this;
|
|
541
|
+
return Effect.gen(function* () {
|
|
542
|
+
const existing = self.#identities.get(input.authUserId);
|
|
543
|
+
if (existing === void 0) return yield* fail$1("update", Errors.profileNotFound(input.authUserId));
|
|
544
|
+
const now = yield* self.#now("update.clock");
|
|
545
|
+
const updated = {
|
|
546
|
+
authUserId: existing.authUserId,
|
|
547
|
+
email: existing.email,
|
|
548
|
+
kycTier: existing.kycTier,
|
|
549
|
+
createdAt: existing.createdAt,
|
|
550
|
+
displayName: input.displayName !== void 0 ? input.displayName : existing.displayName,
|
|
551
|
+
country: input.country !== void 0 ? input.country : existing.country,
|
|
552
|
+
onboarded: existing.onboarded,
|
|
553
|
+
withdrawalAddress: existing.withdrawalAddress,
|
|
554
|
+
username: existing.username,
|
|
555
|
+
imageUrl: existing.imageUrl,
|
|
556
|
+
updatedAt: now
|
|
557
|
+
};
|
|
558
|
+
self.#identities.set(input.authUserId, cloneProfile(updated));
|
|
559
|
+
return cloneProfile(updated);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
completeOnboarding(input) {
|
|
563
|
+
const self = this;
|
|
564
|
+
return Effect.gen(function* () {
|
|
565
|
+
const now = yield* self.#now("completeOnboarding.clock");
|
|
566
|
+
const existing = self.#identities.get(input.authUserId);
|
|
567
|
+
const profile = {
|
|
568
|
+
authUserId: input.authUserId,
|
|
569
|
+
email: existing?.email ?? input.email,
|
|
570
|
+
displayName: input.displayName,
|
|
571
|
+
country: input.country,
|
|
572
|
+
onboarded: true,
|
|
573
|
+
withdrawalAddress: input.withdrawalAddress ?? existing?.withdrawalAddress ?? null,
|
|
574
|
+
username: existing?.username ?? null,
|
|
575
|
+
imageUrl: existing?.imageUrl ?? null,
|
|
576
|
+
kycTier: existing?.kycTier ?? toKycTier(0),
|
|
577
|
+
createdAt: existing?.createdAt ?? now,
|
|
578
|
+
updatedAt: now
|
|
579
|
+
};
|
|
580
|
+
self.#identities.set(input.authUserId, cloneProfile(profile));
|
|
581
|
+
return cloneProfile(profile);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
#now(operation) {
|
|
585
|
+
return this.#clock.now.pipe(Effect.mapError((cause) => identityErrorFromCapxul(operation, Errors.providerError("clock", "now", cause), cause)));
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
function fail$1(operation, error) {
|
|
589
|
+
return Effect.fail(identityErrorFromCapxul(operation, error));
|
|
590
|
+
}
|
|
591
|
+
function cloneProfile(profile) {
|
|
592
|
+
return {
|
|
593
|
+
authUserId: profile.authUserId,
|
|
594
|
+
email: profile.email,
|
|
595
|
+
displayName: profile.displayName,
|
|
596
|
+
country: profile.country,
|
|
597
|
+
onboarded: profile.onboarded,
|
|
598
|
+
withdrawalAddress: profile.withdrawalAddress,
|
|
599
|
+
username: profile.username,
|
|
600
|
+
imageUrl: profile.imageUrl,
|
|
601
|
+
kycTier: profile.kycTier,
|
|
602
|
+
createdAt: profile.createdAt,
|
|
603
|
+
updatedAt: profile.updatedAt
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function cloneProfileOrNull(profile) {
|
|
607
|
+
return profile === null ? null : cloneProfile(profile);
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/testing/smart-account/smart-account-derivation.ts
|
|
611
|
+
const SUPPORTED_CHAIN_IDS = new Set([84532, 8453]);
|
|
612
|
+
toAddress("0xa6b71e26c5e0845f74c812102ca7114b6a896ab2");
|
|
613
|
+
keccak256("0x");
|
|
614
|
+
function isSupportedSmartAccountChain(chainId) {
|
|
615
|
+
return SUPPORTED_CHAIN_IDS.has(wireChainId(chainId));
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/testing/smart-account/InMemorySmartAccountAdapter.ts
|
|
619
|
+
/**
|
|
620
|
+
* Hermetic backend stand-in (PRD #462 / derivation v2): `provision` derives
|
|
621
|
+
* the Safe from the identity email alone via the REAL canonical derivation
|
|
622
|
+
* (`deriveCapxulSafeAddress({ email })` — owner is the pinned bootstrap
|
|
623
|
+
* constant). No signer exists at provision; `claim` installs the signer and
|
|
624
|
+
* marks the row deployed, mirroring the backend's one-userOp claim lane.
|
|
625
|
+
*/
|
|
626
|
+
var InMemorySmartAccountAdapter = class {
|
|
627
|
+
#accounts = /* @__PURE__ */ new Map();
|
|
628
|
+
#clock;
|
|
629
|
+
#identity;
|
|
630
|
+
#failures;
|
|
631
|
+
#provisionMutex = Semaphore.makeUnsafe(1);
|
|
632
|
+
constructor(deps) {
|
|
633
|
+
this.#clock = deps.clock;
|
|
634
|
+
this.#identity = deps.identity;
|
|
635
|
+
this.#failures = deps.failures;
|
|
636
|
+
}
|
|
637
|
+
loadByAuthUserId(authUserId) {
|
|
638
|
+
const scriptedFailure = this.#failures?.loadByAuthUserId;
|
|
639
|
+
if (scriptedFailure !== void 0) return fail("loadByAuthUserId", scriptedFailure);
|
|
640
|
+
return Effect.sync(() => cloneSmartAccountOrNull(this.#accounts.get(authUserId) ?? null));
|
|
641
|
+
}
|
|
642
|
+
loadBySmartAccountAddress(address) {
|
|
643
|
+
const scriptedFailure = this.#failures?.loadBySmartAccountAddress;
|
|
644
|
+
if (scriptedFailure !== void 0) return fail("loadBySmartAccountAddress", scriptedFailure);
|
|
645
|
+
return Effect.sync(() => {
|
|
646
|
+
const needle = toAddress(address);
|
|
647
|
+
for (const account of this.#accounts.values()) if (account.smartAccountAddress === needle) return cloneSmartAccount(account);
|
|
648
|
+
return null;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
provision(input) {
|
|
652
|
+
const scriptedFailure = this.#failures?.provision;
|
|
653
|
+
if (scriptedFailure !== void 0) return fail("provision", scriptedFailure);
|
|
654
|
+
const self = this;
|
|
655
|
+
return Effect.gen(function* () {
|
|
656
|
+
if (!isSupportedSmartAccountChain(input.chainId)) return yield* fail("provision", Errors.invalidInput("chainId", `unsupported chainId ${String(input.chainId)}`));
|
|
657
|
+
const identity = yield* self.#identity.loadByAuthUserId(input.authUserId).pipe(Effect.mapError((error) => smartAccountErrorFromCapxul("provision.identity", error.publicError, error)));
|
|
658
|
+
if (identity === null) return yield* fail("provision", Errors.profileNotFound(String(input.authUserId)));
|
|
659
|
+
const existing = self.#accounts.get(input.authUserId);
|
|
660
|
+
if (existing !== void 0) return cloneSmartAccount(existing);
|
|
661
|
+
const now = yield* self.#now("provision.clock");
|
|
662
|
+
const account = {
|
|
663
|
+
authUserId: input.authUserId,
|
|
664
|
+
signerAddress: null,
|
|
665
|
+
smartAccountAddress: toAddress(deriveCapxulSafeAddress({ email: identity.email })),
|
|
666
|
+
chainId: input.chainId,
|
|
667
|
+
deployedAt: null,
|
|
668
|
+
claimedAt: null,
|
|
669
|
+
createdAt: now
|
|
670
|
+
};
|
|
671
|
+
self.#accounts.set(input.authUserId, cloneSmartAccount(account));
|
|
672
|
+
return cloneSmartAccount(account);
|
|
673
|
+
}).pipe(self.#provisionMutex.withPermits(1));
|
|
674
|
+
}
|
|
675
|
+
promoteDeployedAt(authUserId, deployedAt) {
|
|
676
|
+
const existing = this.#accounts.get(authUserId);
|
|
677
|
+
if (existing === void 0) return;
|
|
678
|
+
if (existing.deployedAt !== null) return;
|
|
679
|
+
this.#accounts.set(authUserId, {
|
|
680
|
+
...existing,
|
|
681
|
+
deployedAt
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
confirmDeployment(input) {
|
|
685
|
+
const self = this;
|
|
686
|
+
return Effect.gen(function* () {
|
|
687
|
+
const existing = self.#accounts.get(input.authUserId);
|
|
688
|
+
if (existing === void 0) return yield* fail("confirmDeployment", Errors.profileNotFound(String(input.authUserId)));
|
|
689
|
+
if (existing.smartAccountAddress !== toAddress(input.safeAddress)) return yield* fail("confirmDeployment", Errors.invalidInput("safeAddress", "does not match backend smart-account row"));
|
|
690
|
+
if (existing.deployedAt !== null) return cloneSmartAccount(existing);
|
|
691
|
+
const now = yield* self.#now("confirmDeployment.clock");
|
|
692
|
+
const updated = {
|
|
693
|
+
...existing,
|
|
694
|
+
deployedAt: now
|
|
695
|
+
};
|
|
696
|
+
self.#accounts.set(input.authUserId, cloneSmartAccount(updated));
|
|
697
|
+
return cloneSmartAccount(updated);
|
|
698
|
+
}).pipe(self.#provisionMutex.withPermits(1));
|
|
699
|
+
}
|
|
700
|
+
claim(input) {
|
|
701
|
+
const scriptedFailure = this.#failures?.claim;
|
|
702
|
+
if (scriptedFailure !== void 0) return fail("claim", scriptedFailure);
|
|
703
|
+
const self = this;
|
|
704
|
+
return Effect.gen(function* () {
|
|
705
|
+
const existing = self.#accounts.get(input.authUserId);
|
|
706
|
+
if (existing === void 0) return yield* fail("claim", Errors.invalidInput("account", "no provisioned smart account for this user"));
|
|
707
|
+
if (existing.chainId !== input.chainId) return yield* fail("claim", Errors.invalidInput("chainId", "does not match backend smart-account row"));
|
|
708
|
+
const newOwner = toAddress(input.signerAddress);
|
|
709
|
+
if (existing.claimedAt !== null) {
|
|
710
|
+
if (existing.signerAddress !== newOwner) return yield* fail("claim", Errors.invalidInput("signerAddress", "account already claimed by a different signer"));
|
|
711
|
+
return cloneSmartAccount(existing);
|
|
712
|
+
}
|
|
713
|
+
const now = yield* self.#now("claim.clock");
|
|
714
|
+
const updated = {
|
|
715
|
+
...existing,
|
|
716
|
+
signerAddress: newOwner,
|
|
717
|
+
claimedAt: now,
|
|
718
|
+
deployedAt: existing.deployedAt ?? now
|
|
719
|
+
};
|
|
720
|
+
self.#accounts.set(input.authUserId, cloneSmartAccount(updated));
|
|
721
|
+
return cloneSmartAccount(updated);
|
|
722
|
+
}).pipe(self.#provisionMutex.withPermits(1));
|
|
723
|
+
}
|
|
724
|
+
#now(operation) {
|
|
725
|
+
return this.#clock.now.pipe(Effect.mapError((cause) => smartAccountErrorFromCapxul(operation, Errors.providerError("clock", "now", cause), cause)));
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
function fail(operation, error) {
|
|
729
|
+
return Effect.fail(smartAccountErrorFromCapxul(operation, error));
|
|
730
|
+
}
|
|
731
|
+
function cloneSmartAccount(account) {
|
|
732
|
+
return {
|
|
733
|
+
authUserId: account.authUserId,
|
|
734
|
+
signerAddress: account.signerAddress,
|
|
735
|
+
smartAccountAddress: account.smartAccountAddress,
|
|
736
|
+
chainId: account.chainId,
|
|
737
|
+
deployedAt: account.deployedAt,
|
|
738
|
+
claimedAt: account.claimedAt,
|
|
739
|
+
createdAt: account.createdAt
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function cloneSmartAccountOrNull(account) {
|
|
743
|
+
return account === null ? null : cloneSmartAccount(account);
|
|
744
|
+
}
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/testing/sub-account/InMemorySubAccountAdapter.ts
|
|
747
|
+
var InMemorySubAccountAdapter = class {
|
|
748
|
+
#rows = /* @__PURE__ */ new Map();
|
|
749
|
+
#failures;
|
|
750
|
+
#balanceOfRaw;
|
|
751
|
+
#created;
|
|
752
|
+
constructor(deps = {}) {
|
|
753
|
+
this.#failures = deps.failures ?? {};
|
|
754
|
+
this.#balanceOfRaw = deps.balanceOfRaw ?? "0";
|
|
755
|
+
for (const row of deps.seed ?? []) this.#rows.set(row.id, row);
|
|
756
|
+
this.#created = this.#rows.size;
|
|
757
|
+
}
|
|
758
|
+
create(input) {
|
|
759
|
+
const failure = this.#failures.create;
|
|
760
|
+
if (failure !== void 0) return Effect.fail(subAccountErrorFromCapxul("create", failure));
|
|
761
|
+
this.#created += 1;
|
|
762
|
+
const id = toSubAccountId(`subaccount_test${String(this.#created)}`);
|
|
763
|
+
const row = {
|
|
764
|
+
id,
|
|
765
|
+
accountId: input.accountId,
|
|
766
|
+
name: input.name,
|
|
767
|
+
balance: {
|
|
768
|
+
currency: toCurrencyCode("USD"),
|
|
769
|
+
value: "0",
|
|
770
|
+
decimals: 6
|
|
771
|
+
},
|
|
772
|
+
createdAt: toEpochMs(Date.now())
|
|
773
|
+
};
|
|
774
|
+
this.#rows.set(id, row);
|
|
775
|
+
return Effect.succeed(row);
|
|
776
|
+
}
|
|
777
|
+
get(input) {
|
|
778
|
+
return Effect.succeed(this.#rows.get(input.subAccountId) ?? null);
|
|
779
|
+
}
|
|
780
|
+
list(input) {
|
|
781
|
+
const accountId = input.accountId;
|
|
782
|
+
return Effect.succeed([...this.#rows.values()].filter((row) => row.accountId === accountId));
|
|
783
|
+
}
|
|
784
|
+
rename(input) {
|
|
785
|
+
const existing = this.#rows.get(input.subAccountId);
|
|
786
|
+
if (existing === void 0) return Effect.fail(subAccountErrorFromCapxul("rename", Errors.accountNotFound(input.subAccountId)));
|
|
787
|
+
const next = {
|
|
788
|
+
...existing,
|
|
789
|
+
name: input.name
|
|
790
|
+
};
|
|
791
|
+
this.#rows.set(input.subAccountId, next);
|
|
792
|
+
return Effect.succeed(next);
|
|
793
|
+
}
|
|
794
|
+
delete(input) {
|
|
795
|
+
const existing = this.#rows.get(input.subAccountId);
|
|
796
|
+
if (existing === void 0) return Effect.fail(subAccountErrorFromCapxul("delete", Errors.accountNotFound(input.subAccountId)));
|
|
797
|
+
if (Number(existing.balance.value) !== 0) return Effect.fail(subAccountErrorFromCapxul("delete", Errors.invalidInput("subAccountId", "delete requires zero balance")));
|
|
798
|
+
this.#rows.delete(input.subAccountId);
|
|
799
|
+
return Effect.succeed(void 0);
|
|
800
|
+
}
|
|
801
|
+
transfer(input) {
|
|
802
|
+
const decimals = input.amount.decimals;
|
|
803
|
+
const invalid = (field, reason) => Effect.fail(subAccountErrorFromCapxul("transfer", Errors.invalidInput(field, reason)));
|
|
804
|
+
let rawAmount;
|
|
805
|
+
try {
|
|
806
|
+
rawAmount = BigInt(toWei(input.amount));
|
|
807
|
+
} catch {
|
|
808
|
+
return invalid("amount", "must be a decimal money string");
|
|
809
|
+
}
|
|
810
|
+
if (rawAmount <= 0n) return invalid("amount", "must be positive");
|
|
811
|
+
if (input.from === input.to) return invalid("transfer", "from and to must differ");
|
|
812
|
+
const fromRow = input.from === "main" ? null : this.#rows.get(input.from) ?? "missing";
|
|
813
|
+
if (fromRow === "missing") return invalid("subAccountId", "unknown sub-account");
|
|
814
|
+
const toRow = input.to === "main" ? null : this.#rows.get(input.to) ?? "missing";
|
|
815
|
+
if (toRow === "missing") return invalid("subAccountId", "unknown sub-account");
|
|
816
|
+
const safeRaw = BigInt(this.#balanceOfRaw);
|
|
817
|
+
const subTotalRaw = this.#sumSubAccountRaws(decimals);
|
|
818
|
+
if (input.from === "main") {
|
|
819
|
+
const availableRaw = safeRaw - subTotalRaw;
|
|
820
|
+
if (availableRaw < rawAmount) return Effect.fail(subAccountErrorFromCapxul("transfer", Errors.insufficientBalance("main", maxRaw(availableRaw), rawAmount.toString(10))));
|
|
821
|
+
} else if (fromRow !== null) {
|
|
822
|
+
const fromRaw = BigInt(toWei({
|
|
823
|
+
value: fromRow.balance.value,
|
|
824
|
+
decimals
|
|
825
|
+
}));
|
|
826
|
+
if (fromRaw < rawAmount) return Effect.fail(subAccountErrorFromCapxul("transfer", Errors.insufficientBalance(fromRow.name, fromRaw.toString(10), rawAmount.toString(10))));
|
|
827
|
+
}
|
|
828
|
+
let nextFrom = null;
|
|
829
|
+
let nextTo = null;
|
|
830
|
+
if (fromRow !== null) {
|
|
831
|
+
const fromRaw = BigInt(toWei({
|
|
832
|
+
value: fromRow.balance.value,
|
|
833
|
+
decimals
|
|
834
|
+
}));
|
|
835
|
+
nextFrom = this.#patchBalance(fromRow, (fromRaw - rawAmount).toString(10), decimals);
|
|
836
|
+
}
|
|
837
|
+
if (toRow !== null) {
|
|
838
|
+
const toRaw = BigInt(toWei({
|
|
839
|
+
value: toRow.balance.value,
|
|
840
|
+
decimals
|
|
841
|
+
}));
|
|
842
|
+
nextTo = this.#patchBalance(toRow, (toRaw + rawAmount).toString(10), decimals);
|
|
843
|
+
}
|
|
844
|
+
const available = fromWei(maxRaw(safeRaw - this.#sumSubAccountRaws(decimals)), decimals, input.amount.currency);
|
|
845
|
+
return Effect.succeed({
|
|
846
|
+
available,
|
|
847
|
+
from: nextFrom,
|
|
848
|
+
to: nextTo
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
#sumSubAccountRaws(decimals) {
|
|
852
|
+
let total = 0n;
|
|
853
|
+
for (const row of this.#rows.values()) total += BigInt(toWei({
|
|
854
|
+
value: row.balance.value,
|
|
855
|
+
decimals
|
|
856
|
+
}));
|
|
857
|
+
return total;
|
|
858
|
+
}
|
|
859
|
+
#patchBalance(row, rawBalance, decimals) {
|
|
860
|
+
const balance = fromWei(rawBalance, decimals, row.balance.currency);
|
|
861
|
+
const next = {
|
|
862
|
+
...row,
|
|
863
|
+
balance
|
|
864
|
+
};
|
|
865
|
+
this.#rows.set(row.id, next);
|
|
866
|
+
return next;
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
/** Floor a (possibly negative) base-unit raw to a non-negative base-10 string. */
|
|
870
|
+
function maxRaw(raw) {
|
|
871
|
+
return (raw < 0n ? 0n : raw).toString(10);
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/testing/telemetry/RecordingTelemetryAdapter.ts
|
|
875
|
+
var RecordingTelemetryAdapter = class {
|
|
876
|
+
#events = [];
|
|
877
|
+
#operations = [];
|
|
878
|
+
#rawMode;
|
|
879
|
+
constructor(options = {}) {
|
|
880
|
+
this.#rawMode = options.rawMode === true;
|
|
881
|
+
}
|
|
882
|
+
get events() {
|
|
883
|
+
return this.#events.map(cloneEvent);
|
|
884
|
+
}
|
|
885
|
+
get operations() {
|
|
886
|
+
return this.#operations.map(cloneOperation);
|
|
887
|
+
}
|
|
888
|
+
emit(event) {
|
|
889
|
+
return Effect.sync(() => {
|
|
890
|
+
const recorded = redactTelemetryEvent(event, { rawMode: this.#rawMode });
|
|
891
|
+
this.#events.push(recorded);
|
|
892
|
+
this.#operations.push({
|
|
893
|
+
type: "emit",
|
|
894
|
+
event: cloneEvent(recorded)
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
identify(input) {
|
|
899
|
+
return Effect.sync(() => {
|
|
900
|
+
this.#operations.push({
|
|
901
|
+
type: "identify",
|
|
902
|
+
input: cloneIdentifyInput(input)
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
group(input) {
|
|
907
|
+
return Effect.sync(() => {
|
|
908
|
+
this.#operations.push({
|
|
909
|
+
type: "group",
|
|
910
|
+
input: cloneGroupInput(input)
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
reset() {
|
|
915
|
+
return Effect.sync(() => {
|
|
916
|
+
this.#operations.push({ type: "reset" });
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
clear() {
|
|
920
|
+
this.#events.length = 0;
|
|
921
|
+
this.#operations.length = 0;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
function cloneEvent(event) {
|
|
925
|
+
return event.props === void 0 ? { name: event.name } : {
|
|
926
|
+
name: event.name,
|
|
927
|
+
props: cloneProps(event.props)
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function cloneOperation(operation) {
|
|
931
|
+
switch (operation.type) {
|
|
932
|
+
case "emit": return {
|
|
933
|
+
type: "emit",
|
|
934
|
+
event: cloneEvent(operation.event)
|
|
935
|
+
};
|
|
936
|
+
case "identify": return {
|
|
937
|
+
type: "identify",
|
|
938
|
+
input: cloneIdentifyInput(operation.input)
|
|
939
|
+
};
|
|
940
|
+
case "group": return {
|
|
941
|
+
type: "group",
|
|
942
|
+
input: cloneGroupInput(operation.input)
|
|
943
|
+
};
|
|
944
|
+
case "reset": return { type: "reset" };
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function cloneIdentifyInput(input) {
|
|
948
|
+
return {
|
|
949
|
+
distinctId: input.distinctId,
|
|
950
|
+
...input.anonDistinctId === void 0 ? {} : { anonDistinctId: input.anonDistinctId },
|
|
951
|
+
...input.traits === void 0 ? {} : { traits: cloneProps(input.traits) },
|
|
952
|
+
...input.properties === void 0 ? {} : { properties: cloneProps(input.properties) }
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function cloneGroupInput(input) {
|
|
956
|
+
return input.properties === void 0 ? {
|
|
957
|
+
groupType: input.groupType,
|
|
958
|
+
groupKey: input.groupKey
|
|
959
|
+
} : {
|
|
960
|
+
groupType: input.groupType,
|
|
961
|
+
groupKey: input.groupKey,
|
|
962
|
+
properties: cloneProps(input.properties)
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function cloneProps(props) {
|
|
966
|
+
const cloned = {};
|
|
967
|
+
for (const [key, value] of Object.entries(props)) cloned[key] = cloneTelemetryValue(value);
|
|
968
|
+
return cloned;
|
|
969
|
+
}
|
|
970
|
+
function cloneTelemetryValue(value) {
|
|
971
|
+
if (Array.isArray(value)) return value.map(cloneTelemetryValue);
|
|
972
|
+
if (value === null || typeof value !== "object") return value;
|
|
973
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return value;
|
|
974
|
+
const cloned = {};
|
|
975
|
+
for (const [key, nested] of Object.entries(value)) cloned[key] = cloneTelemetryValue(nested);
|
|
976
|
+
return cloned;
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/testing/index.ts
|
|
980
|
+
const TEST_BOOTSTRAP = {
|
|
981
|
+
applicationId: toAppId("app_01HX0000000000000000000000"),
|
|
982
|
+
chainId: toChainId(84532),
|
|
983
|
+
sessionToken: toSessionToken("bootstrap-test-token"),
|
|
984
|
+
issuedAt: toEpochMs(0),
|
|
985
|
+
expiresIn: toDurationMs(6e4),
|
|
986
|
+
authBaseUrl: "https://auth.example.test",
|
|
987
|
+
convexUrl: "https://convex.example.test",
|
|
988
|
+
siteBaseUrl: "https://example.test",
|
|
989
|
+
openfortPublishableKey: "pk_test_openfort_fixture",
|
|
990
|
+
shieldPublishableKey: "shield_pk_test_fixture"
|
|
991
|
+
};
|
|
992
|
+
/**
|
|
993
|
+
* Assemble the real public client over deterministic in-memory ports.
|
|
994
|
+
* Unconfigured backend calls fail locally; no adapter owns a network exit.
|
|
995
|
+
*/
|
|
996
|
+
function createCapxulTestClient(options = {}) {
|
|
997
|
+
const clock = new ManualClockAdapter({});
|
|
998
|
+
const authCache = new InMemoryAuthCacheAdapter();
|
|
999
|
+
const authClient = new InMemoryAuthClientAdapter({
|
|
1000
|
+
authCache,
|
|
1001
|
+
clock
|
|
1002
|
+
});
|
|
1003
|
+
const identity = new InMemoryIdentityAdapter({ clock });
|
|
1004
|
+
const telemetry = new RecordingTelemetryAdapter();
|
|
1005
|
+
const client = assembleCapxulClient({
|
|
1006
|
+
ports: {
|
|
1007
|
+
authClient: authClientPortFromPromiseAdapter(authClient),
|
|
1008
|
+
authCache,
|
|
1009
|
+
identity,
|
|
1010
|
+
smartAccount: new InMemorySmartAccountAdapter({
|
|
1011
|
+
clock,
|
|
1012
|
+
identity
|
|
1013
|
+
}),
|
|
1014
|
+
accountRead: new InMemoryAccountReadAdapter({ rawBalance: "0" }),
|
|
1015
|
+
subAccount: new InMemorySubAccountAdapter(),
|
|
1016
|
+
bootstrap: new BootstrapStubAdapter({ script: { resolutions: new Map([[`${toPublishableKey("cap_pk_test_0123456789ABCDEFGHJKMNPQRSTVWXYZ")}::${toAllowedOrigin("https://example.test")}`, TEST_BOOTSTRAP]]) } }),
|
|
1017
|
+
clock,
|
|
1018
|
+
telemetry,
|
|
1019
|
+
convexCall: new ConvexCallStubAdapter({ script: {} })
|
|
1020
|
+
},
|
|
1021
|
+
bootstrap: TEST_BOOTSTRAP,
|
|
1022
|
+
authCache,
|
|
1023
|
+
requirement: options.requirement ?? "none"
|
|
1024
|
+
});
|
|
1025
|
+
const transitions = [];
|
|
1026
|
+
const unsubscribe = client._internal.identity.subscribeTransitions((record) => {
|
|
1027
|
+
transitions.push(record);
|
|
1028
|
+
});
|
|
1029
|
+
let closed = false;
|
|
1030
|
+
return {
|
|
1031
|
+
client,
|
|
1032
|
+
clock: Object.freeze({ advance: (ms) => clock.advance(ms) }),
|
|
1033
|
+
observation: {
|
|
1034
|
+
emit: (event) => Effect.runPromise(telemetry.emit(event)),
|
|
1035
|
+
identify: (input) => Effect.runPromise(telemetry.identify(input)),
|
|
1036
|
+
group: (input) => Effect.runPromise(telemetry.group(input)),
|
|
1037
|
+
reset: () => Effect.runPromise(telemetry.reset())
|
|
1038
|
+
},
|
|
1039
|
+
get transitions() {
|
|
1040
|
+
return transitions.slice();
|
|
1041
|
+
},
|
|
1042
|
+
get observations() {
|
|
1043
|
+
return telemetry.operations;
|
|
1044
|
+
},
|
|
1045
|
+
peekOtp: (email) => authClient.peekOtpForTesting(toEmail(email)),
|
|
1046
|
+
seedIdentity: async (input) => {
|
|
1047
|
+
const authUserId = toAuthUserId(input.authUserId);
|
|
1048
|
+
const email = toEmail(input.email);
|
|
1049
|
+
await Effect.runPromise(identity.create({
|
|
1050
|
+
authUserId,
|
|
1051
|
+
email,
|
|
1052
|
+
...input.displayName === void 0 ? {} : { displayName: input.displayName },
|
|
1053
|
+
...input.country === void 0 ? {} : { country: toCountryCode(input.country) }
|
|
1054
|
+
}));
|
|
1055
|
+
authClient.bindAuthUserIdForTesting(email, authUserId);
|
|
1056
|
+
},
|
|
1057
|
+
close: async () => {
|
|
1058
|
+
if (closed) return;
|
|
1059
|
+
closed = true;
|
|
1060
|
+
unsubscribe();
|
|
1061
|
+
await client._internal.close?.();
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
//#endregion
|
|
1066
|
+
export { createCapxulTestClient };
|
|
1067
|
+
|
|
1068
|
+
//# sourceMappingURL=index.mjs.map
|