@myzonerocks/pact 0.1.2 → 0.1.4

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 (67) hide show
  1. package/dist/src/adapter.d.ts +1 -1
  2. package/dist/src/adapters/erc20.d.ts +13 -3
  3. package/dist/src/adapters/erc20.js +93 -49
  4. package/dist/src/adapters/http.d.ts +2 -0
  5. package/dist/src/adapters/http.js +12 -0
  6. package/dist/src/adapters/mpesa.d.ts +9 -2
  7. package/dist/src/adapters/mpesa.js +82 -12
  8. package/dist/src/adapters/paypal.d.ts +15 -3
  9. package/dist/src/adapters/paypal.js +121 -21
  10. package/dist/src/adapters/stripe.d.ts +6 -2
  11. package/dist/src/adapters/stripe.js +25 -12
  12. package/dist/src/bridge.js +12 -5
  13. package/dist/src/canonical.js +5 -0
  14. package/dist/src/client.d.ts +8 -2
  15. package/dist/src/client.js +181 -20
  16. package/dist/src/compliance.d.ts +4 -0
  17. package/dist/src/compliance.js +10 -3
  18. package/dist/src/crypto.js +4 -1
  19. package/dist/src/index.d.ts +0 -1
  20. package/dist/src/index.js +0 -1
  21. package/dist/src/ledger.d.ts +6 -2
  22. package/dist/src/ledger.js +2 -2
  23. package/dist/src/leg.d.ts +1 -1
  24. package/dist/src/message.d.ts +1 -1
  25. package/dist/src/message.js +8 -5
  26. package/dist/src/money.d.ts +1 -0
  27. package/dist/src/money.js +18 -3
  28. package/dist/src/protocol.d.ts +1 -0
  29. package/dist/src/protocol.js +8 -0
  30. package/dist/src/router.d.ts +2 -0
  31. package/dist/src/router.js +45 -7
  32. package/dist/src/wire.js +19 -2
  33. package/dist/test/erc20.test.js +95 -31
  34. package/dist/test/fake.d.ts +29 -0
  35. package/dist/test/fake.js +79 -0
  36. package/dist/test/lifecycle.test.js +31 -3
  37. package/dist/test/money.test.d.ts +1 -0
  38. package/dist/test/money.test.js +27 -0
  39. package/dist/test/mpesa.test.js +41 -8
  40. package/dist/test/paypal.test.js +54 -8
  41. package/dist/test/policy.test.js +6 -2
  42. package/dist/test/router.test.d.ts +1 -0
  43. package/dist/test/router.test.js +52 -0
  44. package/dist/test/stripe.test.js +5 -4
  45. package/dist/test/vectors.test.js +48 -2
  46. package/dist/test/wire.test.js +15 -0
  47. package/package.json +1 -1
  48. package/src/adapter.ts +7 -2
  49. package/src/adapters/erc20.ts +118 -51
  50. package/src/adapters/http.ts +14 -0
  51. package/src/adapters/mpesa.ts +102 -13
  52. package/src/adapters/paypal.ts +168 -22
  53. package/src/adapters/stripe.ts +16 -13
  54. package/src/bridge.ts +12 -5
  55. package/src/canonical.ts +5 -0
  56. package/src/client.ts +194 -22
  57. package/src/compliance.ts +20 -3
  58. package/src/crypto.ts +4 -1
  59. package/src/index.ts +0 -1
  60. package/src/ledger.ts +12 -4
  61. package/src/leg.ts +4 -1
  62. package/src/message.ts +8 -5
  63. package/src/money.ts +19 -3
  64. package/src/protocol.ts +9 -0
  65. package/src/router.ts +44 -4
  66. package/src/wire.ts +20 -3
  67. package/src/fake.ts +0 -96
package/src/router.ts CHANGED
@@ -28,28 +28,52 @@ export function isExpired(deadline: number, now: number, skew: number): boolean
28
28
  return deadline + skew <= now;
29
29
  }
30
30
 
31
+ // maxQuotes and maxFundingOptions bound the lists the router ranks and the client
32
+ // quotes over, so a caller (or an adapter-supplied list forwarded through one)
33
+ // cannot force unbounded work. The limits are generous next to any real corridor.
34
+ export const maxQuotes = 256;
35
+ export const maxFundingOptions = 64;
36
+
31
37
  // route selects one quote deterministically. It first discards quotes that are
32
38
  // expired against now or that exceed the identity's KYC limit, then ranks the
33
39
  // survivors by the policy. Ties break by pay-in adapter, then pay-out adapter,
34
40
  // then quote id, so the choice is total and stable and never depends on input
35
41
  // order or randomness.
36
42
  export function route(quotes: Quote[], policy: Policy, kyc: KycStatus, now: number, skew: number): Quote {
43
+ if (quotes.length > maxQuotes) {
44
+ throw new NoQuoteError(`quote set of ${quotes.length} exceeds the maximum of ${maxQuotes}`);
45
+ }
37
46
  const eligible = quotes.filter((q) => !isExpired(q.expiresAt, now, skew) && withinLimits(kyc, q));
38
47
  if (eligible.length === 0) {
39
48
  throw new NoQuoteError("all quotes expired or over limit");
40
49
  }
50
+ // Ranking by cost compares source amounts, which is meaningful only in one
51
+ // currency: the router holds no rate to weigh a dollar against a shilling. A
52
+ // mixed-currency set would otherwise fall through to the id tie-break and
53
+ // quietly crown a costlier quote, so it is refused rather than mis-ranked.
54
+ if (policy.kind === PolicyKind.Cheapest && spansSourceCurrencies(eligible)) {
55
+ throw new NoQuoteError("cannot rank by cost across source currencies without a common rate");
56
+ }
41
57
  const rank = rankFn(policy);
42
58
  eligible.sort((a, b) => {
43
59
  const primary = rank(a, b);
44
60
  if (primary !== 0) return primary;
45
- if (a.payInAdapterId !== b.payInAdapterId) return a.payInAdapterId < b.payInAdapterId ? -1 : 1;
46
- if (a.payOutAdapterId !== b.payOutAdapterId) return a.payOutAdapterId < b.payOutAdapterId ? -1 : 1;
47
- if (a.id !== b.id) return a.id < b.id ? -1 : 1;
48
- return 0;
61
+ const byPayIn = compareUtf8(a.payInAdapterId, b.payInAdapterId);
62
+ if (byPayIn !== 0) return byPayIn;
63
+ const byPayOut = compareUtf8(a.payOutAdapterId, b.payOutAdapterId);
64
+ if (byPayOut !== 0) return byPayOut;
65
+ return compareUtf8(a.id, b.id);
49
66
  });
50
67
  return eligible[0] as Quote;
51
68
  }
52
69
 
70
+ // spansSourceCurrencies reports whether the quotes carry more than one distinct
71
+ // source currency, the case a cost ranking cannot resolve without a rate.
72
+ function spansSourceCurrencies(quotes: Quote[]): boolean {
73
+ const first = quotes[0]!.srcAmount.currency;
74
+ return quotes.some((q) => q.srcAmount.currency !== first);
75
+ }
76
+
53
77
  function rankFn(policy: Policy): (a: Quote, b: Quote) => number {
54
78
  switch (policy.kind) {
55
79
  case PolicyKind.Fastest:
@@ -81,6 +105,22 @@ function cmpNumber(a: number, b: number): number {
81
105
  return a < b ? -1 : a > b ? 1 : 0;
82
106
  }
83
107
 
108
+ // compareUtf8 orders two strings by their UTF-8 byte encoding, so the tie-break
109
+ // agrees with the Go SDK. A plain string comparison orders by UTF-16 code unit,
110
+ // which disagrees for characters above U+FFFF and would break determinism across
111
+ // languages on a non-ASCII adapter id.
112
+ const utf8Encoder = new TextEncoder();
113
+ function compareUtf8(a: string, b: string): number {
114
+ if (a === b) return 0;
115
+ const ab = utf8Encoder.encode(a);
116
+ const bb = utf8Encoder.encode(b);
117
+ const n = Math.min(ab.length, bb.length);
118
+ for (let i = 0; i < n; i++) {
119
+ if (ab[i] !== bb[i]) return ab[i]! < bb[i]! ? -1 : 1;
120
+ }
121
+ return ab.length < bb.length ? -1 : ab.length > bb.length ? 1 : 0;
122
+ }
123
+
84
124
  // cmpMoney orders two quotes by source amount. Amounts in different currencies
85
125
  // are treated as incomparable and reported equal, deferring to the tie-break so
86
126
  // the router never throws on a mixed-currency quote set.
package/src/wire.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Money } from "./money.js";
2
- import { State } from "./state.js";
2
+ import { State, stateName } from "./state.js";
3
3
  import { CanonicalWriter } from "./canonical.js";
4
4
  import type { Intent, Quote, Authorization, Settlement } from "./message.js";
5
5
 
@@ -146,7 +146,14 @@ class WireReader {
146
146
  }
147
147
  const view = new DataView(this.buf.buffer, this.buf.byteOffset + this.pos, 8);
148
148
  this.pos += 8;
149
- return Number(view.getBigUint64(0, false));
149
+ const value = view.getBigUint64(0, false);
150
+ // A value above 2^53 cannot be held exactly in a JS number, so reject it
151
+ // rather than decode a Go or Dart value into a silently different one. Every
152
+ // real field here — millisecond timestamps, ordinals, latency — fits easily.
153
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
154
+ throw new Error("pact: u64 field exceeds the exact-integer range");
155
+ }
156
+ return Number(value);
150
157
  }
151
158
 
152
159
  money(): Money {
@@ -231,11 +238,21 @@ export function decodeAuthorization(b: Uint8Array): Authorization {
231
238
  return auth;
232
239
  }
233
240
 
241
+ // stateFromOrdinal rejects an ordinal outside the known states rather than pass
242
+ // an out-of-range number through as a State, so a corrupt wire byte cannot decode
243
+ // to a nonexistent state.
244
+ function stateFromOrdinal(n: number): State {
245
+ if (!(n in stateName)) {
246
+ throw new Error(`pact: unknown settlement state ordinal ${n}`);
247
+ }
248
+ return n as State;
249
+ }
250
+
234
251
  export function decodeSettlement(b: Uint8Array): Settlement {
235
252
  const r = new WireReader(b);
236
253
  const settlement: Settlement = {
237
254
  intentId: r.str(),
238
- state: r.u64() as State,
255
+ state: stateFromOrdinal(r.u64()),
239
256
  adapterId: r.str(),
240
257
  providerTxRef: r.str(),
241
258
  onchainTxHash: r.str(),
package/src/fake.ts DELETED
@@ -1,96 +0,0 @@
1
- import { State } from "./state.js";
2
- import { RefundKind } from "./adapter.js";
3
- import type { Money } from "./money.js";
4
- import type { Quote, Authorization, Settlement } from "./message.js";
5
- import type { RateSource, EscrowVault } from "./bridge.js";
6
- import type {
7
- PayInLeg,
8
- PayOutLeg,
9
- PayInCapabilities,
10
- PayOutCapabilities,
11
- CollectResult,
12
- DisburseResult,
13
- } from "./leg.js";
14
-
15
- // FakeLeg is a deterministic, in-memory pay-in and pay-out leg. It settles
16
- // instantly with no network and implements both sides so a test can build a
17
- // direct corridor from one leg or a bridged corridor from two. failPayout makes
18
- // its disburse fail so a test can exercise the escrow-unwind path.
19
- export class FakeLeg implements PayInLeg, PayOutLeg {
20
- failPayout = false;
21
-
22
- constructor(
23
- readonly id: string,
24
- private readonly rail: string,
25
- private readonly currency: string,
26
- private readonly ids: () => string,
27
- ) {}
28
-
29
- payInCapabilities(): PayInCapabilities {
30
- return { rails: [this.rail], currencies: [this.currency], refunds: RefundKind.Full };
31
- }
32
-
33
- // collect takes the payer's funds. received is the net the bridge converts —
34
- // the source the payer paid less the corridor fees.
35
- async collect(_intentId: string, quote: Quote, _auth: Authorization, _deliverTo: string): Promise<CollectResult> {
36
- return { providerRef: this.ids(), received: quote.srcAmount.sub(quote.fees) };
37
- }
38
-
39
- async refundIn(intentId: string, _kind: RefundKind, reason: string): Promise<Settlement> {
40
- return this.terminal(intentId, reason);
41
- }
42
-
43
- payOutCapabilities(): PayOutCapabilities {
44
- return { rails: [this.rail], currencies: [this.currency], reversible: false };
45
- }
46
-
47
- async disburse(_intentId: string, _quote: Quote, _recipientRef: string): Promise<DisburseResult> {
48
- if (this.failPayout) {
49
- throw new Error("fake: payout failed");
50
- }
51
- return { providerRef: this.ids() };
52
- }
53
-
54
- async reverseOut(intentId: string, reason: string): Promise<Settlement> {
55
- return this.terminal(intentId, reason);
56
- }
57
-
58
- private terminal(intentId: string, reason: string): Settlement {
59
- return {
60
- intentId,
61
- state: State.Refunded,
62
- adapterId: this.id,
63
- providerTxRef: this.ids(),
64
- onchainTxHash: "",
65
- receiptHash: new Uint8Array(0),
66
- reason,
67
- settledAt: 0,
68
- };
69
- }
70
- }
71
-
72
- // FakeRates is an in-memory rate source keyed by "FROM:TO", standing in for a
73
- // price feed so a bridge can be tested without a live market.
74
- export class FakeRates implements RateSource {
75
- constructor(private readonly table: Record<string, string>) {}
76
-
77
- async rate(from: string, to: string): Promise<string> {
78
- const rate = this.table[`${from}:${to}`];
79
- if (rate === undefined) {
80
- throw new Error(`fake: no rate for ${from}:${to}`);
81
- }
82
- return rate;
83
- }
84
- }
85
-
86
- // FakeVault is an in-memory escrow that records holds and releases, standing in
87
- // for the custody a real bridge relies on.
88
- export class FakeVault implements EscrowVault {
89
- async hold(intentId: string, _amount: Money): Promise<string> {
90
- return `escrow-${intentId}`;
91
- }
92
-
93
- async release(_intentId: string): Promise<void> {
94
- // Nothing to release in memory.
95
- }
96
- }