@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.
- package/dist/src/adapter.d.ts +18 -0
- package/dist/src/adapter.js +11 -0
- package/dist/src/adapters/erc20.d.ts +43 -0
- package/dist/src/adapters/erc20.js +126 -0
- package/dist/src/adapters/mpesa.d.ts +61 -0
- package/dist/src/adapters/mpesa.js +272 -0
- package/dist/src/adapters/paypal.d.ts +68 -0
- package/dist/src/adapters/paypal.js +279 -0
- package/dist/src/adapters/stripe.d.ts +61 -0
- package/dist/src/adapters/stripe.js +336 -0
- package/dist/src/bridge.d.ts +40 -0
- package/dist/src/bridge.js +99 -0
- package/dist/src/canonical.d.ts +14 -0
- package/dist/src/canonical.js +82 -0
- package/dist/src/client.d.ts +84 -0
- package/dist/src/client.js +510 -0
- package/dist/src/compliance.d.ts +23 -0
- package/dist/src/compliance.js +26 -0
- package/dist/src/crypto.d.ts +12 -0
- package/dist/src/crypto.js +50 -0
- package/dist/src/fake.d.ts +29 -0
- package/dist/src/fake.js +79 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.js +16 -0
- package/dist/src/ledger.d.ts +47 -0
- package/dist/src/ledger.js +84 -0
- package/dist/src/leg.d.ts +34 -0
- package/dist/src/leg.js +4 -0
- package/dist/src/message.d.ts +55 -0
- package/dist/src/message.js +88 -0
- package/dist/src/money.d.ts +16 -0
- package/dist/src/money.js +81 -0
- package/dist/src/payload.d.ts +11 -0
- package/dist/src/payload.js +52 -0
- package/dist/src/policy.d.ts +26 -0
- package/dist/src/policy.js +83 -0
- package/dist/src/protocol.d.ts +4 -0
- package/dist/src/protocol.js +15 -0
- package/dist/src/router.d.ts +16 -0
- package/dist/src/router.js +87 -0
- package/dist/src/signing.d.ts +27 -0
- package/dist/src/signing.js +72 -0
- package/dist/src/state.d.ts +23 -0
- package/dist/src/state.js +76 -0
- package/dist/src/wire.d.ts +30 -0
- package/dist/src/wire.js +221 -0
- package/dist/test/erc20.test.d.ts +1 -0
- package/dist/test/erc20.test.js +131 -0
- package/dist/test/lifecycle.test.d.ts +1 -0
- package/dist/test/lifecycle.test.js +212 -0
- package/dist/test/mpesa.test.d.ts +1 -0
- package/dist/test/mpesa.test.js +180 -0
- package/dist/test/payload.test.d.ts +1 -0
- package/dist/test/payload.test.js +32 -0
- package/dist/test/paypal.test.d.ts +1 -0
- package/dist/test/paypal.test.js +140 -0
- package/dist/test/policy.test.d.ts +1 -0
- package/dist/test/policy.test.js +131 -0
- package/dist/test/stripe.test.d.ts +1 -0
- package/dist/test/stripe.test.js +176 -0
- package/dist/test/vectors.test.d.ts +1 -0
- package/dist/test/vectors.test.js +91 -0
- package/dist/test/wire.test.d.ts +1 -0
- package/dist/test/wire.test.js +104 -0
- package/package.json +50 -0
- package/src/adapter.ts +32 -0
- package/src/adapters/erc20.ts +181 -0
- package/src/adapters/mpesa.ts +408 -0
- package/src/adapters/paypal.ts +409 -0
- package/src/adapters/stripe.ts +478 -0
- package/src/bridge.ts +148 -0
- package/src/canonical.ts +94 -0
- package/src/client.ts +605 -0
- package/src/compliance.ts +65 -0
- package/src/crypto.ts +68 -0
- package/src/fake.ts +96 -0
- package/src/index.ts +106 -0
- package/src/ledger.ts +145 -0
- package/src/leg.ts +65 -0
- package/src/message.ts +178 -0
- package/src/money.ts +87 -0
- package/src/payload.ts +58 -0
- package/src/policy.ts +110 -0
- package/src/protocol.ts +19 -0
- package/src/router.ts +97 -0
- package/src/signing.ts +92 -0
- package/src/state.ts +76 -0
- package/src/wire.ts +248 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Money } from "./money.js";
|
|
2
|
+
import type { Quote } from "./message.js";
|
|
3
|
+
|
|
4
|
+
// KycLimits caps what an identity may transact. An absent perPayment means no cap
|
|
5
|
+
// on that dimension.
|
|
6
|
+
export interface KycLimits {
|
|
7
|
+
perPayment?: Money;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// KycStatus is what a KycProvider knows about an identity. The kernel uses only
|
|
11
|
+
// the limits to gate quotes; tier and jurisdiction are carried for the host's
|
|
12
|
+
// own policy.
|
|
13
|
+
export interface KycStatus {
|
|
14
|
+
tier: string;
|
|
15
|
+
jurisdiction: string;
|
|
16
|
+
limits: KycLimits;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// KycProvider resolves an identity's compliance standing. It is a seam: the
|
|
20
|
+
// kernel ships a permissive default and holds no policy of its own. A real
|
|
21
|
+
// deployment supplies one before any live-money adapter is enabled.
|
|
22
|
+
export interface KycProvider {
|
|
23
|
+
status(identity: string): Promise<KycStatus>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// permissiveKyc imposes no limits, which is only acceptable for test-mode and
|
|
27
|
+
// testnet adapters.
|
|
28
|
+
export const permissiveKyc: KycProvider = {
|
|
29
|
+
async status(): Promise<KycStatus> {
|
|
30
|
+
return { tier: "unverified", jurisdiction: "", limits: {} };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// withinLimits reports whether a quote's source amount is inside an identity's
|
|
35
|
+
// per-payment cap. An absent cap or a currency mismatch is treated as no
|
|
36
|
+
// applicable limit.
|
|
37
|
+
export function withinLimits(status: KycStatus, quote: Quote): boolean {
|
|
38
|
+
const cap = status.limits.perPayment;
|
|
39
|
+
if (!cap) return true;
|
|
40
|
+
try {
|
|
41
|
+
return quote.srcAmount.cmp(cap) <= 0;
|
|
42
|
+
} catch {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// RiskDecision is the outcome of a RiskHook consultation.
|
|
48
|
+
export interface RiskDecision {
|
|
49
|
+
allow: boolean;
|
|
50
|
+
reason: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// RiskHook is consulted immediately before an authorization is produced. It
|
|
54
|
+
// receives the identity that will sign, so a policy can reason about who is
|
|
55
|
+
// paying, not just what. A veto blocks it. Like KycProvider it is a seam with a
|
|
56
|
+
// permissive default.
|
|
57
|
+
export interface RiskHook {
|
|
58
|
+
evaluate(identity: string, intent: import("./message.js").Intent, quote: Quote): RiskDecision;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const allowRisk: RiskHook = {
|
|
62
|
+
evaluate(): RiskDecision {
|
|
63
|
+
return { allow: true, reason: "" };
|
|
64
|
+
},
|
|
65
|
+
};
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
createPrivateKey,
|
|
4
|
+
createPublicKey,
|
|
5
|
+
sign as nodeSign,
|
|
6
|
+
verify as nodeVerify,
|
|
7
|
+
type KeyObject,
|
|
8
|
+
} from "node:crypto";
|
|
9
|
+
|
|
10
|
+
// SHA-256 of a byte string. Every message hash and receipt commitment flows
|
|
11
|
+
// through it.
|
|
12
|
+
export function sha256(data: Uint8Array): Uint8Array {
|
|
13
|
+
return new Uint8Array(createHash("sha256").update(data).digest());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The fixed DER prefixes that wrap a raw 32-byte Ed25519 seed or public key so
|
|
17
|
+
// the platform key APIs will import them. The seed becomes a PKCS8 private key;
|
|
18
|
+
// the public key becomes an SPKI public key.
|
|
19
|
+
const pkcs8SeedPrefix = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
20
|
+
const spkiPublicPrefix = Buffer.from("302a300506032b6570032100", "hex");
|
|
21
|
+
|
|
22
|
+
export interface Ed25519KeyPair {
|
|
23
|
+
privateKey: KeyObject;
|
|
24
|
+
publicKey: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ed25519KeyFromSeed derives a signing key pair from a 32-byte seed. Ed25519 is
|
|
28
|
+
// deterministic, so a given seed and message always yield the same signature,
|
|
29
|
+
// which is what lets the cross-language vectors pin an exact signature.
|
|
30
|
+
export function ed25519KeyFromSeed(seed: Uint8Array): Ed25519KeyPair {
|
|
31
|
+
if (seed.length !== 32) {
|
|
32
|
+
throw new Error("pact: ed25519 seed must be 32 bytes");
|
|
33
|
+
}
|
|
34
|
+
const der = Buffer.concat([pkcs8SeedPrefix, Buffer.from(seed)]);
|
|
35
|
+
const privateKey = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
|
|
36
|
+
const spki = createPublicKey(privateKey).export({ format: "der", type: "spki" }) as Buffer;
|
|
37
|
+
return { privateKey, publicKey: new Uint8Array(spki.subarray(spki.length - 32)) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ed25519Sign(privateKey: KeyObject, message: Uint8Array): Uint8Array {
|
|
41
|
+
return new Uint8Array(nodeSign(null, Buffer.from(message), privateKey));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ed25519Verify(publicKeyRaw: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean {
|
|
45
|
+
if (publicKeyRaw.length !== 32) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const spki = Buffer.concat([spkiPublicPrefix, Buffer.from(publicKeyRaw)]);
|
|
49
|
+
const publicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
|
|
50
|
+
return nodeVerify(null, Buffer.from(message), publicKey, Buffer.from(signature));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// toHex and fromHex render byte strings for the wire and for test vectors.
|
|
54
|
+
export function toHex(bytes: Uint8Array): string {
|
|
55
|
+
return Buffer.from(bytes).toString("hex");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function fromHex(hex: string): Uint8Array {
|
|
59
|
+
return new Uint8Array(Buffer.from(hex, "hex"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
63
|
+
if (a.length !== b.length) return false;
|
|
64
|
+
for (let i = 0; i < a.length; i++) {
|
|
65
|
+
if (a[i] !== b[i]) return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
package/src/fake.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export { ID, VERSION, domain, DEFAULT_SKEW_MILLIS } from "./protocol.js";
|
|
2
|
+
export { Money } from "./money.js";
|
|
3
|
+
export { State, stateName, canTransition, isTerminal, TransitionError } from "./state.js";
|
|
4
|
+
export { CanonicalWriter, hashPreimage } from "./canonical.js";
|
|
5
|
+
export {
|
|
6
|
+
type Intent,
|
|
7
|
+
type Quote,
|
|
8
|
+
type Authorization,
|
|
9
|
+
type Settlement,
|
|
10
|
+
BRIDGE_PASSTHROUGH,
|
|
11
|
+
intentPreimage,
|
|
12
|
+
intentHash,
|
|
13
|
+
quotePreimage,
|
|
14
|
+
quoteHash,
|
|
15
|
+
isDirect,
|
|
16
|
+
signingPreimage,
|
|
17
|
+
authorizationHash,
|
|
18
|
+
receiptHash,
|
|
19
|
+
corridorReceipt,
|
|
20
|
+
} from "./message.js";
|
|
21
|
+
export {
|
|
22
|
+
type Signer,
|
|
23
|
+
type Verifier,
|
|
24
|
+
authorize,
|
|
25
|
+
verifyAuthorization,
|
|
26
|
+
Ed25519Signer,
|
|
27
|
+
Ed25519Verifier,
|
|
28
|
+
BadSignatureError,
|
|
29
|
+
} from "./signing.js";
|
|
30
|
+
export { RefundKind, type AdapterEvent, type WebhookParser } from "./adapter.js";
|
|
31
|
+
export {
|
|
32
|
+
railInList,
|
|
33
|
+
type PayInLeg,
|
|
34
|
+
type PayOutLeg,
|
|
35
|
+
type PayInCapabilities,
|
|
36
|
+
type PayOutCapabilities,
|
|
37
|
+
type CollectResult,
|
|
38
|
+
type DisburseResult,
|
|
39
|
+
} from "./leg.js";
|
|
40
|
+
export {
|
|
41
|
+
BRIDGE_USDC,
|
|
42
|
+
PassThroughBridge,
|
|
43
|
+
UsdcBridge,
|
|
44
|
+
applyRate,
|
|
45
|
+
applyInverseRate,
|
|
46
|
+
parseRate,
|
|
47
|
+
type Bridge,
|
|
48
|
+
type BridgeQuote,
|
|
49
|
+
type ConvertResult,
|
|
50
|
+
type RateSource,
|
|
51
|
+
type EscrowVault,
|
|
52
|
+
} from "./bridge.js";
|
|
53
|
+
export {
|
|
54
|
+
type KycProvider,
|
|
55
|
+
type KycStatus,
|
|
56
|
+
type KycLimits,
|
|
57
|
+
type RiskHook,
|
|
58
|
+
type RiskDecision,
|
|
59
|
+
permissiveKyc,
|
|
60
|
+
allowRisk,
|
|
61
|
+
withinLimits,
|
|
62
|
+
} from "./compliance.js";
|
|
63
|
+
export { PolicyKind, type Policy, route, isExpired, NoQuoteError } from "./router.js";
|
|
64
|
+
export {
|
|
65
|
+
MemoryLedger,
|
|
66
|
+
eventReceipt,
|
|
67
|
+
chainLeaf,
|
|
68
|
+
IdempotencyConflictError,
|
|
69
|
+
type Ledger,
|
|
70
|
+
type LedgerEvent,
|
|
71
|
+
type Transition,
|
|
72
|
+
} from "./ledger.js";
|
|
73
|
+
export {
|
|
74
|
+
Client,
|
|
75
|
+
type ClientConfig,
|
|
76
|
+
type IntentSpec,
|
|
77
|
+
type Funding,
|
|
78
|
+
type Clock,
|
|
79
|
+
type IdGen,
|
|
80
|
+
systemClock,
|
|
81
|
+
randomId,
|
|
82
|
+
recipientDestination,
|
|
83
|
+
} from "./client.js";
|
|
84
|
+
export { FakeLeg, FakeRates, FakeVault } from "./fake.js";
|
|
85
|
+
export {
|
|
86
|
+
WireKind,
|
|
87
|
+
type WireMessage,
|
|
88
|
+
encodeIntent,
|
|
89
|
+
encodeQuote,
|
|
90
|
+
encodeAuthorization,
|
|
91
|
+
encodeSettlement,
|
|
92
|
+
encodeMessage,
|
|
93
|
+
decodeMessage,
|
|
94
|
+
decodeIntent,
|
|
95
|
+
decodeQuote,
|
|
96
|
+
decodeAuthorization,
|
|
97
|
+
decodeSettlement,
|
|
98
|
+
} from "./wire.js";
|
|
99
|
+
export {
|
|
100
|
+
PAYLOAD_VERSION,
|
|
101
|
+
encodePayload,
|
|
102
|
+
decodePayload,
|
|
103
|
+
isPayload,
|
|
104
|
+
NotPayloadError,
|
|
105
|
+
UnsupportedPayloadVersionError,
|
|
106
|
+
} from "./payload.js";
|
package/src/ledger.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { State, stateName, canTransition, TransitionError } from "./state.js";
|
|
2
|
+
import { CanonicalWriter, hashPreimage } from "./canonical.js";
|
|
3
|
+
import { domain } from "./protocol.js";
|
|
4
|
+
import { bytesEqual } from "./crypto.js";
|
|
5
|
+
import type { Intent, Quote, Authorization, Settlement } from "./message.js";
|
|
6
|
+
import type { CollectResult } from "./leg.js";
|
|
7
|
+
import type { ConvertResult } from "./bridge.js";
|
|
8
|
+
|
|
9
|
+
// LedgerEvent is one immutable step in an intent's history. State is never
|
|
10
|
+
// mutated in place; the current state is the last event's state, and the whole
|
|
11
|
+
// history folds into a single Merkle head.
|
|
12
|
+
export interface LedgerEvent {
|
|
13
|
+
seq: number;
|
|
14
|
+
intentId: string;
|
|
15
|
+
state: State;
|
|
16
|
+
payloadHash: Uint8Array;
|
|
17
|
+
receipt: Uint8Array;
|
|
18
|
+
leaf: Uint8Array;
|
|
19
|
+
intent?: Intent;
|
|
20
|
+
quote?: Quote;
|
|
21
|
+
authorization?: Authorization;
|
|
22
|
+
settlement?: Settlement;
|
|
23
|
+
// collect and bridge persist the intermediate results a corridor needs to
|
|
24
|
+
// resume: what the pay-in leg collected, and what the bridge converted. They
|
|
25
|
+
// let settlement be driven forward by later provider events without holding
|
|
26
|
+
// anything open between them.
|
|
27
|
+
collect?: CollectResult;
|
|
28
|
+
bridge?: ConvertResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Transition is a proposed append: the target state, the hash of the causing
|
|
32
|
+
// message, and the message itself for durable storage.
|
|
33
|
+
export interface Transition {
|
|
34
|
+
intentId: string;
|
|
35
|
+
to: State;
|
|
36
|
+
payloadHash: Uint8Array;
|
|
37
|
+
intent?: Intent;
|
|
38
|
+
quote?: Quote;
|
|
39
|
+
authorization?: Authorization;
|
|
40
|
+
settlement?: Settlement;
|
|
41
|
+
collect?: CollectResult;
|
|
42
|
+
bridge?: ConvertResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class IdempotencyConflictError extends Error {
|
|
46
|
+
constructor() {
|
|
47
|
+
super("pact: idempotency conflict: step already recorded with a different payload");
|
|
48
|
+
this.name = "IdempotencyConflictError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Ledger is the append-only store of payment history. The kernel ships an
|
|
53
|
+
// in-memory reference; durable backends implement the same interface and must
|
|
54
|
+
// preserve the same event order, receipts, and Merkle head.
|
|
55
|
+
export interface Ledger {
|
|
56
|
+
apply(t: Transition): LedgerEvent;
|
|
57
|
+
state(intentId: string): State;
|
|
58
|
+
events(intentId: string): LedgerEvent[];
|
|
59
|
+
head(intentId: string): Uint8Array | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const zeroLeaf = new Uint8Array(32);
|
|
63
|
+
|
|
64
|
+
// MemoryLedger is the in-memory reference ledger.
|
|
65
|
+
export class MemoryLedger implements Ledger {
|
|
66
|
+
private readonly byIntent = new Map<string, LedgerEvent[]>();
|
|
67
|
+
|
|
68
|
+
apply(t: Transition): LedgerEvent {
|
|
69
|
+
const history = this.byIntent.get(t.intentId) ?? [];
|
|
70
|
+
|
|
71
|
+
// A repeat of the same step is a no-op returning the first result; the same
|
|
72
|
+
// step with a different payload is a conflict.
|
|
73
|
+
for (const e of history) {
|
|
74
|
+
if (e.state === t.to) {
|
|
75
|
+
if (bytesEqual(e.payloadHash, t.payloadHash)) return e;
|
|
76
|
+
throw new IdempotencyConflictError();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const current = history.length > 0 ? (history[history.length - 1] as LedgerEvent).state : State.Unspecified;
|
|
81
|
+
if (!canTransition(current, t.to)) {
|
|
82
|
+
throw new TransitionError(current, t.to);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const prevLeaf = history.length > 0 ? (history[history.length - 1] as LedgerEvent).leaf : zeroLeaf;
|
|
86
|
+
const seq = history.length + 1;
|
|
87
|
+
const receipt = eventReceipt(seq, t.intentId, t.to, t.payloadHash);
|
|
88
|
+
const leaf = chainLeaf(prevLeaf, receipt);
|
|
89
|
+
|
|
90
|
+
const event: LedgerEvent = {
|
|
91
|
+
seq,
|
|
92
|
+
intentId: t.intentId,
|
|
93
|
+
state: t.to,
|
|
94
|
+
payloadHash: t.payloadHash.slice(),
|
|
95
|
+
receipt,
|
|
96
|
+
leaf,
|
|
97
|
+
...(t.intent ? { intent: t.intent } : {}),
|
|
98
|
+
...(t.quote ? { quote: t.quote } : {}),
|
|
99
|
+
...(t.authorization ? { authorization: t.authorization } : {}),
|
|
100
|
+
...(t.settlement ? { settlement: t.settlement } : {}),
|
|
101
|
+
...(t.collect ? { collect: t.collect } : {}),
|
|
102
|
+
...(t.bridge ? { bridge: t.bridge } : {}),
|
|
103
|
+
};
|
|
104
|
+
this.byIntent.set(t.intentId, [...history, event]);
|
|
105
|
+
return event;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
state(intentId: string): State {
|
|
109
|
+
const history = this.byIntent.get(intentId);
|
|
110
|
+
if (!history || history.length === 0) return State.Unspecified;
|
|
111
|
+
return (history[history.length - 1] as LedgerEvent).state;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
events(intentId: string): LedgerEvent[] {
|
|
115
|
+
return [...(this.byIntent.get(intentId) ?? [])];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
head(intentId: string): Uint8Array | undefined {
|
|
119
|
+
const history = this.byIntent.get(intentId);
|
|
120
|
+
if (!history || history.length === 0) return undefined;
|
|
121
|
+
return (history[history.length - 1] as LedgerEvent).leaf;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// eventReceipt commits to an event's identity independent of the Merkle chaining,
|
|
126
|
+
// so a single event can be shown and checked on its own.
|
|
127
|
+
export function eventReceipt(seq: number, intentId: string, state: State, payloadHash: Uint8Array): Uint8Array {
|
|
128
|
+
return hashPreimage(
|
|
129
|
+
new CanonicalWriter()
|
|
130
|
+
.str(domain("event"))
|
|
131
|
+
.u64(seq)
|
|
132
|
+
.str(intentId)
|
|
133
|
+
.str(stateName[state])
|
|
134
|
+
.bytes(payloadHash)
|
|
135
|
+
.preimage(),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// chainLeaf folds one event's receipt into the running Merkle head.
|
|
140
|
+
export function chainLeaf(prevLeaf: Uint8Array, receipt: Uint8Array): Uint8Array {
|
|
141
|
+
const joined = new Uint8Array(prevLeaf.length + receipt.length);
|
|
142
|
+
joined.set(prevLeaf, 0);
|
|
143
|
+
joined.set(receipt, prevLeaf.length);
|
|
144
|
+
return hashPreimage(joined);
|
|
145
|
+
}
|
package/src/leg.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Money } from "./money.js";
|
|
2
|
+
import type { Quote, Authorization, Settlement } from "./message.js";
|
|
3
|
+
import type { RefundKind } from "./adapter.js";
|
|
4
|
+
|
|
5
|
+
// A corridor is built from two legs. A provider adapter implements the legs it
|
|
6
|
+
// can serve: a pay-in leg collects from the payer, a pay-out leg disburses to
|
|
7
|
+
// the recipient. Many providers serve only one; some serve both. The kernel
|
|
8
|
+
// composes them and never holds funds itself.
|
|
9
|
+
|
|
10
|
+
// PayInCapabilities describes what a pay-in leg can collect. Methods lists the
|
|
11
|
+
// funding surfaces the leg exposes without a separate adapter — a card leg via a
|
|
12
|
+
// processor exposes card plus the wallets that ride on it. Methods are
|
|
13
|
+
// informational; routing is on the rail.
|
|
14
|
+
export interface PayInCapabilities {
|
|
15
|
+
rails: string[];
|
|
16
|
+
currencies: string[];
|
|
17
|
+
methods?: string[];
|
|
18
|
+
refunds: RefundKind;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// CollectResult is what a pay-in leg returns once it has taken the payer's
|
|
22
|
+
// funds. received is the amount actually collected, which the bridge converts.
|
|
23
|
+
export interface CollectResult {
|
|
24
|
+
providerRef: string;
|
|
25
|
+
received: Money;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// PayInLeg collects the payer's funds in the source currency. deliverTo is where
|
|
29
|
+
// the collected funds should land: the recipient's handle for a direct corridor,
|
|
30
|
+
// or the bridge escrow for a bridged one. The kernel computes it, so the leg does
|
|
31
|
+
// not need to know which corridor shape it is serving.
|
|
32
|
+
export interface PayInLeg {
|
|
33
|
+
id: string;
|
|
34
|
+
payInCapabilities(): PayInCapabilities;
|
|
35
|
+
collect(intentId: string, quote: Quote, auth: Authorization, deliverTo: string): Promise<CollectResult>;
|
|
36
|
+
refundIn(intentId: string, kind: RefundKind, reason: string): Promise<Settlement>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// PayOutCapabilities describes what a pay-out leg can deliver. reversible reports
|
|
40
|
+
// whether a completed payout can be pulled back; an irreversible rail answers a
|
|
41
|
+
// refund with a counter-transfer, never a reversal.
|
|
42
|
+
export interface PayOutCapabilities {
|
|
43
|
+
rails: string[];
|
|
44
|
+
currencies: string[];
|
|
45
|
+
reversible: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// DisburseResult is what a pay-out leg returns once it has sent funds to the
|
|
49
|
+
// recipient.
|
|
50
|
+
export interface DisburseResult {
|
|
51
|
+
providerRef: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// PayOutLeg delivers funds to the recipient in the destination currency.
|
|
55
|
+
export interface PayOutLeg {
|
|
56
|
+
id: string;
|
|
57
|
+
payOutCapabilities(): PayOutCapabilities;
|
|
58
|
+
disburse(intentId: string, quote: Quote, recipientRef: string): Promise<DisburseResult>;
|
|
59
|
+
reverseOut(intentId: string, reason: string): Promise<Settlement>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// railInList reports whether a set of rails contains the requested rail.
|
|
63
|
+
export function railInList(rails: string[], rail: string): boolean {
|
|
64
|
+
return rails.includes(rail);
|
|
65
|
+
}
|