@myzonerocks/pact 0.1.3 → 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.
- package/dist/src/adapter.d.ts +1 -1
- package/dist/src/adapters/erc20.d.ts +13 -3
- package/dist/src/adapters/erc20.js +93 -49
- package/dist/src/adapters/http.d.ts +2 -0
- package/dist/src/adapters/http.js +12 -0
- package/dist/src/adapters/mpesa.d.ts +9 -2
- package/dist/src/adapters/mpesa.js +82 -12
- package/dist/src/adapters/paypal.d.ts +5 -1
- package/dist/src/adapters/paypal.js +76 -25
- package/dist/src/adapters/stripe.d.ts +3 -2
- package/dist/src/adapters/stripe.js +14 -11
- package/dist/src/bridge.js +12 -5
- package/dist/src/canonical.js +5 -0
- package/dist/src/client.d.ts +8 -2
- package/dist/src/client.js +181 -20
- package/dist/src/compliance.d.ts +4 -0
- package/dist/src/compliance.js +10 -3
- package/dist/src/crypto.js +4 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/dist/src/ledger.d.ts +6 -2
- package/dist/src/ledger.js +2 -2
- package/dist/src/leg.d.ts +1 -1
- package/dist/src/message.d.ts +1 -1
- package/dist/src/message.js +8 -5
- package/dist/src/money.d.ts +1 -0
- package/dist/src/money.js +18 -3
- package/dist/src/protocol.d.ts +1 -0
- package/dist/src/protocol.js +8 -0
- package/dist/src/router.d.ts +2 -0
- package/dist/src/router.js +45 -7
- package/dist/src/wire.js +19 -2
- package/dist/test/erc20.test.js +95 -31
- package/dist/test/fake.d.ts +29 -0
- package/dist/test/fake.js +79 -0
- package/dist/test/lifecycle.test.js +31 -3
- package/dist/test/money.test.d.ts +1 -0
- package/dist/test/money.test.js +27 -0
- package/dist/test/mpesa.test.js +41 -8
- package/dist/test/paypal.test.js +33 -8
- package/dist/test/policy.test.js +6 -2
- package/dist/test/router.test.d.ts +1 -0
- package/dist/test/router.test.js +52 -0
- package/dist/test/stripe.test.js +5 -4
- package/dist/test/vectors.test.js +48 -2
- package/dist/test/wire.test.js +15 -0
- package/package.json +1 -1
- package/src/adapter.ts +7 -2
- package/src/adapters/erc20.ts +118 -51
- package/src/adapters/http.ts +14 -0
- package/src/adapters/mpesa.ts +102 -13
- package/src/adapters/paypal.ts +102 -24
- package/src/adapters/stripe.ts +16 -13
- package/src/bridge.ts +12 -5
- package/src/canonical.ts +5 -0
- package/src/client.ts +194 -22
- package/src/compliance.ts +20 -3
- package/src/crypto.ts +4 -1
- package/src/index.ts +0 -1
- package/src/ledger.ts +12 -4
- package/src/leg.ts +4 -1
- package/src/message.ts +8 -5
- package/src/money.ts +19 -3
- package/src/protocol.ts +9 -0
- package/src/router.ts +44 -4
- package/src/wire.ts +20 -3
- package/src/fake.ts +0 -96
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// can be refunded in full, but a delivered payout cannot be pulled back.
|
|
7
7
|
import { State } from "../state.js";
|
|
8
8
|
import { RefundKind } from "../adapter.js";
|
|
9
|
+
import { idempotencyKey } from "../protocol.js";
|
|
10
|
+
import { fetchWithTimeout } from "./http.js";
|
|
9
11
|
// The public base of the PayPal REST API. It is the same for every live
|
|
10
12
|
// integration and carries no secret; the sandbox host or a local test server is
|
|
11
13
|
// injected instead.
|
|
@@ -22,6 +24,9 @@ export class PaypalLeg {
|
|
|
22
24
|
api;
|
|
23
25
|
ids;
|
|
24
26
|
captures = new Map();
|
|
27
|
+
// What each intent was quoted to collect, so a capture webhook can be
|
|
28
|
+
// cross-checked: the paid amount and currency must match before it settles.
|
|
29
|
+
expected = new Map();
|
|
25
30
|
constructor(cfg) {
|
|
26
31
|
this.id = cfg.id ?? "paypal";
|
|
27
32
|
this.currencies = cfg.currencies;
|
|
@@ -33,7 +38,7 @@ export class PaypalLeg {
|
|
|
33
38
|
rails: ["paypal"],
|
|
34
39
|
currencies: this.currencies,
|
|
35
40
|
methods: ["paypal", "venmo", "card"],
|
|
36
|
-
refunds: RefundKind.
|
|
41
|
+
refunds: RefundKind.Partial,
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
44
|
payOutCapabilities() {
|
|
@@ -50,6 +55,10 @@ export class PaypalLeg {
|
|
|
50
55
|
referenceId: referencePrefix + intentId,
|
|
51
56
|
});
|
|
52
57
|
this.captures.set(intentId, capture.captureId);
|
|
58
|
+
this.expected.set(intentId, {
|
|
59
|
+
value: majorAmount(quote.srcAmount.minor(), quote.srcAmount.exponent),
|
|
60
|
+
currency: quote.srcAmount.currency,
|
|
61
|
+
});
|
|
53
62
|
return { providerRef: capture.captureId, received: quote.srcAmount.sub(quote.fees) };
|
|
54
63
|
}
|
|
55
64
|
// prepare creates an order the payer approves and captures on their own device
|
|
@@ -69,6 +78,10 @@ export class PaypalLeg {
|
|
|
69
78
|
params.platformFee = majorAmount(quote.fees.minor(), quote.fees.exponent);
|
|
70
79
|
}
|
|
71
80
|
const order = await this.api.createOrder(params);
|
|
81
|
+
this.expected.set(intentId, {
|
|
82
|
+
value: majorAmount(quote.srcAmount.minor(), quote.srcAmount.exponent),
|
|
83
|
+
currency: quote.srcAmount.currency,
|
|
84
|
+
});
|
|
72
85
|
return { providerRef: order.id, clientSecret: order.id, method: "paypal" };
|
|
73
86
|
}
|
|
74
87
|
// disburse sends a payout of the recipient's amount to recipientRef and returns
|
|
@@ -82,18 +95,23 @@ export class PaypalLeg {
|
|
|
82
95
|
});
|
|
83
96
|
return { providerRef: payout.batchId };
|
|
84
97
|
}
|
|
85
|
-
// refundIn reverses a captured payment in full. PayPal captures the
|
|
86
|
-
// the capture id it returned from collect, so the refund binds to
|
|
87
|
-
// reference.
|
|
88
|
-
async refundIn(intentId, kind, reason) {
|
|
89
|
-
if (kind !== RefundKind.Full) {
|
|
98
|
+
// refundIn reverses a captured payment, in full or in part. PayPal captures the
|
|
99
|
+
// order id in the capture id it returned from collect, so the refund binds to
|
|
100
|
+
// that reference; a partial refund names the amount to return.
|
|
101
|
+
async refundIn(intentId, kind, amount, reason) {
|
|
102
|
+
if (kind !== RefundKind.Full && kind !== RefundKind.Partial) {
|
|
90
103
|
throw new Error(`paypal: cannot perform refund kind ${kind}`);
|
|
91
104
|
}
|
|
92
105
|
const captureId = this.captures.get(intentId);
|
|
93
106
|
if (!captureId) {
|
|
94
107
|
throw new Error(`paypal: no capture for intent ${intentId}`);
|
|
95
108
|
}
|
|
96
|
-
|
|
109
|
+
// A partial refund names the amount in major units; a full refund leaves it
|
|
110
|
+
// absent so PayPal returns the whole capture.
|
|
111
|
+
const params = kind === RefundKind.Partial
|
|
112
|
+
? { captureId, reason, value: majorAmount(amount.minor(), amount.exponent), currencyCode: amount.currency }
|
|
113
|
+
: { captureId, reason };
|
|
114
|
+
const refund = await this.api.refundCapture(params);
|
|
97
115
|
return {
|
|
98
116
|
intentId,
|
|
99
117
|
state: State.Refunded,
|
|
@@ -123,8 +141,20 @@ export class PaypalLeg {
|
|
|
123
141
|
const intentId = intentFromReference(resource.custom_id ?? "", resource.invoice_id ?? "");
|
|
124
142
|
const providerRef = resource.id ?? "";
|
|
125
143
|
switch (event.event_type) {
|
|
126
|
-
case "PAYMENT.CAPTURE.COMPLETED":
|
|
144
|
+
case "PAYMENT.CAPTURE.COMPLETED": {
|
|
145
|
+
// The custom_id that ties a capture to an intent is chosen by whoever
|
|
146
|
+
// created the order, so a genuine, signed capture for a different order can
|
|
147
|
+
// carry a target intent's id. Settle only when the captured amount and
|
|
148
|
+
// currency match what the intent was quoted to collect.
|
|
149
|
+
const want = this.expected.get(intentId);
|
|
150
|
+
if (!want) {
|
|
151
|
+
throw new Error(`paypal: capture for unknown intent ${JSON.stringify(intentId)}`);
|
|
152
|
+
}
|
|
153
|
+
if (resource.amount?.value !== want.value || resource.amount?.currency_code !== want.currency) {
|
|
154
|
+
throw new Error(`paypal: captured ${resource.amount?.value} ${resource.amount?.currency_code} does not match the quoted ${want.value} ${want.currency}`);
|
|
155
|
+
}
|
|
127
156
|
return [oneEvent(intentId, State.Settled, providerRef, "")];
|
|
157
|
+
}
|
|
128
158
|
case "PAYMENT.CAPTURE.DENIED":
|
|
129
159
|
return [oneEvent(intentId, State.Failed, providerRef, "capture denied")];
|
|
130
160
|
case "PAYMENT.CAPTURE.REFUNDED":
|
|
@@ -182,7 +212,7 @@ class HttpPaypalApi {
|
|
|
182
212
|
return this.token;
|
|
183
213
|
}
|
|
184
214
|
const basic = Buffer.from(`${this.creds.clientId}:${this.creds.clientSecret}`).toString("base64");
|
|
185
|
-
const resp = await
|
|
215
|
+
const resp = await fetchWithTimeout(`${this.baseURL}/v1/oauth2/token`, {
|
|
186
216
|
method: "POST",
|
|
187
217
|
headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/x-www-form-urlencoded" },
|
|
188
218
|
body: "grant_type=client_credentials",
|
|
@@ -203,15 +233,12 @@ class HttpPaypalApi {
|
|
|
203
233
|
if (params.payee) {
|
|
204
234
|
unit.payee = { email_address: params.payee };
|
|
205
235
|
}
|
|
206
|
-
const created = await this.postJSON("/v2/checkout/orders", {
|
|
207
|
-
intent: "CAPTURE",
|
|
208
|
-
purchase_units: [unit],
|
|
209
|
-
});
|
|
236
|
+
const created = await this.postJSON("/v2/checkout/orders", { intent: "CAPTURE", purchase_units: [unit] }, idempotencyKey(params.referenceId, "paypal-order"));
|
|
210
237
|
const existing = firstCapture(created);
|
|
211
238
|
if (existing) {
|
|
212
239
|
return { orderId: created.id ?? "", captureId: existing.id ?? "", status: existing.status ?? "" };
|
|
213
240
|
}
|
|
214
|
-
const captured = await this.postJSON(`/v2/checkout/orders/${encodeURIComponent(created.id ?? "")}/capture`, {});
|
|
241
|
+
const captured = await this.postJSON(`/v2/checkout/orders/${encodeURIComponent(created.id ?? "")}/capture`, {}, idempotencyKey(params.referenceId, "paypal-capture"));
|
|
215
242
|
const capture = firstCapture(captured);
|
|
216
243
|
if (!capture) {
|
|
217
244
|
throw new Error(`paypal: order ${created.id} captured without a capture id`);
|
|
@@ -235,10 +262,7 @@ class HttpPaypalApi {
|
|
|
235
262
|
platform_fees: [{ amount: { currency_code: params.currencyCode, value: params.platformFee } }],
|
|
236
263
|
};
|
|
237
264
|
}
|
|
238
|
-
const created = await this.postJSON("/v2/checkout/orders", {
|
|
239
|
-
intent: "CAPTURE",
|
|
240
|
-
purchase_units: [unit],
|
|
241
|
-
});
|
|
265
|
+
const created = await this.postJSON("/v2/checkout/orders", { intent: "CAPTURE", purchase_units: [unit] }, idempotencyKey(params.referenceId, "paypal-order"));
|
|
242
266
|
let approve = "";
|
|
243
267
|
for (const link of created.links ?? []) {
|
|
244
268
|
if (link.rel === "approve" || link.rel === "payer-action") {
|
|
@@ -267,7 +291,11 @@ class HttpPaypalApi {
|
|
|
267
291
|
if (params.reason) {
|
|
268
292
|
body.note_to_payer = params.reason;
|
|
269
293
|
}
|
|
270
|
-
|
|
294
|
+
// A partial refund names the amount; an absent value refunds the full capture.
|
|
295
|
+
if (params.value) {
|
|
296
|
+
body.amount = { value: params.value, currency_code: params.currencyCode };
|
|
297
|
+
}
|
|
298
|
+
const out = await this.postJSON(`/v2/payments/captures/${encodeURIComponent(params.captureId)}/refund`, body, idempotencyKey(params.captureId, "paypal-refund"));
|
|
271
299
|
return { id: out.id ?? "", status: out.status ?? "" };
|
|
272
300
|
}
|
|
273
301
|
// verifyWebhook forwards the signature headers and the raw body to PayPal's
|
|
@@ -275,23 +303,46 @@ class HttpPaypalApi {
|
|
|
275
303
|
// binds a payload to this integration; PayPal reports whether the signature is
|
|
276
304
|
// authentic.
|
|
277
305
|
async verifyWebhook(headers, body) {
|
|
278
|
-
|
|
306
|
+
// PayPal's signature covers the exact bytes of the event it delivered, so the
|
|
307
|
+
// event is forwarded verbatim. Parsing and re-serializing it would reorder
|
|
308
|
+
// keys or restyle numbers and make an authentic webhook fail verification.
|
|
309
|
+
const rawEvent = new TextDecoder().decode(body);
|
|
310
|
+
const sentinel = "__pact_raw_webhook_event__";
|
|
311
|
+
const envelope = JSON.stringify({
|
|
279
312
|
webhook_id: this.creds.webhookId,
|
|
280
313
|
transmission_id: header(headers, "PayPal-Transmission-Id"),
|
|
281
314
|
transmission_time: header(headers, "PayPal-Transmission-Time"),
|
|
282
315
|
transmission_sig: header(headers, "PayPal-Transmission-Sig"),
|
|
283
316
|
cert_url: header(headers, "PayPal-Cert-Url"),
|
|
284
317
|
auth_algo: header(headers, "PayPal-Auth-Algo"),
|
|
285
|
-
webhook_event:
|
|
318
|
+
webhook_event: sentinel,
|
|
286
319
|
});
|
|
320
|
+
const payload = envelope.replace(`"${sentinel}"`, rawEvent);
|
|
321
|
+
const out = await this.postSerialized("/v1/notifications/verify-webhook-signature", payload);
|
|
287
322
|
return out.verification_status === "SUCCESS";
|
|
288
323
|
}
|
|
289
|
-
|
|
324
|
+
// requestId, when set, is sent as PayPal-Request-Id. PayPal deduplicates a
|
|
325
|
+
// mutation that carries a request id it has already seen, so a retry after a
|
|
326
|
+
// lost response reuses the first order, capture, or refund rather than creating
|
|
327
|
+
// a second.
|
|
328
|
+
async postJSON(path, body, requestId) {
|
|
329
|
+
return this.postSerialized(path, JSON.stringify(body), requestId);
|
|
330
|
+
}
|
|
331
|
+
// postSerialized posts an already-serialized JSON payload, so a caller that must
|
|
332
|
+
// control the exact bytes on the wire — a webhook forwarded verbatim — can do so.
|
|
333
|
+
async postSerialized(path, payload, requestId) {
|
|
290
334
|
const token = await this.accessToken();
|
|
291
|
-
const
|
|
335
|
+
const headers = {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
Authorization: `Bearer ${token}`,
|
|
338
|
+
};
|
|
339
|
+
if (requestId) {
|
|
340
|
+
headers["PayPal-Request-Id"] = requestId;
|
|
341
|
+
}
|
|
342
|
+
const resp = await fetchWithTimeout(this.baseURL + path, {
|
|
292
343
|
method: "POST",
|
|
293
|
-
headers
|
|
294
|
-
body:
|
|
344
|
+
headers,
|
|
345
|
+
body: payload,
|
|
295
346
|
});
|
|
296
347
|
if (resp.status >= 300) {
|
|
297
348
|
throw new Error(`paypal: ${path} returned ${resp.status}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Money } from "../money.js";
|
|
1
2
|
import type { Quote, Authorization, Settlement } from "../message.js";
|
|
2
3
|
import { RefundKind, type AdapterEvent } from "../adapter.js";
|
|
3
4
|
import type { InteractivePayInLeg, PayInCapabilities, CollectResult, PayInPreparation } from "../leg.js";
|
|
@@ -45,7 +46,7 @@ export interface StripeConfig {
|
|
|
45
46
|
currencies: string[];
|
|
46
47
|
api: StripeApi;
|
|
47
48
|
webhookKey: string;
|
|
48
|
-
clock
|
|
49
|
+
clock: () => number;
|
|
49
50
|
tolerance?: number;
|
|
50
51
|
ids: () => string;
|
|
51
52
|
methods?: string[];
|
|
@@ -63,7 +64,7 @@ export declare class StripeLeg implements InteractivePayInLeg {
|
|
|
63
64
|
payInCapabilities(): PayInCapabilities;
|
|
64
65
|
collect(intentId: string, quote: Quote, _auth: Authorization, deliverTo: string): Promise<CollectResult>;
|
|
65
66
|
prepare(intentId: string, quote: Quote, _auth: Authorization, deliverTo: string): Promise<PayInPreparation>;
|
|
66
|
-
refundIn(intentId: string, kind: RefundKind, reason: string): Promise<Settlement>;
|
|
67
|
+
refundIn(intentId: string, kind: RefundKind, amount: Money, reason: string): Promise<Settlement>;
|
|
67
68
|
parseWebhook(raw: Uint8Array, headers: Record<string, string[]>): AdapterEvent[];
|
|
68
69
|
}
|
|
69
70
|
export declare const ErrNoSignature = "stripe: missing signature header";
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
9
9
|
import { State } from "../state.js";
|
|
10
10
|
import { RefundKind } from "../adapter.js";
|
|
11
|
+
import { idempotencyKey } from "../protocol.js";
|
|
12
|
+
import { fetchWithTimeout } from "./http.js";
|
|
11
13
|
// The public base of the Stripe REST API. It is the same for every integration
|
|
12
14
|
// and carries no secret; tests point the leg at a local server instead.
|
|
13
15
|
const defaultBaseURL = "https://api.stripe.com";
|
|
@@ -20,8 +22,6 @@ const metadataIntentKey = "pact_intent_id";
|
|
|
20
22
|
// defaultToleranceSeconds is how far a webhook timestamp may drift from now
|
|
21
23
|
// before it is rejected as a possible replay.
|
|
22
24
|
const defaultToleranceSeconds = 300;
|
|
23
|
-
// The protocol identifier used to bind an idempotency key to one protocol step.
|
|
24
|
-
const idPrefix = "pact";
|
|
25
25
|
// Capture selects when the charge is captured. Manual creates the intent already
|
|
26
26
|
// carrying a payment method for the server to capture immediately — the flow an
|
|
27
27
|
// agent or a saved-card charge uses. OnConfirmation defers capture to the moment
|
|
@@ -48,11 +48,16 @@ export class StripeLeg {
|
|
|
48
48
|
if (!cfg.webhookKey) {
|
|
49
49
|
throw new Error("stripe: config requires a webhook signing key");
|
|
50
50
|
}
|
|
51
|
+
// Without a clock every webhook timestamp reads as far in the past and
|
|
52
|
+
// silently fails the tolerance check, so a real webhook never verifies.
|
|
53
|
+
if (!cfg.clock) {
|
|
54
|
+
throw new Error("stripe: config requires a clock for webhook timestamp checks");
|
|
55
|
+
}
|
|
51
56
|
this.id = cfg.id ?? "stripe";
|
|
52
57
|
this.currencies = cfg.currencies;
|
|
53
58
|
this.api = cfg.api;
|
|
54
59
|
this.webhookKey = cfg.webhookKey;
|
|
55
|
-
this.clock = cfg.clock
|
|
60
|
+
this.clock = cfg.clock;
|
|
56
61
|
this.tolerance = cfg.tolerance ?? defaultToleranceSeconds;
|
|
57
62
|
this.ids = cfg.ids;
|
|
58
63
|
this.methods = cfg.methods;
|
|
@@ -106,12 +111,15 @@ export class StripeLeg {
|
|
|
106
111
|
// refundIn reverses a captured payment, in full or in part. Stripe supports
|
|
107
112
|
// both, so the leg accepts the full and partial refund kinds and rejects a
|
|
108
113
|
// counter-transfer it cannot express.
|
|
109
|
-
async refundIn(intentId, kind, reason) {
|
|
114
|
+
async refundIn(intentId, kind, amount, reason) {
|
|
110
115
|
if (kind !== RefundKind.Full && kind !== RefundKind.Partial) {
|
|
111
116
|
throw new Error(`stripe: cannot perform refund kind ${kind}`);
|
|
112
117
|
}
|
|
113
118
|
const pi = await this.api.findPaymentIntent(intentId);
|
|
114
|
-
|
|
119
|
+
// Stripe refunds the full capture when no amount is set, so a full refund
|
|
120
|
+
// leaves amount zero and a partial one carries the exact minor units to return.
|
|
121
|
+
const minor = kind === RefundKind.Partial ? minorToInteger(amount) : 0;
|
|
122
|
+
const refund = await this.api.createRefund({ paymentIntentId: pi.id, amount: minor, reason }, idempotencyKey(intentId, "refund"));
|
|
115
123
|
return {
|
|
116
124
|
intentId,
|
|
117
125
|
state: State.Refunded,
|
|
@@ -145,11 +153,6 @@ function signatureHeader(headers) {
|
|
|
145
153
|
}
|
|
146
154
|
return "";
|
|
147
155
|
}
|
|
148
|
-
// idempotencyKey binds a Stripe mutation to one protocol step so a retry reuses
|
|
149
|
-
// the original result instead of acting twice.
|
|
150
|
-
function idempotencyKey(ref, step) {
|
|
151
|
-
return `${idPrefix}:${ref}:${step}`;
|
|
152
|
-
}
|
|
153
156
|
// minorToInteger narrows a Money amount to the safe integer Stripe expects,
|
|
154
157
|
// refusing anything that would overflow. Fiat amounts fit comfortably; the guard
|
|
155
158
|
// exists so a token amount can never be sent to a card rail by mistake.
|
|
@@ -342,7 +345,7 @@ class HttpStripeApi {
|
|
|
342
345
|
Authorization: `Bearer ${this.secretKey}`,
|
|
343
346
|
"Stripe-Version": apiVersion,
|
|
344
347
|
};
|
|
345
|
-
const resp = await
|
|
348
|
+
const resp = await fetchWithTimeout(this.baseURL + path, { ...init, headers });
|
|
346
349
|
const body = await resp.text();
|
|
347
350
|
if (resp.status >= 300) {
|
|
348
351
|
throw new Error(`stripe: ${path} returned ${resp.status}: ${stripeErrorMessage(body)}`);
|
package/dist/src/bridge.js
CHANGED
|
@@ -83,14 +83,21 @@ export function applyInverseRate(dst, rate, srcCurrency, srcExponent) {
|
|
|
83
83
|
// parseRate reads a decimal rate like "129.45" into a numerator and denominator,
|
|
84
84
|
// so the conversion stays exact integer arithmetic with no floating point.
|
|
85
85
|
export function parseRate(rate) {
|
|
86
|
+
// Bound the length so an untrusted rate can't force a huge bigint parse and
|
|
87
|
+
// exponentiation in the FX math.
|
|
88
|
+
if (rate.length > 80) {
|
|
89
|
+
throw new Error("pact: rate has more than 80 characters");
|
|
90
|
+
}
|
|
91
|
+
// A non-negative integer part with no leading zeros and an optional fractional
|
|
92
|
+
// part; no sign, no radix prefix, no whitespace, matching the amount grammar so
|
|
93
|
+
// the SDKs never disagree on a rate's validity or value.
|
|
94
|
+
if (!/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(rate)) {
|
|
95
|
+
throw new Error(`pact: ${JSON.stringify(rate)} is not a canonical decimal rate`);
|
|
96
|
+
}
|
|
86
97
|
const dot = rate.indexOf(".");
|
|
87
98
|
const whole = dot < 0 ? rate : rate.slice(0, dot);
|
|
88
99
|
const frac = dot < 0 ? "" : rate.slice(dot + 1);
|
|
89
|
-
const
|
|
90
|
-
if (!/^\d+$/.test(digits)) {
|
|
91
|
-
throw new Error(`pact: ${JSON.stringify(rate)} is not a decimal rate`);
|
|
92
|
-
}
|
|
93
|
-
const num = BigInt(digits);
|
|
100
|
+
const num = BigInt(whole + frac);
|
|
94
101
|
const den = frac.length > 0 ? pow10(frac.length) : 1n;
|
|
95
102
|
return [num, den];
|
|
96
103
|
}
|
package/dist/src/canonical.js
CHANGED
|
@@ -20,6 +20,11 @@ export class CanonicalWriter {
|
|
|
20
20
|
this.push(b);
|
|
21
21
|
return this;
|
|
22
22
|
}
|
|
23
|
+
// str writes a string as its raw UTF-8 bytes. No Unicode normalization is
|
|
24
|
+
// applied: the bytes are hashed as given, so two participants that compose the
|
|
25
|
+
// same text in different Unicode forms produce different hashes. Callers that
|
|
26
|
+
// need them to agree must normalize before building a message; string fields are
|
|
27
|
+
// otherwise treated as opaque bytes.
|
|
23
28
|
str(s) {
|
|
24
29
|
return this.bytes(encoder.encode(s));
|
|
25
30
|
}
|
package/dist/src/client.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ import { type Intent, type Quote, type Authorization, type Settlement } from "./
|
|
|
4
4
|
import { type Signer, type Verifier } from "./signing.js";
|
|
5
5
|
import { type Bridge } from "./bridge.js";
|
|
6
6
|
import { type PayInLeg, type PayOutLeg, type PayInPreparation } from "./leg.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type AdapterEvent } from "./adapter.js";
|
|
8
8
|
import { type Policy } from "./router.js";
|
|
9
|
-
import { type KycProvider, type RiskHook } from "./compliance.js";
|
|
9
|
+
import { type KycProvider, type RiskHook, type SignerAuthorizer } from "./compliance.js";
|
|
10
10
|
import { type Ledger, type LedgerEvent } from "./ledger.js";
|
|
11
11
|
export type Clock = () => number;
|
|
12
12
|
export type IdGen = () => string;
|
|
@@ -21,6 +21,7 @@ export interface ClientConfig {
|
|
|
21
21
|
verifier: Verifier;
|
|
22
22
|
kyc?: KycProvider;
|
|
23
23
|
risk?: RiskHook;
|
|
24
|
+
signerAuth?: SignerAuthorizer;
|
|
24
25
|
clock?: Clock;
|
|
25
26
|
idGen?: IdGen;
|
|
26
27
|
skew?: number;
|
|
@@ -49,6 +50,7 @@ export declare class Client {
|
|
|
49
50
|
private readonly verifier;
|
|
50
51
|
private readonly kyc;
|
|
51
52
|
private readonly risk;
|
|
53
|
+
private readonly signerAuth;
|
|
52
54
|
private readonly clock;
|
|
53
55
|
private readonly idGen;
|
|
54
56
|
private readonly skew;
|
|
@@ -75,6 +77,10 @@ export declare class Client {
|
|
|
75
77
|
private holdAndDisburse;
|
|
76
78
|
private settleBridged;
|
|
77
79
|
private unwind;
|
|
80
|
+
refund(intentId: string, amount: Money, reason: string): Promise<{
|
|
81
|
+
settlement: Settlement;
|
|
82
|
+
state: State;
|
|
83
|
+
}>;
|
|
78
84
|
private recordFailure;
|
|
79
85
|
private finishAdvance;
|
|
80
86
|
private finish;
|