@xochi/client 0.0.0-stage → 0.1.1
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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +119 -2
- package/dist/cjs/claim.d.ts +33 -0
- package/dist/cjs/claim.js +44 -0
- package/dist/cjs/fee.d.ts +247 -0
- package/dist/cjs/fee.js +295 -0
- package/dist/cjs/index.d.ts +18 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/machine/claimInfo.d.ts +29 -0
- package/dist/cjs/machine/claimInfo.js +26 -0
- package/dist/cjs/machine/decisions.d.ts +452 -0
- package/dist/cjs/machine/decisions.js +665 -0
- package/dist/cjs/machine/deposit.d.ts +113 -0
- package/dist/cjs/machine/deposit.js +265 -0
- package/dist/cjs/machine/index.d.ts +23 -0
- package/dist/cjs/machine/index.js +53 -0
- package/dist/cjs/machine/machine.d.ts +409 -0
- package/dist/cjs/machine/machine.js +1615 -0
- package/dist/cjs/machine/phase.d.ts +24 -0
- package/dist/cjs/machine/phase.js +45 -0
- package/dist/cjs/machine/quoteChecks.d.ts +41 -0
- package/dist/cjs/machine/quoteChecks.js +82 -0
- package/dist/cjs/machine/receipt.d.ts +151 -0
- package/dist/cjs/machine/receipt.js +69 -0
- package/dist/cjs/machine/recipientEcho.d.ts +16 -0
- package/dist/cjs/machine/recipientEcho.js +27 -0
- package/dist/cjs/machine/solverPin.d.ts +110 -0
- package/dist/cjs/machine/solverPin.js +214 -0
- package/dist/cjs/machine/state.d.ts +167 -0
- package/dist/cjs/machine/state.js +31 -0
- package/dist/cjs/machine/transition.d.ts +15 -0
- package/dist/cjs/machine/transition.js +20 -0
- package/dist/cjs/machine/validateRequest.d.ts +15 -0
- package/dist/cjs/machine/validateRequest.js +127 -0
- package/dist/cjs/machine/view.d.ts +3 -0
- package/dist/cjs/machine/view.js +74 -0
- package/dist/cjs/machine/wire.d.ts +527 -0
- package/dist/cjs/machine/wire.js +280 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/shared/api.d.ts +9 -0
- package/dist/cjs/shared/api.js +5 -0
- package/dist/cjs/shared/chains.d.ts +243 -0
- package/dist/cjs/shared/chains.js +250 -0
- package/dist/cjs/shared/eip712.d.ts +44 -0
- package/dist/cjs/shared/eip712.js +64 -0
- package/dist/cjs/shared/index.d.ts +15 -0
- package/dist/cjs/shared/index.js +31 -0
- package/dist/cjs/shared/intentStream.d.ts +74 -0
- package/dist/cjs/shared/intentStream.js +59 -0
- package/dist/cjs/shared/intents.d.ts +88 -0
- package/dist/cjs/shared/intents.js +227 -0
- package/dist/cjs/shared/schemas.d.ts +928 -0
- package/dist/cjs/shared/schemas.js +524 -0
- package/dist/cjs/shared/stealth.d.ts +53 -0
- package/dist/cjs/shared/stealth.js +68 -0
- package/dist/cjs/shared/trading.d.ts +153 -0
- package/dist/cjs/shared/trading.js +185 -0
- package/dist/cjs/shared/types.d.ts +37 -0
- package/dist/cjs/shared/types.js +7 -0
- package/dist/cjs/shared/validators.d.ts +30 -0
- package/dist/cjs/shared/validators.js +79 -0
- package/dist/cjs/transport/client.d.ts +234 -0
- package/dist/cjs/transport/client.js +1456 -0
- package/dist/esm/claim.d.ts +33 -0
- package/dist/esm/claim.js +41 -0
- package/dist/esm/fee.d.ts +247 -0
- package/dist/esm/fee.js +285 -0
- package/dist/esm/index.d.ts +18 -0
- package/dist/esm/index.js +17 -0
- package/dist/esm/machine/claimInfo.d.ts +29 -0
- package/dist/esm/machine/claimInfo.js +23 -0
- package/dist/esm/machine/decisions.d.ts +452 -0
- package/dist/esm/machine/decisions.js +640 -0
- package/dist/esm/machine/deposit.d.ts +113 -0
- package/dist/esm/machine/deposit.js +260 -0
- package/dist/esm/machine/index.d.ts +23 -0
- package/dist/esm/machine/index.js +23 -0
- package/dist/esm/machine/machine.d.ts +409 -0
- package/dist/esm/machine/machine.js +1609 -0
- package/dist/esm/machine/phase.d.ts +24 -0
- package/dist/esm/machine/phase.js +42 -0
- package/dist/esm/machine/quoteChecks.d.ts +41 -0
- package/dist/esm/machine/quoteChecks.js +78 -0
- package/dist/esm/machine/receipt.d.ts +151 -0
- package/dist/esm/machine/receipt.js +64 -0
- package/dist/esm/machine/recipientEcho.d.ts +16 -0
- package/dist/esm/machine/recipientEcho.js +24 -0
- package/dist/esm/machine/solverPin.d.ts +110 -0
- package/dist/esm/machine/solverPin.js +210 -0
- package/dist/esm/machine/state.d.ts +167 -0
- package/dist/esm/machine/state.js +28 -0
- package/dist/esm/machine/transition.d.ts +15 -0
- package/dist/esm/machine/transition.js +17 -0
- package/dist/esm/machine/validateRequest.d.ts +15 -0
- package/dist/esm/machine/validateRequest.js +123 -0
- package/dist/esm/machine/view.d.ts +3 -0
- package/dist/esm/machine/view.js +71 -0
- package/dist/esm/machine/wire.d.ts +527 -0
- package/dist/esm/machine/wire.js +266 -0
- package/dist/esm/shared/api.d.ts +9 -0
- package/dist/esm/shared/api.js +4 -0
- package/dist/esm/shared/chains.d.ts +243 -0
- package/dist/esm/shared/chains.js +236 -0
- package/dist/esm/shared/eip712.d.ts +44 -0
- package/dist/esm/shared/eip712.js +61 -0
- package/dist/esm/shared/index.d.ts +15 -0
- package/dist/esm/shared/index.js +15 -0
- package/dist/esm/shared/intentStream.d.ts +74 -0
- package/dist/esm/shared/intentStream.js +55 -0
- package/dist/esm/shared/intents.d.ts +88 -0
- package/dist/esm/shared/intents.js +213 -0
- package/dist/esm/shared/schemas.d.ts +928 -0
- package/dist/esm/shared/schemas.js +521 -0
- package/dist/esm/shared/stealth.d.ts +53 -0
- package/dist/esm/shared/stealth.js +64 -0
- package/dist/esm/shared/trading.d.ts +153 -0
- package/dist/esm/shared/trading.js +182 -0
- package/dist/esm/shared/types.d.ts +37 -0
- package/dist/esm/shared/types.js +6 -0
- package/dist/esm/shared/validators.d.ts +30 -0
- package/dist/esm/shared/validators.js +74 -0
- package/dist/esm/transport/client.d.ts +234 -0
- package/dist/esm/transport/client.js +1447 -0
- package/package.json +61 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deposit-route (Tron and other non-EVM origin) binding: the shapes the machine
|
|
3
|
+
* stores, and the checks it runs at quote ingest and on resume.
|
|
4
|
+
*
|
|
5
|
+
* The attestation itself -- the v3 message, its EIP-191 recovery against the
|
|
6
|
+
* pinned signer, the request binding, the deposit window and the
|
|
7
|
+
* destination-token guard -- is `@xochi/signer`'s. This module adapts it to
|
|
8
|
+
* the machine: it assembles the request side, carries the caller's pinned
|
|
9
|
+
* signer, keeps the per-session address-reuse tripwire as plain data on the
|
|
10
|
+
* model, and maps the signer's error classes to `DepositAttestationResult`.
|
|
11
|
+
*
|
|
12
|
+
* There is no clock parameter anywhere here, on purpose: the signer bounds the
|
|
13
|
+
* deposit window against the wall clock, and an injectable `now` on a security
|
|
14
|
+
* assertion is a bypass wearing a test affordance's name.
|
|
15
|
+
*/
|
|
16
|
+
import { recipientIsDestinationToken, type DepositAttestationFields } from "@xochi/signer";
|
|
17
|
+
import type { QuoteRequest, QuoteResponse } from "./wire.js";
|
|
18
|
+
export { recipientIsDestinationToken, type DepositAttestationFields };
|
|
19
|
+
/**
|
|
20
|
+
* `kind` names the rejections a caller must treat differently, and only those.
|
|
21
|
+
*
|
|
22
|
+
* `addressReuse` is the one the user can act on: it is the per-session tripwire
|
|
23
|
+
* on `Model.seenDepositAddresses`, which cannot tell a genuine collision from
|
|
24
|
+
* Riddler re-issuing an address to a re-quote, and which a reload clears.
|
|
25
|
+
* Everything else is `unavailable`, where retrying is the right advice and the
|
|
26
|
+
* detail belongs in the log rather than on screen.
|
|
27
|
+
*/
|
|
28
|
+
export type DepositAttestationResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
kind: "unavailable" | "addressReuse";
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A deposit binding as persisted alongside a swap record, and the signature over
|
|
37
|
+
* it. `attested` is the signer's own field set, exactly as it was verified.
|
|
38
|
+
*
|
|
39
|
+
* The FIELDS are stored verbatim rather than re-derived on read. Re-deriving
|
|
40
|
+
* would mean the values displayed and the values verified are built by two
|
|
41
|
+
* different code paths, which is precisely how a screen comes to show one
|
|
42
|
+
* address while a check passes over another.
|
|
43
|
+
*/
|
|
44
|
+
export interface StoredDepositAttestation {
|
|
45
|
+
attested: DepositAttestationFields;
|
|
46
|
+
attestation: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `verifyStoredDeposit`'s answer. On success it carries the binding it PROVED,
|
|
50
|
+
* in the signer's field set, and that object is the one to render and persist:
|
|
51
|
+
* a record written by an older build arrives in a different shape (see
|
|
52
|
+
* `STORED_FIELDS`), so the caller's own copy is not what was verified.
|
|
53
|
+
*/
|
|
54
|
+
export type StoredDepositResult = {
|
|
55
|
+
ok: true;
|
|
56
|
+
stored: StoredDepositAttestation;
|
|
57
|
+
} | Extract<DepositAttestationResult, {
|
|
58
|
+
ok: false;
|
|
59
|
+
}>;
|
|
60
|
+
export type DepositQuoteCheck = {
|
|
61
|
+
ok: true;
|
|
62
|
+
stored: StoredDepositAttestation;
|
|
63
|
+
seen: Readonly<Record<string, string>>;
|
|
64
|
+
} | Extract<DepositAttestationResult, {
|
|
65
|
+
ok: false;
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Verify a served deposit route against the request that produced it.
|
|
69
|
+
*
|
|
70
|
+
* Runs, in order: the signature-shape gate above; `depositAttestationFields`
|
|
71
|
+
* (the request half of the message is rebuilt from `request`, the server half
|
|
72
|
+
* from the quote, with the requested `from_amount` because the Worker envelope
|
|
73
|
+
* carries none on an exact-input route); `verifyDepositAttestation` against the
|
|
74
|
+
* caller-pinned `signer`; `assertDepositQuoteMatchesRequest` (amount, displayed
|
|
75
|
+
* recipient, destination-token payout, attested deposit window); and finally
|
|
76
|
+
* `recordDepositAddress` against `seen`, only once the signature has verified so
|
|
77
|
+
* a forged quote cannot poison the record.
|
|
78
|
+
*
|
|
79
|
+
* Returns the next `seen` map rather than mutating the one passed in, so the
|
|
80
|
+
* model stays plain data.
|
|
81
|
+
*/
|
|
82
|
+
export declare function checkDepositQuote(request: QuoteRequest, quote: QuoteResponse, signer: string | null, seen: Readonly<Record<string, string>>): DepositQuoteCheck;
|
|
83
|
+
/**
|
|
84
|
+
* Prove a deposit binding restored from storage, before any of it is shown.
|
|
85
|
+
*
|
|
86
|
+
* A resumed swap reads its deposit address out of host storage, which is not a
|
|
87
|
+
* trust boundary. Rendering an address from there, having never checked it,
|
|
88
|
+
* hands away the invariant at the one point the user is about to move funds by
|
|
89
|
+
* hand. TWO independent things are checked, and both are necessary:
|
|
90
|
+
*
|
|
91
|
+
* 1. The BINDING to the record it was stored under. An attacker can obtain a
|
|
92
|
+
* genuine attestation for their own deposit quote and transplant it into a
|
|
93
|
+
* victim's record; every field is internally consistent and the signature
|
|
94
|
+
* verifies. What it cannot fake is whose deal it is: v3 binds `wallet` and
|
|
95
|
+
* the record is filed under its owner, and `intent_id` must match the record.
|
|
96
|
+
* 2. The SIGNATURE, by `verifyDepositAttestation` against the pinned signer,
|
|
97
|
+
* plus the destination-token guard the live path runs.
|
|
98
|
+
*
|
|
99
|
+
* Takes the record as `unknown` because that is what storage hands back. A
|
|
100
|
+
* record persisted before the cutover to `@xochi/signer` (camelCase fields,
|
|
101
|
+
* `resolvedSettlementType`) is renamed first and then checked exactly like a
|
|
102
|
+
* current one; any other shape refuses. On success the result carries the
|
|
103
|
+
* proven binding in the current shape: render THAT, never the record passed
|
|
104
|
+
* in, and write it back if the record is re-persisted.
|
|
105
|
+
*
|
|
106
|
+
* NOT checked here: the deposit window. A resumed swap legitimately comes back
|
|
107
|
+
* after it closed, and the honest view then names no address to send to; the
|
|
108
|
+
* caller reads `attested.deposit_deadline` and renders accordingly.
|
|
109
|
+
*/
|
|
110
|
+
export declare function verifyStoredDeposit(raw: unknown, signer: string | null, expected: {
|
|
111
|
+
intentId: string;
|
|
112
|
+
owner: string;
|
|
113
|
+
}): StoredDepositResult;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deposit-route (Tron and other non-EVM origin) binding: the shapes the machine
|
|
3
|
+
* stores, and the checks it runs at quote ingest and on resume.
|
|
4
|
+
*
|
|
5
|
+
* The attestation itself -- the v3 message, its EIP-191 recovery against the
|
|
6
|
+
* pinned signer, the request binding, the deposit window and the
|
|
7
|
+
* destination-token guard -- is `@xochi/signer`'s. This module adapts it to
|
|
8
|
+
* the machine: it assembles the request side, carries the caller's pinned
|
|
9
|
+
* signer, keeps the per-session address-reuse tripwire as plain data on the
|
|
10
|
+
* model, and maps the signer's error classes to `DepositAttestationResult`.
|
|
11
|
+
*
|
|
12
|
+
* There is no clock parameter anywhere here, on purpose: the signer bounds the
|
|
13
|
+
* deposit window against the wall clock, and an injectable `now` on a security
|
|
14
|
+
* assertion is a bypass wearing a test affordance's name.
|
|
15
|
+
*/
|
|
16
|
+
import { DepositAttestationError, DepositQuoteMismatchError, assertDepositQuoteMatchesRequest, depositAttestationFields, recipientIsDestinationToken, recordDepositAddress, verifyDepositAttestation, } from "@xochi/signer";
|
|
17
|
+
import { quoteRequestParams } from "./quoteChecks.js";
|
|
18
|
+
export { recipientIsDestinationToken };
|
|
19
|
+
/**
|
|
20
|
+
* Every signed field: its spelling in records persisted by builds before the
|
|
21
|
+
* cutover to `@xochi/signer` (xochi `src/lib/depositAttestation.ts`, the
|
|
22
|
+
* widget's `packages/machine`), and the JSON type it must have.
|
|
23
|
+
*
|
|
24
|
+
* Those builds stored camelCase `DepositAttestationFields` and named the
|
|
25
|
+
* settlement field `resolvedSettlementType`; the signer names it
|
|
26
|
+
* `settlement_preference`. Same eighteen-line v3 message, same values, so a
|
|
27
|
+
* renamed record signs the same bytes. Keyed by the signer's type so a field
|
|
28
|
+
* added there without a row here fails to compile.
|
|
29
|
+
*
|
|
30
|
+
* `optional` fields were `undefined` in the old type, which `JSON.stringify`
|
|
31
|
+
* drops, so an absent key is how an old record says "empty line". Absent and
|
|
32
|
+
* `null` both become `null`, which signs the same empty line the signer writes
|
|
33
|
+
* for either.
|
|
34
|
+
*/
|
|
35
|
+
const STORED_FIELDS = {
|
|
36
|
+
intent_id: { legacy: "intentId", type: "string" },
|
|
37
|
+
quote_id: { legacy: "quoteId", type: "string" },
|
|
38
|
+
wallet: { legacy: "wallet", type: "string" },
|
|
39
|
+
from_chain_id: { legacy: "fromChainId", type: "integer" },
|
|
40
|
+
from_token: { legacy: "fromToken", type: "string" },
|
|
41
|
+
from_amount: { legacy: "fromAmount", type: "string" },
|
|
42
|
+
deposit_address: { legacy: "depositAddress", type: "string" },
|
|
43
|
+
deposit_deadline: { legacy: "depositDeadline", type: "optionalInteger" },
|
|
44
|
+
refund_address: { legacy: "refundAddress", type: "optionalString" },
|
|
45
|
+
to_chain_id: { legacy: "toChainId", type: "integer" },
|
|
46
|
+
to_token: { legacy: "toToken", type: "string" },
|
|
47
|
+
to_amount: { legacy: "toAmount", type: "string" },
|
|
48
|
+
min_to_amount: { legacy: "minToAmount", type: "string" },
|
|
49
|
+
recipient: { legacy: "recipient", type: "string" },
|
|
50
|
+
stealth_spending_pub_key: { legacy: "stealthSpendingPubKey", type: "optionalString" },
|
|
51
|
+
stealth_viewing_pub_key: { legacy: "stealthViewingPubKey", type: "optionalString" },
|
|
52
|
+
settlement_preference: { legacy: "resolvedSettlementType", type: "string" },
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Read a persisted binding into the signer's field set, renaming a pre-cutover
|
|
56
|
+
* camelCase record on the way. Pure, and decides SHAPE only: whatever comes out
|
|
57
|
+
* still goes through every check `verifyStoredDeposit` runs, so a migrated
|
|
58
|
+
* record is trusted exactly as far as a current one, which is not at all until
|
|
59
|
+
* its signature recovers.
|
|
60
|
+
*
|
|
61
|
+
* A record in neither shape, one missing a required field, or one with a field
|
|
62
|
+
* of the wrong type is refused rather than repaired. Only the signed fields are
|
|
63
|
+
* copied, so nothing unsigned rides along into what the caller renders.
|
|
64
|
+
*/
|
|
65
|
+
function readStoredDeposit(raw) {
|
|
66
|
+
if (typeof raw !== "object" ||
|
|
67
|
+
raw === null ||
|
|
68
|
+
!("attestation" in raw) ||
|
|
69
|
+
typeof raw.attestation !== "string" ||
|
|
70
|
+
!raw.attestation) {
|
|
71
|
+
return { ok: false, reason: "stored deposit carries no attestation" };
|
|
72
|
+
}
|
|
73
|
+
const { attestation } = raw;
|
|
74
|
+
if (!("attested" in raw) || typeof raw.attested !== "object" || raw.attested === null) {
|
|
75
|
+
return { ok: false, reason: "stored deposit carries no attested fields" };
|
|
76
|
+
}
|
|
77
|
+
const source = raw.attested;
|
|
78
|
+
const legacy = !("intent_id" in source) && "intentId" in source;
|
|
79
|
+
if (!legacy && !("intent_id" in source)) {
|
|
80
|
+
return { ok: false, reason: "stored deposit is in no recognised shape" };
|
|
81
|
+
}
|
|
82
|
+
const fields = {};
|
|
83
|
+
for (const name of Object.keys(STORED_FIELDS)) {
|
|
84
|
+
const { legacy: legacyName, type } = STORED_FIELDS[name];
|
|
85
|
+
const key = legacy ? legacyName : name;
|
|
86
|
+
const value = key in source ? Reflect.get(source, key) : undefined;
|
|
87
|
+
const optional = type === "optionalString" || type === "optionalInteger";
|
|
88
|
+
if (optional && (value === undefined || value === null)) {
|
|
89
|
+
fields[name] = null;
|
|
90
|
+
}
|
|
91
|
+
else if ((type === "string" || type === "optionalString") && typeof value === "string") {
|
|
92
|
+
fields[name] = value;
|
|
93
|
+
}
|
|
94
|
+
else if ((type === "integer" || type === "optionalInteger") &&
|
|
95
|
+
typeof value === "number" &&
|
|
96
|
+
Number.isSafeInteger(value)) {
|
|
97
|
+
fields[name] = value;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: `stored deposit field ${key} is ${value === undefined ? "missing" : "malformed"}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Every key of STORED_FIELDS (which is keyed by the signer's type) was
|
|
107
|
+
// assigned above with the JSON type its row names; the loop cannot tell
|
|
108
|
+
// the compiler which row is which, so the per-field types are asserted here.
|
|
109
|
+
const attested = fields;
|
|
110
|
+
return { ok: true, stored: { attested, attestation } };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 0x + 65-byte secp256k1 signature. Stricter than the signer, which also
|
|
114
|
+
* recovers a 64-byte EIP-2098 compact signature; the solver only ever serves
|
|
115
|
+
* the 65-byte form, so anything else is refused before recovery.
|
|
116
|
+
*/
|
|
117
|
+
const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
118
|
+
/**
|
|
119
|
+
* Verify a served deposit route against the request that produced it.
|
|
120
|
+
*
|
|
121
|
+
* Runs, in order: the signature-shape gate above; `depositAttestationFields`
|
|
122
|
+
* (the request half of the message is rebuilt from `request`, the server half
|
|
123
|
+
* from the quote, with the requested `from_amount` because the Worker envelope
|
|
124
|
+
* carries none on an exact-input route); `verifyDepositAttestation` against the
|
|
125
|
+
* caller-pinned `signer`; `assertDepositQuoteMatchesRequest` (amount, displayed
|
|
126
|
+
* recipient, destination-token payout, attested deposit window); and finally
|
|
127
|
+
* `recordDepositAddress` against `seen`, only once the signature has verified so
|
|
128
|
+
* a forged quote cannot poison the record.
|
|
129
|
+
*
|
|
130
|
+
* Returns the next `seen` map rather than mutating the one passed in, so the
|
|
131
|
+
* model stays plain data.
|
|
132
|
+
*/
|
|
133
|
+
export function checkDepositQuote(request, quote, signer, seen) {
|
|
134
|
+
const attestation = quote.depositAttestation;
|
|
135
|
+
if (attestation && !SIGNATURE_RE.test(attestation)) {
|
|
136
|
+
return { ok: false, kind: "unavailable", reason: "malformed deposit attestation signature" };
|
|
137
|
+
}
|
|
138
|
+
const params = quoteRequestParams(request);
|
|
139
|
+
let fields;
|
|
140
|
+
try {
|
|
141
|
+
fields = depositAttestationFields({
|
|
142
|
+
intent_id: quote.intentId,
|
|
143
|
+
quote_id: quote.quoteId,
|
|
144
|
+
from_amount: request.fromAmount,
|
|
145
|
+
deposit_address: quote.depositAddress,
|
|
146
|
+
deposit_deadline: quote.depositDeadline,
|
|
147
|
+
to_amount: quote.toAmount,
|
|
148
|
+
min_to_amount: quote.minToAmount,
|
|
149
|
+
}, params);
|
|
150
|
+
verifyDepositAttestation(fields, attestation, signer);
|
|
151
|
+
// `escrow` is the raw served envelope: the signer refuses any deposit quote
|
|
152
|
+
// that carries one, since this client runs no escrow binding.
|
|
153
|
+
assertDepositQuoteMatchesRequest(params, fields, {
|
|
154
|
+
recipient_address: quote.recipientAddress,
|
|
155
|
+
escrow: quote.escrow,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return depositRefusal(err);
|
|
160
|
+
}
|
|
161
|
+
// Only AFTER the signature verifies: recording an unverified address would let
|
|
162
|
+
// a forged quote poison the record and lock the user out of a legitimate one.
|
|
163
|
+
const next = { ...seen };
|
|
164
|
+
try {
|
|
165
|
+
recordDepositAddress({ get: (key) => next[key], set: (key, intentId) => void (next[key] = intentId) }, fields.deposit_address, fields.intent_id);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err instanceof DepositQuoteMismatchError) {
|
|
169
|
+
return { ok: false, kind: "addressReuse", reason: err.message };
|
|
170
|
+
}
|
|
171
|
+
return depositRefusal(err);
|
|
172
|
+
}
|
|
173
|
+
// `verifyDepositAttestation` threw on an absent attestation, so it is a string here.
|
|
174
|
+
return { ok: true, stored: { attested: fields, attestation: attestation }, seen: next };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Map the signer's error classes onto the machine's refusal shape: every reason
|
|
178
|
+
* not to send funds is `unavailable`. Anything that is not one of the signer's
|
|
179
|
+
* classes still refuses (fail closed) and says so.
|
|
180
|
+
*/
|
|
181
|
+
function depositRefusal(err) {
|
|
182
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
183
|
+
if (err instanceof DepositAttestationError)
|
|
184
|
+
return { ok: false, kind: "unavailable", reason };
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
kind: "unavailable",
|
|
188
|
+
reason: `deposit verification failed unexpectedly: ${reason}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Prove a deposit binding restored from storage, before any of it is shown.
|
|
193
|
+
*
|
|
194
|
+
* A resumed swap reads its deposit address out of host storage, which is not a
|
|
195
|
+
* trust boundary. Rendering an address from there, having never checked it,
|
|
196
|
+
* hands away the invariant at the one point the user is about to move funds by
|
|
197
|
+
* hand. TWO independent things are checked, and both are necessary:
|
|
198
|
+
*
|
|
199
|
+
* 1. The BINDING to the record it was stored under. An attacker can obtain a
|
|
200
|
+
* genuine attestation for their own deposit quote and transplant it into a
|
|
201
|
+
* victim's record; every field is internally consistent and the signature
|
|
202
|
+
* verifies. What it cannot fake is whose deal it is: v3 binds `wallet` and
|
|
203
|
+
* the record is filed under its owner, and `intent_id` must match the record.
|
|
204
|
+
* 2. The SIGNATURE, by `verifyDepositAttestation` against the pinned signer,
|
|
205
|
+
* plus the destination-token guard the live path runs.
|
|
206
|
+
*
|
|
207
|
+
* Takes the record as `unknown` because that is what storage hands back. A
|
|
208
|
+
* record persisted before the cutover to `@xochi/signer` (camelCase fields,
|
|
209
|
+
* `resolvedSettlementType`) is renamed first and then checked exactly like a
|
|
210
|
+
* current one; any other shape refuses. On success the result carries the
|
|
211
|
+
* proven binding in the current shape: render THAT, never the record passed
|
|
212
|
+
* in, and write it back if the record is re-persisted.
|
|
213
|
+
*
|
|
214
|
+
* NOT checked here: the deposit window. A resumed swap legitimately comes back
|
|
215
|
+
* after it closed, and the honest view then names no address to send to; the
|
|
216
|
+
* caller reads `attested.deposit_deadline` and renders accordingly.
|
|
217
|
+
*/
|
|
218
|
+
export function verifyStoredDeposit(raw, signer, expected) {
|
|
219
|
+
const read = readStoredDeposit(raw);
|
|
220
|
+
if (!read.ok)
|
|
221
|
+
return { ok: false, kind: "unavailable", reason: read.reason };
|
|
222
|
+
const { stored } = read;
|
|
223
|
+
const { attested } = stored;
|
|
224
|
+
if (attested.intent_id !== expected.intentId) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
kind: "unavailable",
|
|
228
|
+
reason: `stored deposit attests intent ${attested.intent_id}, record is ${expected.intentId}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (!expected.owner || canonical(attested.wallet) !== canonical(expected.owner)) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
kind: "unavailable",
|
|
235
|
+
reason: `stored deposit attests wallet ${attested.wallet}, record is filed under ` +
|
|
236
|
+
`${expected.owner || "(none)"}; refusing a binding for a different account`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (!SIGNATURE_RE.test(stored.attestation)) {
|
|
240
|
+
return { ok: false, kind: "unavailable", reason: "malformed deposit attestation signature" };
|
|
241
|
+
}
|
|
242
|
+
if (recipientIsDestinationToken(attested.recipient, attested.to_token)) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
kind: "unavailable",
|
|
246
|
+
reason: `payout recipient is the destination token contract (${attested.to_token}); funds sent there are unspendable`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
verifyDepositAttestation(attested, stored.attestation, signer);
|
|
251
|
+
return { ok: true, stored };
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
return depositRefusal(err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** The message's own canonicalisation: a 0x address lowercased, base58 verbatim. */
|
|
258
|
+
function canonical(value) {
|
|
259
|
+
return value.startsWith("0x") ? value.toLowerCase() : value;
|
|
260
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trade lifecycle as a pure state machine.
|
|
3
|
+
*
|
|
4
|
+
* update(model, msg) -> [model, effects] (machine.ts)
|
|
5
|
+
* view(model) -> Receipt (view.ts)
|
|
6
|
+
*
|
|
7
|
+
* Plus the wire decoders and the fund-safety gates the machine runs, exported
|
|
8
|
+
* so the widget and headless integrators share one implementation of each.
|
|
9
|
+
*/
|
|
10
|
+
export { init, update, QUOTE_DEBOUNCE_MS, QUOTE_REFRESH_BUFFER_MS, RECONCILE_AFTER_ATTEMPTS, SIGN_PROMPT_TIMEOUT_MS, type Model, type Msg, type MsgBody, type Effect, type ApiCall, type WalletCall, type TimerCall, type TimerName, type MachineEvent, type ErrorReport, type Context, type WalletContext, type Outcome, type StealthMetaAddress, type MachineConfig, type QuoteFlight, type SignFlow, type PollState, type ExecuteAttempt, } from "./machine.js";
|
|
11
|
+
export { view } from "./view.js";
|
|
12
|
+
export { payoutOf, costOf, settlementDisclosure, type Receipt, type Served, type PayoutSlot, type CostSlot, type QuoteSlot, type SigningSlot, type SettlementDisclosure, } from "./receipt.js";
|
|
13
|
+
export { phaseFromStatus, type SwapPhase } from "./phase.js";
|
|
14
|
+
export * from "./decisions.js";
|
|
15
|
+
export * from "./wire.js";
|
|
16
|
+
export * from "./state.js";
|
|
17
|
+
export * from "./deposit.js";
|
|
18
|
+
export * from "./quoteChecks.js";
|
|
19
|
+
export * from "./claimInfo.js";
|
|
20
|
+
export * from "./recipientEcho.js";
|
|
21
|
+
export * from "./solverPin.js";
|
|
22
|
+
export { applyStatusTransition } from "./transition.js";
|
|
23
|
+
export { validateSwapParams, type SwapValidationResult } from "./validateRequest.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trade lifecycle as a pure state machine.
|
|
3
|
+
*
|
|
4
|
+
* update(model, msg) -> [model, effects] (machine.ts)
|
|
5
|
+
* view(model) -> Receipt (view.ts)
|
|
6
|
+
*
|
|
7
|
+
* Plus the wire decoders and the fund-safety gates the machine runs, exported
|
|
8
|
+
* so the widget and headless integrators share one implementation of each.
|
|
9
|
+
*/
|
|
10
|
+
export { init, update, QUOTE_DEBOUNCE_MS, QUOTE_REFRESH_BUFFER_MS, RECONCILE_AFTER_ATTEMPTS, SIGN_PROMPT_TIMEOUT_MS, } from "./machine.js";
|
|
11
|
+
export { view } from "./view.js";
|
|
12
|
+
export { payoutOf, costOf, settlementDisclosure, } from "./receipt.js";
|
|
13
|
+
export { phaseFromStatus } from "./phase.js";
|
|
14
|
+
export * from "./decisions.js";
|
|
15
|
+
export * from "./wire.js";
|
|
16
|
+
export * from "./state.js";
|
|
17
|
+
export * from "./deposit.js";
|
|
18
|
+
export * from "./quoteChecks.js";
|
|
19
|
+
export * from "./claimInfo.js";
|
|
20
|
+
export * from "./recipientEcho.js";
|
|
21
|
+
export * from "./solverPin.js";
|
|
22
|
+
export { applyStatusTransition } from "./transition.js";
|
|
23
|
+
export { validateSwapParams } from "./validateRequest.js";
|