@myzonerocks/pact 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/src/adapter.d.ts +18 -0
  2. package/dist/src/adapter.js +11 -0
  3. package/dist/src/adapters/erc20.d.ts +43 -0
  4. package/dist/src/adapters/erc20.js +126 -0
  5. package/dist/src/adapters/mpesa.d.ts +61 -0
  6. package/dist/src/adapters/mpesa.js +272 -0
  7. package/dist/src/adapters/paypal.d.ts +68 -0
  8. package/dist/src/adapters/paypal.js +279 -0
  9. package/dist/src/adapters/stripe.d.ts +61 -0
  10. package/dist/src/adapters/stripe.js +336 -0
  11. package/dist/src/bridge.d.ts +40 -0
  12. package/dist/src/bridge.js +99 -0
  13. package/dist/src/canonical.d.ts +14 -0
  14. package/dist/src/canonical.js +82 -0
  15. package/dist/src/client.d.ts +84 -0
  16. package/dist/src/client.js +510 -0
  17. package/dist/src/compliance.d.ts +23 -0
  18. package/dist/src/compliance.js +26 -0
  19. package/dist/src/crypto.d.ts +12 -0
  20. package/dist/src/crypto.js +50 -0
  21. package/dist/src/fake.d.ts +29 -0
  22. package/dist/src/fake.js +79 -0
  23. package/dist/src/index.d.ts +16 -0
  24. package/dist/src/index.js +16 -0
  25. package/dist/src/ledger.d.ts +47 -0
  26. package/dist/src/ledger.js +84 -0
  27. package/dist/src/leg.d.ts +34 -0
  28. package/dist/src/leg.js +4 -0
  29. package/dist/src/message.d.ts +55 -0
  30. package/dist/src/message.js +88 -0
  31. package/dist/src/money.d.ts +16 -0
  32. package/dist/src/money.js +81 -0
  33. package/dist/src/payload.d.ts +11 -0
  34. package/dist/src/payload.js +52 -0
  35. package/dist/src/policy.d.ts +26 -0
  36. package/dist/src/policy.js +83 -0
  37. package/dist/src/protocol.d.ts +4 -0
  38. package/dist/src/protocol.js +15 -0
  39. package/dist/src/router.d.ts +16 -0
  40. package/dist/src/router.js +87 -0
  41. package/dist/src/signing.d.ts +27 -0
  42. package/dist/src/signing.js +72 -0
  43. package/dist/src/state.d.ts +23 -0
  44. package/dist/src/state.js +76 -0
  45. package/dist/src/wire.d.ts +30 -0
  46. package/dist/src/wire.js +221 -0
  47. package/dist/test/erc20.test.d.ts +1 -0
  48. package/dist/test/erc20.test.js +131 -0
  49. package/dist/test/lifecycle.test.d.ts +1 -0
  50. package/dist/test/lifecycle.test.js +212 -0
  51. package/dist/test/mpesa.test.d.ts +1 -0
  52. package/dist/test/mpesa.test.js +180 -0
  53. package/dist/test/payload.test.d.ts +1 -0
  54. package/dist/test/payload.test.js +32 -0
  55. package/dist/test/paypal.test.d.ts +1 -0
  56. package/dist/test/paypal.test.js +140 -0
  57. package/dist/test/policy.test.d.ts +1 -0
  58. package/dist/test/policy.test.js +131 -0
  59. package/dist/test/stripe.test.d.ts +1 -0
  60. package/dist/test/stripe.test.js +176 -0
  61. package/dist/test/vectors.test.d.ts +1 -0
  62. package/dist/test/vectors.test.js +91 -0
  63. package/dist/test/wire.test.d.ts +1 -0
  64. package/dist/test/wire.test.js +104 -0
  65. package/package.json +50 -0
  66. package/src/adapter.ts +32 -0
  67. package/src/adapters/erc20.ts +181 -0
  68. package/src/adapters/mpesa.ts +408 -0
  69. package/src/adapters/paypal.ts +409 -0
  70. package/src/adapters/stripe.ts +478 -0
  71. package/src/bridge.ts +148 -0
  72. package/src/canonical.ts +94 -0
  73. package/src/client.ts +605 -0
  74. package/src/compliance.ts +65 -0
  75. package/src/crypto.ts +68 -0
  76. package/src/fake.ts +96 -0
  77. package/src/index.ts +106 -0
  78. package/src/ledger.ts +145 -0
  79. package/src/leg.ts +65 -0
  80. package/src/message.ts +178 -0
  81. package/src/money.ts +87 -0
  82. package/src/payload.ts +58 -0
  83. package/src/policy.ts +110 -0
  84. package/src/protocol.ts +19 -0
  85. package/src/router.ts +97 -0
  86. package/src/signing.ts +92 -0
  87. package/src/state.ts +76 -0
  88. package/src/wire.ts +248 -0
@@ -0,0 +1,510 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { State } from "./state.js";
3
+ import { Money } from "./money.js";
4
+ import { DEFAULT_SKEW_MILLIS, domain } from "./protocol.js";
5
+ import { CanonicalWriter, hashPreimage } from "./canonical.js";
6
+ import { intentHash, quoteHash, authorizationHash, corridorReceipt, isDirect, BRIDGE_PASSTHROUGH, } from "./message.js";
7
+ import { authorize as signAuthorization, verifyAuthorization } from "./signing.js";
8
+ import { PassThroughBridge } from "./bridge.js";
9
+ import { railInList } from "./leg.js";
10
+ import { route, PolicyKind, NoQuoteError, isExpired } from "./router.js";
11
+ import { permissiveKyc, allowRisk, withinLimits } from "./compliance.js";
12
+ import { MemoryLedger, eventReceipt } from "./ledger.js";
13
+ export function systemClock() {
14
+ return Date.now();
15
+ }
16
+ export function randomId() {
17
+ return Buffer.from(randomBytes(16)).toString("hex");
18
+ }
19
+ // Client is the corridor orchestrator a host embeds. It mints intents, composes
20
+ // corridor quotes across legs and bridges, authorizes with the host's signer,
21
+ // and runs collect -> convert -> disburse, folding each step into the ledger. It
22
+ // never holds funds and never touches transport.
23
+ export class Client {
24
+ payIn = new Map();
25
+ payOut = new Map();
26
+ bridges = new Map();
27
+ ledger;
28
+ policy;
29
+ verifier;
30
+ kyc;
31
+ risk;
32
+ clock;
33
+ idGen;
34
+ skew;
35
+ escrowRef;
36
+ subscribers = [];
37
+ constructor(cfg) {
38
+ for (const l of cfg.payIn ?? [])
39
+ this.payIn.set(l.id, l);
40
+ for (const l of cfg.payOut ?? [])
41
+ this.payOut.set(l.id, l);
42
+ for (const b of cfg.bridges ?? [])
43
+ this.bridges.set(b.id, b);
44
+ if (this.bridges.size === 0) {
45
+ this.bridges.set(BRIDGE_PASSTHROUGH, new PassThroughBridge());
46
+ }
47
+ this.ledger = cfg.ledger ?? new MemoryLedger();
48
+ this.policy = cfg.policy ?? { kind: PolicyKind.Cheapest };
49
+ this.verifier = cfg.verifier;
50
+ this.kyc = cfg.kyc ?? permissiveKyc;
51
+ this.risk = cfg.risk ?? allowRisk;
52
+ this.clock = cfg.clock ?? systemClock;
53
+ this.idGen = cfg.idGen ?? randomId;
54
+ this.skew = cfg.skew ?? DEFAULT_SKEW_MILLIS;
55
+ this.escrowRef = cfg.escrowRef ?? "";
56
+ }
57
+ // createIntent mints an intent and records it as a draft.
58
+ createIntent(spec) {
59
+ const intent = {
60
+ id: this.idGen(),
61
+ senderRef: spec.senderRef,
62
+ recipientRef: spec.recipientRef,
63
+ amount: spec.amount,
64
+ memo: spec.memo ?? "",
65
+ expiresAt: spec.expiresAt,
66
+ allowedRails: spec.allowedRails ?? [],
67
+ metadata: spec.metadata ?? {},
68
+ };
69
+ this.ledger.apply({ intentId: intent.id, to: State.Draft, payloadHash: intentHash(intent), intent });
70
+ return intent;
71
+ }
72
+ // quoteOptions composes a corridor quote for each way the payer offered to fund
73
+ // the payment. For every funding option it finds a bridge that can reach the
74
+ // recipient's currency and a pay-out leg that can deliver it, prices the whole
75
+ // corridor, and returns the options for the payer to choose among.
76
+ async quoteOptions(intent, funding) {
77
+ const quotes = [];
78
+ let lastErr;
79
+ for (const f of funding) {
80
+ const payInLeg = this.payIn.get(f.payInAdapterId);
81
+ if (!payInLeg) {
82
+ lastErr = new Error(`pact: no pay-in leg ${JSON.stringify(f.payInAdapterId)}`);
83
+ continue;
84
+ }
85
+ if (!containsOrEmpty(payInLeg.payInCapabilities().currencies, f.currency)) {
86
+ lastErr = new Error(`pact: pay-in leg ${JSON.stringify(f.payInAdapterId)} does not fund ${f.currency}`);
87
+ continue;
88
+ }
89
+ try {
90
+ quotes.push(await this.composeQuote(intent, payInLeg, f));
91
+ }
92
+ catch (err) {
93
+ lastErr = err;
94
+ }
95
+ }
96
+ if (quotes.length === 0) {
97
+ if (lastErr instanceof Error)
98
+ throw lastErr;
99
+ throw new NoQuoteError("no fundable corridor");
100
+ }
101
+ return quotes;
102
+ }
103
+ // composeQuote prices one corridor: a pay-out leg delivers the recipient's
104
+ // currency, a bridge plans the source needed from the payer's currency, and the
105
+ // leg and bridge fees are summed into what the payer pays.
106
+ async composeQuote(intent, payInLeg, f) {
107
+ const payOutLeg = this.pickPayOut(intent);
108
+ const bridge = this.pickBridge(f.currency, intent.amount.currency);
109
+ const bridged = await bridge.quote(intent.amount, f.currency, f.exponent);
110
+ const src = bridged.srcAmount.add(bridged.fee);
111
+ return {
112
+ id: this.idGen(),
113
+ intentId: intent.id,
114
+ payInAdapterId: payInLeg.id,
115
+ payInRail: firstRail(payInLeg.payInCapabilities().rails),
116
+ payOutAdapterId: payOutLeg.id,
117
+ payOutRail: this.payOutRail(payOutLeg, intent),
118
+ bridgeId: bridge.id,
119
+ srcAmount: src,
120
+ dstAmount: intent.amount,
121
+ fees: bridged.fee,
122
+ fxRate: bridged.fxRate,
123
+ expiresAt: intent.expiresAt,
124
+ providerQuoteRef: "",
125
+ latencyEstimateMs: 0,
126
+ };
127
+ }
128
+ // pickPayOut finds a pay-out leg that delivers the recipient's currency on an
129
+ // allowed rail.
130
+ pickPayOut(intent) {
131
+ for (const leg of this.payOut.values()) {
132
+ const caps = leg.payOutCapabilities();
133
+ if (!containsOrEmpty(caps.currencies, intent.amount.currency))
134
+ continue;
135
+ if (this.payOutRail(leg, intent) !== "")
136
+ return leg;
137
+ }
138
+ throw new NoQuoteError("no pay-out leg for the recipient's currency and rails");
139
+ }
140
+ // payOutRail returns the rail this leg would use for the intent, or empty if
141
+ // none of the intent's allowed rails are served.
142
+ payOutRail(leg, intent) {
143
+ const caps = leg.payOutCapabilities();
144
+ if (intent.allowedRails.length === 0)
145
+ return firstRail(caps.rails);
146
+ for (const rail of intent.allowedRails) {
147
+ if (railInList(caps.rails, rail))
148
+ return rail;
149
+ }
150
+ return "";
151
+ }
152
+ // pickBridge selects a bridge that converts the payer's currency to the
153
+ // recipient's. Same-currency prefers pass-through; otherwise a converting
154
+ // bridge.
155
+ pickBridge(srcCurrency, dstCurrency) {
156
+ if (srcCurrency === dstCurrency) {
157
+ const passThrough = this.bridges.get(BRIDGE_PASSTHROUGH);
158
+ if (passThrough)
159
+ return passThrough;
160
+ }
161
+ for (const b of this.bridges.values()) {
162
+ if (b.id !== BRIDGE_PASSTHROUGH)
163
+ return b;
164
+ }
165
+ const passThrough = this.bridges.get(BRIDGE_PASSTHROUGH);
166
+ if (passThrough && srcCurrency === dstCurrency)
167
+ return passThrough;
168
+ throw new NoQuoteError(`no bridge from ${srcCurrency} to ${dstCurrency}`);
169
+ }
170
+ // select applies the configured policy to a set of corridor quotes.
171
+ async select(quotes, identity) {
172
+ const status = await this.kyc.status(identity);
173
+ return route(quotes, this.policy, status, this.clock(), this.skew);
174
+ }
175
+ // authorize records the chosen corridor, runs the risk hook, signs the sender's
176
+ // commitment, and records the authorization, advancing draft -> quoted ->
177
+ // authorized.
178
+ async authorize(intent, quote, signer) {
179
+ if (quote.intentId !== intent.id) {
180
+ throw new Error("pact: quote does not belong to intent");
181
+ }
182
+ const now = this.clock();
183
+ if (isExpired(intent.expiresAt, now, this.skew)) {
184
+ throw new Error("pact: intent has expired");
185
+ }
186
+ if (isExpired(quote.expiresAt, now, this.skew)) {
187
+ throw new Error("pact: quote has expired");
188
+ }
189
+ const status = await this.kyc.status(signer.identity());
190
+ if (!withinLimits(status, quote)) {
191
+ throw new Error("pact: quote exceeds KYC limit");
192
+ }
193
+ const decision = this.risk.evaluate(signer.identity(), intent, quote);
194
+ if (!decision.allow) {
195
+ throw new Error(`pact: risk hook vetoed authorization: ${decision.reason}`);
196
+ }
197
+ this.ledger.apply({ intentId: intent.id, to: State.Quoted, payloadHash: quoteHash(quote), quote });
198
+ const auth = signAuthorization(intent, quote, signer, now);
199
+ const authHash = authorizationHash(intentHash(intent), quoteHash(quote), auth.signerIdentity, auth.signedAt);
200
+ this.ledger.apply({ intentId: intent.id, to: State.Authorized, payloadHash: authHash, authorization: auth });
201
+ return auth;
202
+ }
203
+ // initiate starts a corridor and returns its in-flight state. It verifies the
204
+ // authorization, kicks off the pay-in leg, records the state, and returns
205
+ // immediately — it does not wait for the provider to confirm. Settlement is
206
+ // driven forward by advance as provider events arrive, so no promise or
207
+ // connection is held open per payment and durable state lives only in the
208
+ // ledger.
209
+ async initiate(intent, quote, auth) {
210
+ verifyAuthorization(auth, intent, quote, this.verifier);
211
+ const payInLeg = this.payIn.get(quote.payInAdapterId);
212
+ if (!payInLeg) {
213
+ throw new Error(`pact: no pay-in leg ${JSON.stringify(quote.payInAdapterId)}`);
214
+ }
215
+ // A direct corridor collects straight to the recipient; a bridged one
216
+ // collects into escrow first.
217
+ let deliverTo = recipientDestination(intent);
218
+ let to = State.Submitted;
219
+ if (!isDirect(quote)) {
220
+ deliverTo = this.escrowRef;
221
+ to = State.Collecting;
222
+ }
223
+ const collected = await payInLeg.collect(intent.id, quote, auth, deliverTo);
224
+ this.ledger.apply({ intentId: intent.id, to, payloadHash: submitHash(collected.providerRef), collect: collected });
225
+ return to;
226
+ }
227
+ // advance resumes a corridor when a provider event arrives — a webhook, a
228
+ // callback, a chain receipt, already normalized into an AdapterEvent. It reads
229
+ // the corridor's state from the ledger, interprets the event against it, and
230
+ // moves it forward one hop. It returns the terminal settlement once reached and
231
+ // the resulting state. Advancing the same step twice is idempotent, so a
232
+ // duplicated webhook settles once.
233
+ async advance(intentId, adapterId, event) {
234
+ const events = this.ledger.events(intentId);
235
+ const replayed = replay(events);
236
+ if (!replayed) {
237
+ throw new Error("pact: intent has no recorded authorization to advance");
238
+ }
239
+ const { intent, quote, authorization: auth } = replayed;
240
+ const authHash = authorizationHash(intentHash(intent), quoteHash(quote), auth.signerIdentity, auth.signedAt);
241
+ const current = this.ledger.state(intentId);
242
+ const failed = event.state === State.Failed;
243
+ const fromPayIn = adapterId === quote.payInAdapterId;
244
+ const fromPayOut = adapterId === quote.payOutAdapterId;
245
+ // A direct corridor's pay-in confirms and it settles.
246
+ if (fromPayIn && current === State.Submitted) {
247
+ if (failed)
248
+ return this.recordFailure(intentId, authHash, quote, event.reason);
249
+ const ref = payInRef(events);
250
+ const settlement = {
251
+ intentId,
252
+ state: State.Settled,
253
+ adapterId: quote.payInAdapterId,
254
+ providerTxRef: ref,
255
+ onchainTxHash: "",
256
+ receiptHash: corridorReceipt(authHash, ref, ref, quote.fxRate, "", State.Settled),
257
+ reason: "",
258
+ settledAt: this.clock(),
259
+ };
260
+ return this.finishAdvance(intentId, State.Settled, settlement);
261
+ }
262
+ // A bridged corridor's pay-in confirms; hold, convert, and kick off the
263
+ // pay-out.
264
+ if (fromPayIn && current === State.Collecting) {
265
+ // Nothing has entered escrow, so a failure here is a plain failure.
266
+ if (failed)
267
+ return this.recordFailure(intentId, authHash, quote, event.reason);
268
+ return this.holdAndDisburse(intent, quote, authHash, events);
269
+ }
270
+ // The pay-out confirms and the corridor settles, or fails and unwinds.
271
+ if (fromPayOut && current === State.Disbursing) {
272
+ if (failed)
273
+ return this.unwind(intentId, quote, authHash, event.reason);
274
+ return this.settleBridged(intentId, quote, authHash, events);
275
+ }
276
+ // Any other event — a duplicate webhook, or one for a phase already past — is
277
+ // a no-op. This is what makes a re-delivered provider callback settle once.
278
+ return { settlement: lastSettlement(events), state: current };
279
+ }
280
+ // holdAndDisburse records the escrow hold, converts through the bridge, and
281
+ // kicks off the pay-out leg, leaving the corridor disbursing until its pay-out
282
+ // confirms.
283
+ async holdAndDisburse(intent, quote, authHash, events) {
284
+ const payOutLeg = this.payOut.get(quote.payOutAdapterId);
285
+ if (!payOutLeg) {
286
+ throw new Error(`pact: no pay-out leg ${JSON.stringify(quote.payOutAdapterId)}`);
287
+ }
288
+ const bridge = this.bridges.get(quote.bridgeId);
289
+ if (!bridge) {
290
+ throw new Error(`pact: no bridge ${JSON.stringify(quote.bridgeId)}`);
291
+ }
292
+ const collected = collectResult(events);
293
+ this.ledger.apply({
294
+ intentId: intent.id,
295
+ to: State.Held,
296
+ payloadHash: hashString(domain("held"), collected.providerRef),
297
+ });
298
+ let converted;
299
+ try {
300
+ converted = await bridge.convert(intent.id, collected.received, intent.amount.currency, intent.amount.exponent);
301
+ }
302
+ catch (err) {
303
+ return this.unwind(intent.id, quote, authHash, reasonOf(err));
304
+ }
305
+ let disbursed;
306
+ try {
307
+ disbursed = await payOutLeg.disburse(intent.id, quote, recipientDestination(intent));
308
+ }
309
+ catch (err) {
310
+ return this.unwind(intent.id, quote, authHash, reasonOf(err));
311
+ }
312
+ const marker = {
313
+ intentId: intent.id,
314
+ state: State.Disbursing,
315
+ adapterId: payOutLeg.id,
316
+ providerTxRef: disbursed.providerRef,
317
+ onchainTxHash: "",
318
+ receiptHash: new Uint8Array(0),
319
+ reason: "",
320
+ settledAt: 0,
321
+ };
322
+ this.ledger.apply({
323
+ intentId: intent.id,
324
+ to: State.Disbursing,
325
+ payloadHash: hashString(domain("disburse"), disbursed.providerRef),
326
+ settlement: marker,
327
+ bridge: converted,
328
+ });
329
+ return { settlement: emptySettlement(), state: State.Disbursing };
330
+ }
331
+ // settleBridged closes out a bridged corridor once its pay-out has confirmed,
332
+ // binding both legs and the FX into the linked receipt.
333
+ settleBridged(intentId, quote, authHash, events) {
334
+ const inRef = payInRef(events);
335
+ const { payOut: outRef, bridge: bridgeRef } = disburseRefs(events);
336
+ const settlement = {
337
+ intentId,
338
+ state: State.Settled,
339
+ adapterId: quote.payOutAdapterId,
340
+ providerTxRef: outRef,
341
+ onchainTxHash: "",
342
+ receiptHash: corridorReceipt(authHash, inRef, outRef, quote.fxRate, bridgeRef, State.Settled),
343
+ reason: "",
344
+ settledAt: this.clock(),
345
+ };
346
+ return this.finishAdvance(intentId, State.Settled, settlement);
347
+ }
348
+ // unwind refunds the payer from escrow when a bridged corridor cannot complete
349
+ // its pay-out, moving to refunding then refunded.
350
+ async unwind(intentId, quote, authHash, reason) {
351
+ this.ledger.apply({
352
+ intentId,
353
+ to: State.Refunding,
354
+ payloadHash: hashString(domain("refunding"), reason),
355
+ });
356
+ const payInLeg = this.payIn.get(quote.payInAdapterId);
357
+ if (payInLeg) {
358
+ await payInLeg.refundIn(intentId, payInLeg.payInCapabilities().refunds, reason);
359
+ }
360
+ const settlement = {
361
+ intentId,
362
+ state: State.Refunded,
363
+ adapterId: quote.payInAdapterId,
364
+ providerTxRef: "",
365
+ onchainTxHash: "",
366
+ receiptHash: corridorReceipt(authHash, "", "", quote.fxRate, "", State.Refunded),
367
+ reason,
368
+ settledAt: this.clock(),
369
+ };
370
+ return this.finishAdvance(intentId, State.Refunded, settlement);
371
+ }
372
+ // recordFailure marks a pay-in that failed before any escrow was taken.
373
+ recordFailure(intentId, authHash, quote, reason) {
374
+ const settlement = {
375
+ intentId,
376
+ state: State.Failed,
377
+ adapterId: quote.payInAdapterId,
378
+ providerTxRef: "",
379
+ onchainTxHash: "",
380
+ receiptHash: corridorReceipt(authHash, "", "", quote.fxRate, "", State.Failed),
381
+ reason,
382
+ settledAt: this.clock(),
383
+ };
384
+ return this.finishAdvance(intentId, State.Failed, settlement);
385
+ }
386
+ finishAdvance(intentId, state, settlement) {
387
+ const finished = this.finish(intentId, state, settlement);
388
+ return { settlement: finished, state };
389
+ }
390
+ finish(intentId, state, settlement) {
391
+ const event = this.ledger.apply({ intentId, to: state, payloadHash: settlement.receiptHash, settlement });
392
+ this.notify(event);
393
+ return settlement;
394
+ }
395
+ // expire records that an intent's deadline passed before it settled.
396
+ expire(intentId) {
397
+ this.ledger.apply({
398
+ intentId,
399
+ to: State.Expired,
400
+ payloadHash: eventReceipt(0, intentId, State.Expired, new Uint8Array(0)),
401
+ });
402
+ }
403
+ subscribe(handler) {
404
+ this.subscribers.push(handler);
405
+ }
406
+ history(intentId) {
407
+ return this.ledger.events(intentId);
408
+ }
409
+ state(intentId) {
410
+ return this.ledger.state(intentId);
411
+ }
412
+ head(intentId) {
413
+ return this.ledger.head(intentId);
414
+ }
415
+ notify(event) {
416
+ for (const h of this.subscribers)
417
+ h(event);
418
+ }
419
+ }
420
+ // recipientDestination is the rail-specific handle the pay-out lands on — a
421
+ // crypto address, a phone number — carried in metadata, falling back to the
422
+ // generic recipient reference.
423
+ export function recipientDestination(intent) {
424
+ const d = intent.metadata["recipient_destination"];
425
+ return d ? d : intent.recipientRef;
426
+ }
427
+ // replay reconstructs the intent, quote, and authorization from the ledger so a
428
+ // corridor can resume without the caller holding them.
429
+ function replay(events) {
430
+ let intent;
431
+ let quote;
432
+ let authorization;
433
+ for (const e of events) {
434
+ if (e.intent)
435
+ intent = e.intent;
436
+ if (e.quote)
437
+ quote = e.quote;
438
+ if (e.authorization)
439
+ authorization = e.authorization;
440
+ }
441
+ if (!intent || !quote || !authorization)
442
+ return undefined;
443
+ return { intent, quote, authorization };
444
+ }
445
+ // collectResult returns what the pay-in leg recorded, or an empty result if none
446
+ // is present.
447
+ function collectResult(events) {
448
+ for (const e of events) {
449
+ if (e.collect)
450
+ return e.collect;
451
+ }
452
+ return { providerRef: "", received: Money.create("", 0, 0n) };
453
+ }
454
+ function payInRef(events) {
455
+ return collectResult(events).providerRef;
456
+ }
457
+ // disburseRefs returns the pay-out provider reference and the bridge receipt
458
+ // reference recorded on the disbursing step.
459
+ function disburseRefs(events) {
460
+ let payOut = "";
461
+ let bridge = "";
462
+ for (const e of events) {
463
+ if (e.state === State.Disbursing) {
464
+ if (e.settlement)
465
+ payOut = e.settlement.providerTxRef;
466
+ if (e.bridge)
467
+ bridge = e.bridge.receiptRef;
468
+ }
469
+ }
470
+ return { payOut, bridge };
471
+ }
472
+ // lastSettlement returns the settlement recorded on an intent's most recent
473
+ // event, if any, so an idempotent no-op can echo the outcome already reached.
474
+ function lastSettlement(events) {
475
+ for (let i = events.length - 1; i >= 0; i--) {
476
+ const e = events[i];
477
+ if (e.settlement)
478
+ return e.settlement;
479
+ }
480
+ return emptySettlement();
481
+ }
482
+ function emptySettlement() {
483
+ return {
484
+ intentId: "",
485
+ state: State.Unspecified,
486
+ adapterId: "",
487
+ providerTxRef: "",
488
+ onchainTxHash: "",
489
+ receiptHash: new Uint8Array(0),
490
+ reason: "",
491
+ settledAt: 0,
492
+ };
493
+ }
494
+ function reasonOf(cause) {
495
+ return cause instanceof Error ? cause.message : String(cause);
496
+ }
497
+ function containsOrEmpty(xs, target) {
498
+ return xs.length === 0 || xs.includes(target);
499
+ }
500
+ function firstRail(rails) {
501
+ return rails.length === 0 ? "" : rails[0];
502
+ }
503
+ // submitHash commits to a provider reference so a step carries a payload distinct
504
+ // from every other.
505
+ function submitHash(providerRef) {
506
+ return hashString(domain("submit"), providerRef);
507
+ }
508
+ function hashString(domainTag, value) {
509
+ return hashPreimage(new CanonicalWriter().str(domainTag).str(value).preimage());
510
+ }
@@ -0,0 +1,23 @@
1
+ import type { Money } from "./money.js";
2
+ import type { Quote } from "./message.js";
3
+ export interface KycLimits {
4
+ perPayment?: Money;
5
+ }
6
+ export interface KycStatus {
7
+ tier: string;
8
+ jurisdiction: string;
9
+ limits: KycLimits;
10
+ }
11
+ export interface KycProvider {
12
+ status(identity: string): Promise<KycStatus>;
13
+ }
14
+ export declare const permissiveKyc: KycProvider;
15
+ export declare function withinLimits(status: KycStatus, quote: Quote): boolean;
16
+ export interface RiskDecision {
17
+ allow: boolean;
18
+ reason: string;
19
+ }
20
+ export interface RiskHook {
21
+ evaluate(identity: string, intent: import("./message.js").Intent, quote: Quote): RiskDecision;
22
+ }
23
+ export declare const allowRisk: RiskHook;
@@ -0,0 +1,26 @@
1
+ // permissiveKyc imposes no limits, which is only acceptable for test-mode and
2
+ // testnet adapters.
3
+ export const permissiveKyc = {
4
+ async status() {
5
+ return { tier: "unverified", jurisdiction: "", limits: {} };
6
+ },
7
+ };
8
+ // withinLimits reports whether a quote's source amount is inside an identity's
9
+ // per-payment cap. An absent cap or a currency mismatch is treated as no
10
+ // applicable limit.
11
+ export function withinLimits(status, quote) {
12
+ const cap = status.limits.perPayment;
13
+ if (!cap)
14
+ return true;
15
+ try {
16
+ return quote.srcAmount.cmp(cap) <= 0;
17
+ }
18
+ catch {
19
+ return true;
20
+ }
21
+ }
22
+ export const allowRisk = {
23
+ evaluate() {
24
+ return { allow: true, reason: "" };
25
+ },
26
+ };
@@ -0,0 +1,12 @@
1
+ import { type KeyObject } from "node:crypto";
2
+ export declare function sha256(data: Uint8Array): Uint8Array;
3
+ export interface Ed25519KeyPair {
4
+ privateKey: KeyObject;
5
+ publicKey: Uint8Array;
6
+ }
7
+ export declare function ed25519KeyFromSeed(seed: Uint8Array): Ed25519KeyPair;
8
+ export declare function ed25519Sign(privateKey: KeyObject, message: Uint8Array): Uint8Array;
9
+ export declare function ed25519Verify(publicKeyRaw: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
10
+ export declare function toHex(bytes: Uint8Array): string;
11
+ export declare function fromHex(hex: string): Uint8Array;
12
+ export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
@@ -0,0 +1,50 @@
1
+ import { createHash, createPrivateKey, createPublicKey, sign as nodeSign, verify as nodeVerify, } from "node:crypto";
2
+ // SHA-256 of a byte string. Every message hash and receipt commitment flows
3
+ // through it.
4
+ export function sha256(data) {
5
+ return new Uint8Array(createHash("sha256").update(data).digest());
6
+ }
7
+ // The fixed DER prefixes that wrap a raw 32-byte Ed25519 seed or public key so
8
+ // the platform key APIs will import them. The seed becomes a PKCS8 private key;
9
+ // the public key becomes an SPKI public key.
10
+ const pkcs8SeedPrefix = Buffer.from("302e020100300506032b657004220420", "hex");
11
+ const spkiPublicPrefix = Buffer.from("302a300506032b6570032100", "hex");
12
+ // ed25519KeyFromSeed derives a signing key pair from a 32-byte seed. Ed25519 is
13
+ // deterministic, so a given seed and message always yield the same signature,
14
+ // which is what lets the cross-language vectors pin an exact signature.
15
+ export function ed25519KeyFromSeed(seed) {
16
+ if (seed.length !== 32) {
17
+ throw new Error("pact: ed25519 seed must be 32 bytes");
18
+ }
19
+ const der = Buffer.concat([pkcs8SeedPrefix, Buffer.from(seed)]);
20
+ const privateKey = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
21
+ const spki = createPublicKey(privateKey).export({ format: "der", type: "spki" });
22
+ return { privateKey, publicKey: new Uint8Array(spki.subarray(spki.length - 32)) };
23
+ }
24
+ export function ed25519Sign(privateKey, message) {
25
+ return new Uint8Array(nodeSign(null, Buffer.from(message), privateKey));
26
+ }
27
+ export function ed25519Verify(publicKeyRaw, message, signature) {
28
+ if (publicKeyRaw.length !== 32) {
29
+ return false;
30
+ }
31
+ const spki = Buffer.concat([spkiPublicPrefix, Buffer.from(publicKeyRaw)]);
32
+ const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
33
+ return nodeVerify(null, Buffer.from(message), publicKey, Buffer.from(signature));
34
+ }
35
+ // toHex and fromHex render byte strings for the wire and for test vectors.
36
+ export function toHex(bytes) {
37
+ return Buffer.from(bytes).toString("hex");
38
+ }
39
+ export function fromHex(hex) {
40
+ return new Uint8Array(Buffer.from(hex, "hex"));
41
+ }
42
+ export function bytesEqual(a, b) {
43
+ if (a.length !== b.length)
44
+ return false;
45
+ for (let i = 0; i < a.length; i++) {
46
+ if (a[i] !== b[i])
47
+ return false;
48
+ }
49
+ return true;
50
+ }
@@ -0,0 +1,29 @@
1
+ import { RefundKind } from "./adapter.js";
2
+ import type { Money } from "./money.js";
3
+ import type { Quote, Authorization, Settlement } from "./message.js";
4
+ import type { RateSource, EscrowVault } from "./bridge.js";
5
+ import type { PayInLeg, PayOutLeg, PayInCapabilities, PayOutCapabilities, CollectResult, DisburseResult } from "./leg.js";
6
+ export declare class FakeLeg implements PayInLeg, PayOutLeg {
7
+ readonly id: string;
8
+ private readonly rail;
9
+ private readonly currency;
10
+ private readonly ids;
11
+ failPayout: boolean;
12
+ constructor(id: string, rail: string, currency: string, ids: () => string);
13
+ payInCapabilities(): PayInCapabilities;
14
+ collect(_intentId: string, quote: Quote, _auth: Authorization, _deliverTo: string): Promise<CollectResult>;
15
+ refundIn(intentId: string, _kind: RefundKind, reason: string): Promise<Settlement>;
16
+ payOutCapabilities(): PayOutCapabilities;
17
+ disburse(_intentId: string, _quote: Quote, _recipientRef: string): Promise<DisburseResult>;
18
+ reverseOut(intentId: string, reason: string): Promise<Settlement>;
19
+ private terminal;
20
+ }
21
+ export declare class FakeRates implements RateSource {
22
+ private readonly table;
23
+ constructor(table: Record<string, string>);
24
+ rate(from: string, to: string): Promise<string>;
25
+ }
26
+ export declare class FakeVault implements EscrowVault {
27
+ hold(intentId: string, _amount: Money): Promise<string>;
28
+ release(_intentId: string): Promise<void>;
29
+ }