@myzonerocks/pact 0.1.3 → 0.1.6
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 +3 -3
- package/dist/src/adapter.d.ts +1 -1
- package/dist/src/adapters/erc20.d.ts +30 -4
- package/dist/src/adapters/erc20.js +105 -50
- package/dist/src/adapters/http.d.ts +2 -0
- package/dist/src/adapters/http.js +12 -0
- package/dist/src/adapters/mpesa.d.ts +30 -4
- package/dist/src/adapters/mpesa.js +106 -22
- package/dist/src/adapters/paypal.d.ts +23 -2
- package/dist/src/adapters/paypal.js +91 -29
- 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 +10 -2
- package/dist/src/client.js +215 -30
- 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/state.js +3 -1
- 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 +150 -51
- package/src/adapters/http.ts +14 -0
- package/src/adapters/mpesa.ts +148 -28
- package/src/adapters/paypal.ts +138 -28
- package/src/adapters/stripe.ts +16 -13
- package/src/bridge.ts +12 -5
- package/src/canonical.ts +5 -0
- package/src/client.ts +228 -33
- 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/state.ts +3 -1
- package/src/wire.ts +20 -3
- package/src/fake.ts +0 -96
package/src/ledger.ts
CHANGED
|
@@ -49,11 +49,19 @@ export class IdempotencyConflictError extends Error {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// ApplyResult carries the appended event and whether this call actually appended
|
|
53
|
+
// it (created) or found the step already recorded and returned the existing one,
|
|
54
|
+
// so a caller can claim a step before a side effect and abort if it did not win.
|
|
55
|
+
export interface ApplyResult {
|
|
56
|
+
event: LedgerEvent;
|
|
57
|
+
created: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
// Ledger is the append-only store of payment history. The kernel ships an
|
|
53
61
|
// in-memory reference; durable backends implement the same interface and must
|
|
54
62
|
// preserve the same event order, receipts, and Merkle head.
|
|
55
63
|
export interface Ledger {
|
|
56
|
-
apply(t: Transition):
|
|
64
|
+
apply(t: Transition): ApplyResult;
|
|
57
65
|
state(intentId: string): State;
|
|
58
66
|
events(intentId: string): LedgerEvent[];
|
|
59
67
|
head(intentId: string): Uint8Array | undefined;
|
|
@@ -65,14 +73,14 @@ const zeroLeaf = new Uint8Array(32);
|
|
|
65
73
|
export class MemoryLedger implements Ledger {
|
|
66
74
|
private readonly byIntent = new Map<string, LedgerEvent[]>();
|
|
67
75
|
|
|
68
|
-
apply(t: Transition):
|
|
76
|
+
apply(t: Transition): ApplyResult {
|
|
69
77
|
const history = this.byIntent.get(t.intentId) ?? [];
|
|
70
78
|
|
|
71
79
|
// A repeat of the same step is a no-op returning the first result; the same
|
|
72
80
|
// step with a different payload is a conflict.
|
|
73
81
|
for (const e of history) {
|
|
74
82
|
if (e.state === t.to) {
|
|
75
|
-
if (bytesEqual(e.payloadHash, t.payloadHash)) return e;
|
|
83
|
+
if (bytesEqual(e.payloadHash, t.payloadHash)) return { event: e, created: false };
|
|
76
84
|
throw new IdempotencyConflictError();
|
|
77
85
|
}
|
|
78
86
|
}
|
|
@@ -102,7 +110,7 @@ export class MemoryLedger implements Ledger {
|
|
|
102
110
|
...(t.bridge ? { bridge: t.bridge } : {}),
|
|
103
111
|
};
|
|
104
112
|
this.byIntent.set(t.intentId, [...history, event]);
|
|
105
|
-
return event;
|
|
113
|
+
return { event, created: true };
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
state(intentId: string): State {
|
package/src/leg.ts
CHANGED
|
@@ -33,7 +33,10 @@ export interface PayInLeg {
|
|
|
33
33
|
id: string;
|
|
34
34
|
payInCapabilities(): PayInCapabilities;
|
|
35
35
|
collect(intentId: string, quote: Quote, auth: Authorization, deliverTo: string): Promise<CollectResult>;
|
|
36
|
-
refundIn
|
|
36
|
+
// refundIn returns amount to the payer. The amount is explicit so a leg can
|
|
37
|
+
// honour a partial refund, not only a full one; a leg that can refund at all
|
|
38
|
+
// must return exactly it.
|
|
39
|
+
refundIn(intentId: string, kind: RefundKind, amount: Money, reason: string): Promise<Settlement>;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// PayInPreparation is what an interactive pay-in leg hands back so the payer can
|
package/src/message.ts
CHANGED
|
@@ -153,14 +153,17 @@ export function receiptHash(authHash: Uint8Array, providerTxRef: string, state:
|
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// corridorReceipt binds both legs of a bridged corridor and the
|
|
157
|
-
// commitment, so the payer can prove they funded X and
|
|
158
|
-
// they received Y as a single transaction, without
|
|
156
|
+
// corridorReceipt binds both legs of a bridged corridor and the amount that
|
|
157
|
+
// actually moved into one commitment, so the payer can prove they funded X and
|
|
158
|
+
// the recipient can prove they received Y as a single transaction, without
|
|
159
|
+
// revealing anything else. The amount is the value the corridor really delivered
|
|
160
|
+
// on settle or returned on refund — not the quoted rate, which the conversion may
|
|
161
|
+
// not have matched — so the receipt attests the outcome, not the plan.
|
|
159
162
|
export function corridorReceipt(
|
|
160
163
|
authHash: Uint8Array,
|
|
161
164
|
payInProviderRef: string,
|
|
162
165
|
payOutProviderRef: string,
|
|
163
|
-
|
|
166
|
+
amount: Money,
|
|
164
167
|
bridgeReceiptRef: string,
|
|
165
168
|
state: State,
|
|
166
169
|
): Uint8Array {
|
|
@@ -170,7 +173,7 @@ export function corridorReceipt(
|
|
|
170
173
|
.bytes(authHash)
|
|
171
174
|
.str(payInProviderRef)
|
|
172
175
|
.str(payOutProviderRef)
|
|
173
|
-
.
|
|
176
|
+
.money(amount)
|
|
174
177
|
.str(bridgeReceiptRef)
|
|
175
178
|
.str(stateName[state])
|
|
176
179
|
.preimage(),
|
package/src/money.ts
CHANGED
|
@@ -12,6 +12,14 @@ export class Money {
|
|
|
12
12
|
private readonly amount: bigint,
|
|
13
13
|
) {}
|
|
14
14
|
|
|
15
|
+
// zero is the empty-currency, zero-amount sentinel used where a corridor moved
|
|
16
|
+
// no value — a failure receipt, or an absent bridge result. It is not a valid
|
|
17
|
+
// wire amount (an empty currency never parses), only an internal placeholder,
|
|
18
|
+
// and it encodes canonically as minor "0", currency "", exponent 0.
|
|
19
|
+
static zero(): Money {
|
|
20
|
+
return new Money("", 0, 0n);
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
static create(currency: string, exponent: number, amount: bigint): Money {
|
|
16
24
|
if (!currencyPattern.test(currency)) {
|
|
17
25
|
throw new Error("pact: currency must match [A-Z0-9]{1,16}");
|
|
@@ -26,10 +34,18 @@ export class Money {
|
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
// parse builds Money from a decimal-ASCII minor-unit string, the form used on
|
|
29
|
-
// the wire.
|
|
37
|
+
// the wire. The grammar is a bare non-negative integer — no sign, no radix
|
|
38
|
+
// prefix, no whitespace, no leading zeros — pinned identically across every SDK
|
|
39
|
+
// so two participants never disagree on whether a message is valid or on the
|
|
40
|
+
// value it decodes to.
|
|
30
41
|
static parse(currency: string, exponent: number, minor: string): Money {
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
// Bound the length so an untrusted string can't force a huge bigint parse
|
|
43
|
+
// before capability limits ever see it; eighty digits is far past any amount.
|
|
44
|
+
if (minor.length > 80) {
|
|
45
|
+
throw new Error("pact: amount has more than 80 digits");
|
|
46
|
+
}
|
|
47
|
+
if (!/^(0|[1-9][0-9]*)$/.test(minor)) {
|
|
48
|
+
throw new Error(`pact: ${minor} is not a canonical base-10 integer`);
|
|
33
49
|
}
|
|
34
50
|
return Money.create(currency, exponent, BigInt(minor));
|
|
35
51
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -14,6 +14,15 @@ export function domain(kind: string): string {
|
|
|
14
14
|
return domainPrefix + kind;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// idempotencyKey binds a provider mutation to one protocol step so a retry reuses
|
|
18
|
+
// the same key and the provider deduplicates it into a single side effect. ref is
|
|
19
|
+
// the stable reference the step acts on (the intent id, or a provider object id);
|
|
20
|
+
// step names the operation. Every adapter derives its provider idempotency key
|
|
21
|
+
// this way, so the same (ref, step) always maps to the same key.
|
|
22
|
+
export function idempotencyKey(ref: string, step: string): string {
|
|
23
|
+
return `${ID}:${ref}:${step}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
// DEFAULT_SKEW_MILLIS is the tolerated clock difference when deciding whether an
|
|
18
27
|
// intent or quote has expired.
|
|
19
28
|
export const DEFAULT_SKEW_MILLIS = 120_000;
|
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
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
return
|
|
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/state.ts
CHANGED
|
@@ -52,7 +52,9 @@ const transitions: Record<State, ReadonlySet<State>> = {
|
|
|
52
52
|
[State.Held]: new Set([State.Disbursing, State.Refunding, State.Expired]),
|
|
53
53
|
[State.Disbursing]: new Set([State.Settled, State.Failed, State.Refunding]),
|
|
54
54
|
[State.Refunding]: new Set([State.Refunded, State.Failed]),
|
|
55
|
-
|
|
55
|
+
// A settled intent refunds through the same refunding step a bridged unwind uses,
|
|
56
|
+
// so the refund is claimed before any money moves and cannot fire twice.
|
|
57
|
+
[State.Settled]: new Set([State.Refunding, State.Refunded]),
|
|
56
58
|
[State.Failed]: new Set(),
|
|
57
59
|
[State.Expired]: new Set(),
|
|
58
60
|
[State.Refunded]: new 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
|
-
|
|
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()
|
|
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
|
-
}
|