@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
@@ -0,0 +1,83 @@
1
+ import { Money } from "./money.js";
2
+ function railAllowed(tier, rail) {
3
+ if (!tier.allowedRails || tier.allowedRails.length === 0)
4
+ return true;
5
+ return tier.allowedRails.includes(rail);
6
+ }
7
+ // TieredKyc resolves an identity to its tier and reports the per-payment limit the
8
+ // kernel's router already enforces. Identities with no assigned tier fall to the
9
+ // configured fallback tier.
10
+ export class TieredKyc {
11
+ assigned;
12
+ fallback;
13
+ tiers;
14
+ constructor(tiers, assigned, fallback) {
15
+ this.assigned = assigned;
16
+ this.fallback = fallback;
17
+ this.tiers = new Map(tiers.map((t) => [t.name, t]));
18
+ }
19
+ // tierFor returns the tier governing an identity.
20
+ tierFor(identity) {
21
+ const name = this.assigned[identity];
22
+ if (name !== undefined) {
23
+ const tier = this.tiers.get(name);
24
+ if (tier)
25
+ return tier;
26
+ }
27
+ return this.fallback;
28
+ }
29
+ async status(identity) {
30
+ const tier = this.tierFor(identity);
31
+ return {
32
+ tier: tier.name,
33
+ jurisdiction: tier.jurisdiction,
34
+ limits: tier.perPayment ? { perPayment: tier.perPayment } : {},
35
+ };
36
+ }
37
+ }
38
+ // WindowRisk enforces the per-tier rail allow-list and a rolling-window spend
39
+ // cap. It owns its own spend ledger, so the state a compliance decision needs
40
+ // never leaks into the kernel.
41
+ export class WindowRisk {
42
+ kyc;
43
+ windowMillis;
44
+ now;
45
+ spend = new Map();
46
+ constructor(kyc, windowMillis, now) {
47
+ this.kyc = kyc;
48
+ this.windowMillis = windowMillis;
49
+ this.now = now;
50
+ }
51
+ // evaluate vetoes a payment whose rail the tier forbids, or whose source amount
52
+ // would push the identity's spend over the window cap. An allowed payment is
53
+ // recorded so it counts against the window for the next one.
54
+ evaluate(identity, _intent, quote) {
55
+ const tier = this.kyc.tierFor(identity);
56
+ if (!railAllowed(tier, quote.payInRail) || !railAllowed(tier, quote.payOutRail)) {
57
+ return { allow: false, reason: "rail not permitted for this tier" };
58
+ }
59
+ if (!tier.windowSpend) {
60
+ return { allow: true, reason: "" };
61
+ }
62
+ const now = this.now();
63
+ const horizon = now - this.windowMillis;
64
+ const kept = (this.spend.get(identity) ?? []).filter((e) => e.atMillis > horizon);
65
+ let total = Money.create(tier.windowSpend.currency, tier.windowSpend.exponent, 0n);
66
+ try {
67
+ for (const e of kept)
68
+ total = total.add(e.amount);
69
+ const projected = total.add(quote.srcAmount);
70
+ if (projected.cmp(tier.windowSpend) > 0) {
71
+ this.spend.set(identity, kept);
72
+ return { allow: false, reason: "payment exceeds the rolling spend limit" };
73
+ }
74
+ }
75
+ catch {
76
+ this.spend.set(identity, kept);
77
+ return { allow: false, reason: "payment currency differs from the window cap" };
78
+ }
79
+ kept.push({ atMillis: now, amount: quote.srcAmount });
80
+ this.spend.set(identity, kept);
81
+ return { allow: true, reason: "" };
82
+ }
83
+ }
@@ -0,0 +1,4 @@
1
+ export declare const ID = "pact";
2
+ export declare const VERSION = "1";
3
+ export declare function domain(kind: string): string;
4
+ export declare const DEFAULT_SKEW_MILLIS = 120000;
@@ -0,0 +1,15 @@
1
+ // ID is the stable protocol identifier. It is a technical constant, not a brand:
2
+ // it appears only inside domain-separation tags and is never localized or
3
+ // rebranded. Changing it is a breaking wire migration.
4
+ export const ID = "pact";
5
+ // VERSION is the wire version. It composes with ID into the domain prefix every
6
+ // signed preimage begins with.
7
+ export const VERSION = "1";
8
+ const domainPrefix = `${ID}/${VERSION}/`;
9
+ // domain builds the full domain tag for a message kind, e.g. "pact/1/intent".
10
+ export function domain(kind) {
11
+ return domainPrefix + kind;
12
+ }
13
+ // DEFAULT_SKEW_MILLIS is the tolerated clock difference when deciding whether an
14
+ // intent or quote has expired.
15
+ export const DEFAULT_SKEW_MILLIS = 120_000;
@@ -0,0 +1,16 @@
1
+ import type { Quote } from "./message.js";
2
+ import type { KycStatus } from "./compliance.js";
3
+ export declare enum PolicyKind {
4
+ Cheapest = 0,
5
+ Fastest = 1,
6
+ PreferredRail = 2
7
+ }
8
+ export interface Policy {
9
+ kind: PolicyKind;
10
+ preferredRails?: string[];
11
+ }
12
+ export declare class NoQuoteError extends Error {
13
+ constructor(reason: string);
14
+ }
15
+ export declare function isExpired(deadline: number, now: number, skew: number): boolean;
16
+ export declare function route(quotes: Quote[], policy: Policy, kyc: KycStatus, now: number, skew: number): Quote;
@@ -0,0 +1,87 @@
1
+ import { withinLimits } from "./compliance.js";
2
+ // PolicyKind names how the router chooses among competing quotes.
3
+ export var PolicyKind;
4
+ (function (PolicyKind) {
5
+ PolicyKind[PolicyKind["Cheapest"] = 0] = "Cheapest";
6
+ PolicyKind[PolicyKind["Fastest"] = 1] = "Fastest";
7
+ PolicyKind[PolicyKind["PreferredRail"] = 2] = "PreferredRail";
8
+ })(PolicyKind || (PolicyKind = {}));
9
+ export class NoQuoteError extends Error {
10
+ constructor(reason) {
11
+ super(`pact: no eligible quote: ${reason}`);
12
+ this.name = "NoQuoteError";
13
+ }
14
+ }
15
+ export function isExpired(deadline, now, skew) {
16
+ if (deadline === 0)
17
+ return false;
18
+ return deadline + skew <= now;
19
+ }
20
+ // route selects one quote deterministically. It first discards quotes that are
21
+ // expired against now or that exceed the identity's KYC limit, then ranks the
22
+ // survivors by the policy. Ties break by pay-in adapter, then pay-out adapter,
23
+ // then quote id, so the choice is total and stable and never depends on input
24
+ // order or randomness.
25
+ export function route(quotes, policy, kyc, now, skew) {
26
+ const eligible = quotes.filter((q) => !isExpired(q.expiresAt, now, skew) && withinLimits(kyc, q));
27
+ if (eligible.length === 0) {
28
+ throw new NoQuoteError("all quotes expired or over limit");
29
+ }
30
+ const rank = rankFn(policy);
31
+ eligible.sort((a, b) => {
32
+ const primary = rank(a, b);
33
+ if (primary !== 0)
34
+ return primary;
35
+ if (a.payInAdapterId !== b.payInAdapterId)
36
+ return a.payInAdapterId < b.payInAdapterId ? -1 : 1;
37
+ if (a.payOutAdapterId !== b.payOutAdapterId)
38
+ return a.payOutAdapterId < b.payOutAdapterId ? -1 : 1;
39
+ if (a.id !== b.id)
40
+ return a.id < b.id ? -1 : 1;
41
+ return 0;
42
+ });
43
+ return eligible[0];
44
+ }
45
+ function rankFn(policy) {
46
+ switch (policy.kind) {
47
+ case PolicyKind.Fastest:
48
+ return (a, b) => cmpNumber(a.latencyEstimateMs, b.latencyEstimateMs);
49
+ case PolicyKind.PreferredRail: {
50
+ const rank = railRank(policy.preferredRails ?? []);
51
+ return (a, b) => {
52
+ const byRail = cmpNumber(rank(a.payOutRail), rank(b.payOutRail));
53
+ return byRail !== 0 ? byRail : cmpMoney(a, b);
54
+ };
55
+ }
56
+ default:
57
+ return (a, b) => {
58
+ const bySrc = cmpMoney(a, b);
59
+ if (bySrc !== 0)
60
+ return bySrc;
61
+ return safeCmp(a.fees, b.fees);
62
+ };
63
+ }
64
+ }
65
+ function railRank(preferred) {
66
+ const index = new Map();
67
+ preferred.forEach((rail, i) => index.set(rail, i));
68
+ const unranked = preferred.length;
69
+ return (rail) => index.get(rail) ?? unranked;
70
+ }
71
+ function cmpNumber(a, b) {
72
+ return a < b ? -1 : a > b ? 1 : 0;
73
+ }
74
+ // cmpMoney orders two quotes by source amount. Amounts in different currencies
75
+ // are treated as incomparable and reported equal, deferring to the tie-break so
76
+ // the router never throws on a mixed-currency quote set.
77
+ function cmpMoney(a, b) {
78
+ return safeCmp(a.srcAmount, b.srcAmount);
79
+ }
80
+ function safeCmp(a, b) {
81
+ try {
82
+ return a.cmp(b);
83
+ }
84
+ catch {
85
+ return 0;
86
+ }
87
+ }
@@ -0,0 +1,27 @@
1
+ import type { Intent, Quote, Authorization } from "./message.js";
2
+ export interface Signer {
3
+ identity(): string;
4
+ sign(preimage: Uint8Array): Uint8Array;
5
+ }
6
+ export interface Verifier {
7
+ verify(identity: string, preimage: Uint8Array, signature: Uint8Array): boolean;
8
+ }
9
+ export declare class BadSignatureError extends Error {
10
+ constructor();
11
+ }
12
+ export declare function authorize(intent: Intent, quote: Quote, signer: Signer, signedAt: number): Authorization;
13
+ export declare function verifyAuthorization(auth: Authorization, intent: Intent, quote: Quote, verifier: Verifier): void;
14
+ export declare class Ed25519Signer implements Signer {
15
+ private readonly id;
16
+ private readonly privateKey;
17
+ readonly publicKey: Uint8Array;
18
+ private constructor();
19
+ static fromSeed(identity: string, seed: Uint8Array): Ed25519Signer;
20
+ identity(): string;
21
+ sign(preimage: Uint8Array): Uint8Array;
22
+ }
23
+ export declare class Ed25519Verifier implements Verifier {
24
+ private readonly keys;
25
+ constructor(keys: Map<string, Uint8Array>);
26
+ verify(identity: string, preimage: Uint8Array, signature: Uint8Array): boolean;
27
+ }
@@ -0,0 +1,72 @@
1
+ import { intentHash, quoteHash, signingPreimage } from "./message.js";
2
+ import { ed25519KeyFromSeed, ed25519Sign, ed25519Verify } from "./crypto.js";
3
+ export class BadSignatureError extends Error {
4
+ constructor() {
5
+ super("pact: signature does not verify");
6
+ this.name = "BadSignatureError";
7
+ }
8
+ }
9
+ // authorize builds and signs an authorization committing a sender to one quote
10
+ // for one intent.
11
+ export function authorize(intent, quote, signer, signedAt) {
12
+ if (quote.intentId !== intent.id) {
13
+ throw new Error("pact: quote does not belong to intent");
14
+ }
15
+ const preimage = signingPreimage(intentHash(intent), quoteHash(quote), signer.identity(), signedAt);
16
+ return {
17
+ intentId: intent.id,
18
+ quoteId: quote.id,
19
+ signerIdentity: signer.identity(),
20
+ signedAt,
21
+ signature: signer.sign(preimage),
22
+ };
23
+ }
24
+ // verifyAuthorization checks that an authorization was signed for exactly this
25
+ // intent and quote. Binding both hashes into the preimage makes a captured
26
+ // signature useless against any other intent or quote.
27
+ export function verifyAuthorization(auth, intent, quote, verifier) {
28
+ if (auth.intentId !== intent.id || auth.quoteId !== quote.id || quote.intentId !== intent.id) {
29
+ throw new Error("pact: authorization does not match intent and quote");
30
+ }
31
+ const preimage = signingPreimage(intentHash(intent), quoteHash(quote), auth.signerIdentity, auth.signedAt);
32
+ if (!verifier.verify(auth.signerIdentity, preimage, auth.signature)) {
33
+ throw new BadSignatureError();
34
+ }
35
+ }
36
+ // Ed25519Signer is the reference signer used by the shared test vectors and by
37
+ // hosts whose identity key is an Ed25519 key. Hosts that sign with a
38
+ // smart-account validator supply their own Signer instead.
39
+ export class Ed25519Signer {
40
+ id;
41
+ privateKey;
42
+ publicKey;
43
+ constructor(id, privateKey, publicKey) {
44
+ this.id = id;
45
+ this.privateKey = privateKey;
46
+ this.publicKey = publicKey;
47
+ }
48
+ static fromSeed(identity, seed) {
49
+ const { privateKey, publicKey } = ed25519KeyFromSeed(seed);
50
+ return new Ed25519Signer(identity, privateKey, publicKey);
51
+ }
52
+ identity() {
53
+ return this.id;
54
+ }
55
+ sign(preimage) {
56
+ return ed25519Sign(this.privateKey, preimage);
57
+ }
58
+ }
59
+ // Ed25519Verifier resolves an identity string to its Ed25519 public key. A real
60
+ // host resolves keys from its own key directory instead of a map.
61
+ export class Ed25519Verifier {
62
+ keys;
63
+ constructor(keys) {
64
+ this.keys = keys;
65
+ }
66
+ verify(identity, preimage, signature) {
67
+ const pub = this.keys.get(identity);
68
+ if (!pub)
69
+ return false;
70
+ return ed25519Verify(pub, preimage, signature);
71
+ }
72
+ }
@@ -0,0 +1,23 @@
1
+ export declare enum State {
2
+ Unspecified = 0,
3
+ Draft = 1,
4
+ Quoted = 2,
5
+ Authorized = 3,
6
+ Submitted = 4,
7
+ Settled = 5,
8
+ Failed = 6,
9
+ Expired = 7,
10
+ Refunded = 8,
11
+ Collecting = 9,
12
+ Held = 10,
13
+ Disbursing = 11,
14
+ Refunding = 12
15
+ }
16
+ export declare const stateName: Record<State, string>;
17
+ export declare function canTransition(from: State, to: State): boolean;
18
+ export declare function isTerminal(state: State): boolean;
19
+ export declare class TransitionError extends Error {
20
+ readonly from: State;
21
+ readonly to: State;
22
+ constructor(from: State, to: State);
23
+ }
@@ -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 var State;
4
+ (function (State) {
5
+ State[State["Unspecified"] = 0] = "Unspecified";
6
+ State[State["Draft"] = 1] = "Draft";
7
+ State[State["Quoted"] = 2] = "Quoted";
8
+ State[State["Authorized"] = 3] = "Authorized";
9
+ State[State["Submitted"] = 4] = "Submitted";
10
+ State[State["Settled"] = 5] = "Settled";
11
+ State[State["Failed"] = 6] = "Failed";
12
+ State[State["Expired"] = 7] = "Expired";
13
+ State[State["Refunded"] = 8] = "Refunded";
14
+ // The states below carry a bridged corridor through its escrow middle, where
15
+ // the pay-in and pay-out are different providers or currencies.
16
+ State[State["Collecting"] = 9] = "Collecting";
17
+ State[State["Held"] = 10] = "Held";
18
+ State[State["Disbursing"] = 11] = "Disbursing";
19
+ State[State["Refunding"] = 12] = "Refunding";
20
+ })(State || (State = {}));
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 = {
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
+ // transitions is the whole state machine expressed as data. A payment may move
40
+ // from a key only to a listed state; every other pairing is illegal. Terminal
41
+ // states map to an empty set. A direct corridor settles in one phase; a bridged
42
+ // one enters the escrow middle at collecting and unwinds through refunding when
43
+ // its pay-out cannot complete.
44
+ const transitions = {
45
+ [State.Unspecified]: new Set([State.Draft]),
46
+ [State.Draft]: new Set([State.Quoted, State.Expired]),
47
+ [State.Quoted]: new Set([State.Authorized, State.Expired]),
48
+ [State.Authorized]: new Set([State.Submitted, State.Collecting, State.Expired]),
49
+ [State.Submitted]: new Set([State.Settled, State.Failed]),
50
+ [State.Collecting]: new Set([State.Held, State.Failed, State.Expired]),
51
+ [State.Held]: new Set([State.Disbursing, State.Refunding, State.Expired]),
52
+ [State.Disbursing]: new Set([State.Settled, State.Failed, State.Refunding]),
53
+ [State.Refunding]: new Set([State.Refunded, State.Failed]),
54
+ [State.Settled]: new Set([State.Refunded]),
55
+ [State.Failed]: new Set(),
56
+ [State.Expired]: new Set(),
57
+ [State.Refunded]: new Set(),
58
+ };
59
+ export function canTransition(from, to) {
60
+ return transitions[from].has(to);
61
+ }
62
+ // isTerminal reports whether a state accepts no further transition. Settled is
63
+ // not terminal because it still admits the single edge to refunded.
64
+ export function isTerminal(state) {
65
+ return transitions[state].size === 0;
66
+ }
67
+ export class TransitionError extends Error {
68
+ from;
69
+ to;
70
+ constructor(from, to) {
71
+ super(`pact: illegal transition ${stateName[from]} -> ${stateName[to]}`);
72
+ this.from = from;
73
+ this.to = to;
74
+ this.name = "TransitionError";
75
+ }
76
+ }
@@ -0,0 +1,30 @@
1
+ import type { Intent, Quote, Authorization, Settlement } from "./message.js";
2
+ export declare enum WireKind {
3
+ Intent = 1,
4
+ Quote = 2,
5
+ Authorization = 3,
6
+ Settlement = 4
7
+ }
8
+ export declare function encodeIntent(i: Intent): Uint8Array;
9
+ export declare function encodeQuote(q: Quote): Uint8Array;
10
+ export declare function encodeAuthorization(a: Authorization): Uint8Array;
11
+ export declare function encodeSettlement(s: Settlement): Uint8Array;
12
+ export type WireMessage = {
13
+ kind: WireKind.Intent;
14
+ message: Intent;
15
+ } | {
16
+ kind: WireKind.Quote;
17
+ message: Quote;
18
+ } | {
19
+ kind: WireKind.Authorization;
20
+ message: Authorization;
21
+ } | {
22
+ kind: WireKind.Settlement;
23
+ message: Settlement;
24
+ };
25
+ export declare function encodeMessage(msg: WireMessage): Uint8Array;
26
+ export declare function decodeMessage(frame: Uint8Array): WireMessage;
27
+ export declare function decodeIntent(b: Uint8Array): Intent;
28
+ export declare function decodeQuote(b: Uint8Array): Quote;
29
+ export declare function decodeAuthorization(b: Uint8Array): Authorization;
30
+ export declare function decodeSettlement(b: Uint8Array): Settlement;
@@ -0,0 +1,221 @@
1
+ import { Money } from "./money.js";
2
+ import { CanonicalWriter } from "./canonical.js";
3
+ // The wire codec serializes messages for transport. It is distinct from the
4
+ // canonical signing preimage: signing binds a fixed subset of fields under a
5
+ // domain tag, while the wire form carries every field so a peer can reconstruct
6
+ // the message. Both use the same length-delimited primitives; a message's wire
7
+ // bytes are never signed and its signing preimage is never sent.
8
+ export var WireKind;
9
+ (function (WireKind) {
10
+ WireKind[WireKind["Intent"] = 1] = "Intent";
11
+ WireKind[WireKind["Quote"] = 2] = "Quote";
12
+ WireKind[WireKind["Authorization"] = 3] = "Authorization";
13
+ WireKind[WireKind["Settlement"] = 4] = "Settlement";
14
+ })(WireKind || (WireKind = {}));
15
+ const decoder = new TextDecoder();
16
+ export function encodeIntent(i) {
17
+ return new CanonicalWriter()
18
+ .str(i.id)
19
+ .str(i.senderRef)
20
+ .str(i.recipientRef)
21
+ .money(i.amount)
22
+ .str(i.memo)
23
+ .u64(i.expiresAt)
24
+ .list(i.allowedRails)
25
+ .stringMap(i.metadata)
26
+ .preimage();
27
+ }
28
+ export function encodeQuote(q) {
29
+ return new CanonicalWriter()
30
+ .str(q.id)
31
+ .str(q.intentId)
32
+ .str(q.payInAdapterId)
33
+ .str(q.payInRail)
34
+ .str(q.payOutAdapterId)
35
+ .str(q.payOutRail)
36
+ .str(q.bridgeId)
37
+ .money(q.srcAmount)
38
+ .money(q.dstAmount)
39
+ .money(q.fees)
40
+ .str(q.fxRate)
41
+ .u64(q.expiresAt)
42
+ .str(q.providerQuoteRef)
43
+ .u64(q.latencyEstimateMs)
44
+ .preimage();
45
+ }
46
+ export function encodeAuthorization(a) {
47
+ return new CanonicalWriter()
48
+ .str(a.intentId)
49
+ .str(a.quoteId)
50
+ .str(a.signerIdentity)
51
+ .u64(a.signedAt)
52
+ .bytes(a.signature)
53
+ .preimage();
54
+ }
55
+ export function encodeSettlement(s) {
56
+ return new CanonicalWriter()
57
+ .str(s.intentId)
58
+ .u64(s.state)
59
+ .str(s.adapterId)
60
+ .str(s.providerTxRef)
61
+ .str(s.onchainTxHash)
62
+ .bytes(s.receiptHash)
63
+ .str(s.reason)
64
+ .u64(s.settledAt)
65
+ .preimage();
66
+ }
67
+ export function encodeMessage(msg) {
68
+ const payload = msg.kind === WireKind.Intent
69
+ ? encodeIntent(msg.message)
70
+ : msg.kind === WireKind.Quote
71
+ ? encodeQuote(msg.message)
72
+ : msg.kind === WireKind.Authorization
73
+ ? encodeAuthorization(msg.message)
74
+ : encodeSettlement(msg.message);
75
+ const out = new Uint8Array(payload.length + 1);
76
+ out[0] = msg.kind;
77
+ out.set(payload, 1);
78
+ return out;
79
+ }
80
+ export function decodeMessage(frame) {
81
+ if (frame.length === 0) {
82
+ throw new Error("pact: wire frame ended mid-field");
83
+ }
84
+ const kind = frame[0];
85
+ const body = frame.subarray(1);
86
+ switch (kind) {
87
+ case WireKind.Intent:
88
+ return { kind, message: decodeIntent(body) };
89
+ case WireKind.Quote:
90
+ return { kind, message: decodeQuote(body) };
91
+ case WireKind.Authorization:
92
+ return { kind, message: decodeAuthorization(body) };
93
+ case WireKind.Settlement:
94
+ return { kind, message: decodeSettlement(body) };
95
+ default:
96
+ throw new Error(`pact: unknown wire kind ${kind}`);
97
+ }
98
+ }
99
+ // WireReader consumes the length-delimited primitives in order.
100
+ class WireReader {
101
+ buf;
102
+ pos = 0;
103
+ constructor(buf) {
104
+ this.buf = buf;
105
+ }
106
+ bytes() {
107
+ if (this.pos + 4 > this.buf.length) {
108
+ throw new Error("pact: wire frame ended mid-field");
109
+ }
110
+ const view = new DataView(this.buf.buffer, this.buf.byteOffset + this.pos, 4);
111
+ const length = view.getUint32(0, false);
112
+ this.pos += 4;
113
+ if (this.pos + length > this.buf.length) {
114
+ throw new Error("pact: wire frame ended mid-field");
115
+ }
116
+ const out = this.buf.subarray(this.pos, this.pos + length);
117
+ this.pos += length;
118
+ return out;
119
+ }
120
+ str() {
121
+ return decoder.decode(this.bytes());
122
+ }
123
+ u64() {
124
+ if (this.pos + 8 > this.buf.length) {
125
+ throw new Error("pact: wire frame ended mid-field");
126
+ }
127
+ const view = new DataView(this.buf.buffer, this.buf.byteOffset + this.pos, 8);
128
+ this.pos += 8;
129
+ return Number(view.getBigUint64(0, false));
130
+ }
131
+ money() {
132
+ const minor = this.str();
133
+ const currency = this.str();
134
+ const exponent = this.u64();
135
+ return Money.parse(currency, exponent, minor);
136
+ }
137
+ list() {
138
+ const count = this.u64();
139
+ const out = [];
140
+ for (let i = 0; i < count; i++)
141
+ out.push(this.str());
142
+ return out;
143
+ }
144
+ stringMap() {
145
+ const count = this.u64();
146
+ const out = {};
147
+ for (let i = 0; i < count; i++) {
148
+ const key = this.str();
149
+ out[key] = this.str();
150
+ }
151
+ return out;
152
+ }
153
+ requireDone() {
154
+ if (this.pos !== this.buf.length) {
155
+ throw new Error("pact: trailing bytes after wire frame");
156
+ }
157
+ }
158
+ }
159
+ export function decodeIntent(b) {
160
+ const r = new WireReader(b);
161
+ const intent = {
162
+ id: r.str(),
163
+ senderRef: r.str(),
164
+ recipientRef: r.str(),
165
+ amount: r.money(),
166
+ memo: r.str(),
167
+ expiresAt: r.u64(),
168
+ allowedRails: r.list(),
169
+ metadata: r.stringMap(),
170
+ };
171
+ r.requireDone();
172
+ return intent;
173
+ }
174
+ export function decodeQuote(b) {
175
+ const r = new WireReader(b);
176
+ const quote = {
177
+ id: r.str(),
178
+ intentId: r.str(),
179
+ payInAdapterId: r.str(),
180
+ payInRail: r.str(),
181
+ payOutAdapterId: r.str(),
182
+ payOutRail: r.str(),
183
+ bridgeId: r.str(),
184
+ srcAmount: r.money(),
185
+ dstAmount: r.money(),
186
+ fees: r.money(),
187
+ fxRate: r.str(),
188
+ expiresAt: r.u64(),
189
+ providerQuoteRef: r.str(),
190
+ latencyEstimateMs: r.u64(),
191
+ };
192
+ r.requireDone();
193
+ return quote;
194
+ }
195
+ export function decodeAuthorization(b) {
196
+ const r = new WireReader(b);
197
+ const auth = {
198
+ intentId: r.str(),
199
+ quoteId: r.str(),
200
+ signerIdentity: r.str(),
201
+ signedAt: r.u64(),
202
+ signature: r.bytes().slice(),
203
+ };
204
+ r.requireDone();
205
+ return auth;
206
+ }
207
+ export function decodeSettlement(b) {
208
+ const r = new WireReader(b);
209
+ const settlement = {
210
+ intentId: r.str(),
211
+ state: r.u64(),
212
+ adapterId: r.str(),
213
+ providerTxRef: r.str(),
214
+ onchainTxHash: r.str(),
215
+ receiptHash: r.bytes().slice(),
216
+ reason: r.str(),
217
+ settledAt: r.u64(),
218
+ };
219
+ r.requireDone();
220
+ return settlement;
221
+ }
@@ -0,0 +1 @@
1
+ export {};