@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
package/src/state.ts ADDED
@@ -0,0 +1,76 @@
1
+ // State is a stage in a payment's lifecycle. The unspecified value is
2
+ // intentionally invalid so an uninitialized state never passes for a real one.
3
+ export enum State {
4
+ Unspecified = 0,
5
+ Draft,
6
+ Quoted,
7
+ Authorized,
8
+ Submitted,
9
+ Settled,
10
+ Failed,
11
+ Expired,
12
+ Refunded,
13
+ // The states below carry a bridged corridor through its escrow middle, where
14
+ // the pay-in and pay-out are different providers or currencies.
15
+ Collecting,
16
+ Held,
17
+ Disbursing,
18
+ Refunding,
19
+ }
20
+
21
+ // stateName maps each state to the lowercase token used in canonical preimages
22
+ // and on the wire. These strings are signed material and must not change for a
23
+ // given wire version.
24
+ export const stateName: Record<State, string> = {
25
+ [State.Unspecified]: "unspecified",
26
+ [State.Draft]: "draft",
27
+ [State.Quoted]: "quoted",
28
+ [State.Authorized]: "authorized",
29
+ [State.Submitted]: "submitted",
30
+ [State.Settled]: "settled",
31
+ [State.Failed]: "failed",
32
+ [State.Expired]: "expired",
33
+ [State.Refunded]: "refunded",
34
+ [State.Collecting]: "collecting",
35
+ [State.Held]: "held",
36
+ [State.Disbursing]: "disbursing",
37
+ [State.Refunding]: "refunding",
38
+ };
39
+
40
+ // transitions is the whole state machine expressed as data. A payment may move
41
+ // from a key only to a listed state; every other pairing is illegal. Terminal
42
+ // states map to an empty set. A direct corridor settles in one phase; a bridged
43
+ // one enters the escrow middle at collecting and unwinds through refunding when
44
+ // its pay-out cannot complete.
45
+ const transitions: Record<State, ReadonlySet<State>> = {
46
+ [State.Unspecified]: new Set([State.Draft]),
47
+ [State.Draft]: new Set([State.Quoted, State.Expired]),
48
+ [State.Quoted]: new Set([State.Authorized, State.Expired]),
49
+ [State.Authorized]: new Set([State.Submitted, State.Collecting, State.Expired]),
50
+ [State.Submitted]: new Set([State.Settled, State.Failed]),
51
+ [State.Collecting]: new Set([State.Held, State.Failed, State.Expired]),
52
+ [State.Held]: new Set([State.Disbursing, State.Refunding, State.Expired]),
53
+ [State.Disbursing]: new Set([State.Settled, State.Failed, State.Refunding]),
54
+ [State.Refunding]: new Set([State.Refunded, State.Failed]),
55
+ [State.Settled]: new Set([State.Refunded]),
56
+ [State.Failed]: new Set(),
57
+ [State.Expired]: new Set(),
58
+ [State.Refunded]: new Set(),
59
+ };
60
+
61
+ export function canTransition(from: State, to: State): boolean {
62
+ return transitions[from].has(to);
63
+ }
64
+
65
+ // isTerminal reports whether a state accepts no further transition. Settled is
66
+ // not terminal because it still admits the single edge to refunded.
67
+ export function isTerminal(state: State): boolean {
68
+ return transitions[state].size === 0;
69
+ }
70
+
71
+ export class TransitionError extends Error {
72
+ constructor(readonly from: State, readonly to: State) {
73
+ super(`pact: illegal transition ${stateName[from]} -> ${stateName[to]}`);
74
+ this.name = "TransitionError";
75
+ }
76
+ }
package/src/wire.ts ADDED
@@ -0,0 +1,248 @@
1
+ import { Money } from "./money.js";
2
+ import { State } from "./state.js";
3
+ import { CanonicalWriter } from "./canonical.js";
4
+ import type { Intent, Quote, Authorization, Settlement } from "./message.js";
5
+
6
+ // The wire codec serializes messages for transport. It is distinct from the
7
+ // canonical signing preimage: signing binds a fixed subset of fields under a
8
+ // domain tag, while the wire form carries every field so a peer can reconstruct
9
+ // the message. Both use the same length-delimited primitives; a message's wire
10
+ // bytes are never signed and its signing preimage is never sent.
11
+
12
+ export enum WireKind {
13
+ Intent = 1,
14
+ Quote = 2,
15
+ Authorization = 3,
16
+ Settlement = 4,
17
+ }
18
+
19
+ const decoder = new TextDecoder();
20
+
21
+ export function encodeIntent(i: Intent): Uint8Array {
22
+ return new CanonicalWriter()
23
+ .str(i.id)
24
+ .str(i.senderRef)
25
+ .str(i.recipientRef)
26
+ .money(i.amount)
27
+ .str(i.memo)
28
+ .u64(i.expiresAt)
29
+ .list(i.allowedRails)
30
+ .stringMap(i.metadata)
31
+ .preimage();
32
+ }
33
+
34
+ export function encodeQuote(q: Quote): Uint8Array {
35
+ return new CanonicalWriter()
36
+ .str(q.id)
37
+ .str(q.intentId)
38
+ .str(q.payInAdapterId)
39
+ .str(q.payInRail)
40
+ .str(q.payOutAdapterId)
41
+ .str(q.payOutRail)
42
+ .str(q.bridgeId)
43
+ .money(q.srcAmount)
44
+ .money(q.dstAmount)
45
+ .money(q.fees)
46
+ .str(q.fxRate)
47
+ .u64(q.expiresAt)
48
+ .str(q.providerQuoteRef)
49
+ .u64(q.latencyEstimateMs)
50
+ .preimage();
51
+ }
52
+
53
+ export function encodeAuthorization(a: Authorization): Uint8Array {
54
+ return new CanonicalWriter()
55
+ .str(a.intentId)
56
+ .str(a.quoteId)
57
+ .str(a.signerIdentity)
58
+ .u64(a.signedAt)
59
+ .bytes(a.signature)
60
+ .preimage();
61
+ }
62
+
63
+ export function encodeSettlement(s: Settlement): Uint8Array {
64
+ return new CanonicalWriter()
65
+ .str(s.intentId)
66
+ .u64(s.state)
67
+ .str(s.adapterId)
68
+ .str(s.providerTxRef)
69
+ .str(s.onchainTxHash)
70
+ .bytes(s.receiptHash)
71
+ .str(s.reason)
72
+ .u64(s.settledAt)
73
+ .preimage();
74
+ }
75
+
76
+ // frame prefixes a kind tag onto an encoding so a receiver can dispatch without
77
+ // knowing the type in advance. This is what a host carries inside its envelope.
78
+ export type WireMessage =
79
+ | { kind: WireKind.Intent; message: Intent }
80
+ | { kind: WireKind.Quote; message: Quote }
81
+ | { kind: WireKind.Authorization; message: Authorization }
82
+ | { kind: WireKind.Settlement; message: Settlement };
83
+
84
+ export function encodeMessage(msg: WireMessage): Uint8Array {
85
+ const payload =
86
+ msg.kind === WireKind.Intent
87
+ ? encodeIntent(msg.message)
88
+ : msg.kind === WireKind.Quote
89
+ ? encodeQuote(msg.message)
90
+ : msg.kind === WireKind.Authorization
91
+ ? encodeAuthorization(msg.message)
92
+ : encodeSettlement(msg.message);
93
+ const out = new Uint8Array(payload.length + 1);
94
+ out[0] = msg.kind;
95
+ out.set(payload, 1);
96
+ return out;
97
+ }
98
+
99
+ export function decodeMessage(frame: Uint8Array): WireMessage {
100
+ if (frame.length === 0) {
101
+ throw new Error("pact: wire frame ended mid-field");
102
+ }
103
+ const kind = frame[0] as WireKind;
104
+ const body = frame.subarray(1);
105
+ switch (kind) {
106
+ case WireKind.Intent:
107
+ return { kind, message: decodeIntent(body) };
108
+ case WireKind.Quote:
109
+ return { kind, message: decodeQuote(body) };
110
+ case WireKind.Authorization:
111
+ return { kind, message: decodeAuthorization(body) };
112
+ case WireKind.Settlement:
113
+ return { kind, message: decodeSettlement(body) };
114
+ default:
115
+ throw new Error(`pact: unknown wire kind ${kind}`);
116
+ }
117
+ }
118
+
119
+ // WireReader consumes the length-delimited primitives in order.
120
+ class WireReader {
121
+ private pos = 0;
122
+ constructor(private readonly buf: Uint8Array) {}
123
+
124
+ bytes(): Uint8Array {
125
+ if (this.pos + 4 > this.buf.length) {
126
+ throw new Error("pact: wire frame ended mid-field");
127
+ }
128
+ const view = new DataView(this.buf.buffer, this.buf.byteOffset + this.pos, 4);
129
+ const length = view.getUint32(0, false);
130
+ this.pos += 4;
131
+ if (this.pos + length > this.buf.length) {
132
+ throw new Error("pact: wire frame ended mid-field");
133
+ }
134
+ const out = this.buf.subarray(this.pos, this.pos + length);
135
+ this.pos += length;
136
+ return out;
137
+ }
138
+
139
+ str(): string {
140
+ return decoder.decode(this.bytes());
141
+ }
142
+
143
+ u64(): number {
144
+ if (this.pos + 8 > this.buf.length) {
145
+ throw new Error("pact: wire frame ended mid-field");
146
+ }
147
+ const view = new DataView(this.buf.buffer, this.buf.byteOffset + this.pos, 8);
148
+ this.pos += 8;
149
+ return Number(view.getBigUint64(0, false));
150
+ }
151
+
152
+ money(): Money {
153
+ const minor = this.str();
154
+ const currency = this.str();
155
+ const exponent = this.u64();
156
+ return Money.parse(currency, exponent, minor);
157
+ }
158
+
159
+ list(): string[] {
160
+ const count = this.u64();
161
+ const out: string[] = [];
162
+ for (let i = 0; i < count; i++) out.push(this.str());
163
+ return out;
164
+ }
165
+
166
+ stringMap(): Record<string, string> {
167
+ const count = this.u64();
168
+ const out: Record<string, string> = {};
169
+ for (let i = 0; i < count; i++) {
170
+ const key = this.str();
171
+ out[key] = this.str();
172
+ }
173
+ return out;
174
+ }
175
+
176
+ requireDone(): void {
177
+ if (this.pos !== this.buf.length) {
178
+ throw new Error("pact: trailing bytes after wire frame");
179
+ }
180
+ }
181
+ }
182
+
183
+ export function decodeIntent(b: Uint8Array): Intent {
184
+ const r = new WireReader(b);
185
+ const intent: Intent = {
186
+ id: r.str(),
187
+ senderRef: r.str(),
188
+ recipientRef: r.str(),
189
+ amount: r.money(),
190
+ memo: r.str(),
191
+ expiresAt: r.u64(),
192
+ allowedRails: r.list(),
193
+ metadata: r.stringMap(),
194
+ };
195
+ r.requireDone();
196
+ return intent;
197
+ }
198
+
199
+ export function decodeQuote(b: Uint8Array): Quote {
200
+ const r = new WireReader(b);
201
+ const quote: Quote = {
202
+ id: r.str(),
203
+ intentId: r.str(),
204
+ payInAdapterId: r.str(),
205
+ payInRail: r.str(),
206
+ payOutAdapterId: r.str(),
207
+ payOutRail: r.str(),
208
+ bridgeId: r.str(),
209
+ srcAmount: r.money(),
210
+ dstAmount: r.money(),
211
+ fees: r.money(),
212
+ fxRate: r.str(),
213
+ expiresAt: r.u64(),
214
+ providerQuoteRef: r.str(),
215
+ latencyEstimateMs: r.u64(),
216
+ };
217
+ r.requireDone();
218
+ return quote;
219
+ }
220
+
221
+ export function decodeAuthorization(b: Uint8Array): Authorization {
222
+ const r = new WireReader(b);
223
+ const auth: Authorization = {
224
+ intentId: r.str(),
225
+ quoteId: r.str(),
226
+ signerIdentity: r.str(),
227
+ signedAt: r.u64(),
228
+ signature: r.bytes().slice(),
229
+ };
230
+ r.requireDone();
231
+ return auth;
232
+ }
233
+
234
+ export function decodeSettlement(b: Uint8Array): Settlement {
235
+ const r = new WireReader(b);
236
+ const settlement: Settlement = {
237
+ intentId: r.str(),
238
+ state: r.u64() as State,
239
+ adapterId: r.str(),
240
+ providerTxRef: r.str(),
241
+ onchainTxHash: r.str(),
242
+ receiptHash: r.bytes().slice(),
243
+ reason: r.str(),
244
+ settledAt: r.u64(),
245
+ };
246
+ r.requireDone();
247
+ return settlement;
248
+ }