@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,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swap flow phase: the presentation phase of the intent lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
import type { IntentStatus } from "../shared/index.js";
|
|
5
|
+
export type SwapPhase = "confirm" | "awaiting-wallet" | "deposit" | "submitting" | "bridging" | "settling" | "success" | "refunded" | "failed" | "may-have-moved" | "not-sent" | "pending";
|
|
6
|
+
/**
|
|
7
|
+
* Derive the modal phase from intent status.
|
|
8
|
+
*
|
|
9
|
+
* - Wallet rejection sets status back to "quoted" (with error "Signature
|
|
10
|
+
* rejected"), so soft-cancel returns to the confirm surface automatically.
|
|
11
|
+
* - Only "failed" maps to the failure view (Retry). A `failed` the caller
|
|
12
|
+
* marks `mayHaveMoved` maps to "may-have-moved" instead. An `executing` the
|
|
13
|
+
* caller marks `notSent` maps to "not-sent". An in-doubt swap
|
|
14
|
+
* keeps a non-failure status (executing/bridging/settling); when the caller
|
|
15
|
+
* knows it is reconciling, it surfaces as "pending" ("still confirming"),
|
|
16
|
+
* never as a failure. This covers both the slow-fill reconcile flip and the
|
|
17
|
+
* #226 fail-closed success:false downgrade (funds moved, settlement
|
|
18
|
+
* unconfirmed).
|
|
19
|
+
*/
|
|
20
|
+
export declare function phaseFromStatus(status: IntentStatus, opts?: {
|
|
21
|
+
reconciling?: boolean;
|
|
22
|
+
mayHaveMoved?: boolean;
|
|
23
|
+
notSent?: boolean;
|
|
24
|
+
}): SwapPhase;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swap flow phase: the presentation phase of the intent lifecycle.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Derive the modal phase from intent status.
|
|
6
|
+
*
|
|
7
|
+
* - Wallet rejection sets status back to "quoted" (with error "Signature
|
|
8
|
+
* rejected"), so soft-cancel returns to the confirm surface automatically.
|
|
9
|
+
* - Only "failed" maps to the failure view (Retry). A `failed` the caller
|
|
10
|
+
* marks `mayHaveMoved` maps to "may-have-moved" instead. An `executing` the
|
|
11
|
+
* caller marks `notSent` maps to "not-sent". An in-doubt swap
|
|
12
|
+
* keeps a non-failure status (executing/bridging/settling); when the caller
|
|
13
|
+
* knows it is reconciling, it surfaces as "pending" ("still confirming"),
|
|
14
|
+
* never as a failure. This covers both the slow-fill reconcile flip and the
|
|
15
|
+
* #226 fail-closed success:false downgrade (funds moved, settlement
|
|
16
|
+
* unconfirmed).
|
|
17
|
+
*/
|
|
18
|
+
export function phaseFromStatus(status, opts = {}) {
|
|
19
|
+
switch (status) {
|
|
20
|
+
case "signing":
|
|
21
|
+
return "awaiting-wallet";
|
|
22
|
+
case "awaiting_deposit":
|
|
23
|
+
return "deposit";
|
|
24
|
+
case "executing":
|
|
25
|
+
if (opts.notSent)
|
|
26
|
+
return "not-sent";
|
|
27
|
+
return opts.reconciling ? "pending" : "submitting";
|
|
28
|
+
case "bridging":
|
|
29
|
+
return opts.reconciling ? "pending" : "bridging";
|
|
30
|
+
case "settling":
|
|
31
|
+
return opts.reconciling ? "pending" : "settling";
|
|
32
|
+
case "completed":
|
|
33
|
+
return "success";
|
|
34
|
+
case "refunded":
|
|
35
|
+
return "refunded";
|
|
36
|
+
case "failed":
|
|
37
|
+
return opts.mayHaveMoved ? "may-have-moved" : "failed";
|
|
38
|
+
// idle | pending | quoting | quoted | expired -> the confirm surface
|
|
39
|
+
default:
|
|
40
|
+
return "confirm";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@xochi/signer` value checks the machine runs on a signable (pull-route)
|
|
3
|
+
* quote at ingest, adapted to the machine's request and refusal shapes.
|
|
4
|
+
*
|
|
5
|
+
* `assertIntentMatchesRequest` holds the served XochiIntent to the request that
|
|
6
|
+
* produced it (wallet, chains, tokens, recipient, settlement preference, amount,
|
|
7
|
+
* and the intent deadline against the wall clock). `assertPullMatchesIntent`
|
|
8
|
+
* then holds the origin pull to that intent (chain, token, amount, expiry, the
|
|
9
|
+
* Permit2 contract) and, where the caller pinned one, its recipient.
|
|
10
|
+
*
|
|
11
|
+
* Both are additive to the machine's own gates (`validateSolverPin`,
|
|
12
|
+
* `validatePayoutPin`, the recipient echo, the claim-info factory), which keep
|
|
13
|
+
* running where they always ran.
|
|
14
|
+
*/
|
|
15
|
+
import { type PinnedPullRecipients, type QuoteRequestParams } from "@xochi/signer";
|
|
16
|
+
import type { QuoteRequest, QuoteResponse } from "./wire.js";
|
|
17
|
+
/**
|
|
18
|
+
* The request in the signer's snake_case parameter spelling.
|
|
19
|
+
*
|
|
20
|
+
* Built from the request the machine SENT (already trimmed by
|
|
21
|
+
* `buildQuoteRequest`), never from the panel state, so the checks compare the
|
|
22
|
+
* served terms against the bytes that asked for them. The widget only quotes
|
|
23
|
+
* exact-input, so `swap_kind` is stated rather than left to the signer's default.
|
|
24
|
+
*/
|
|
25
|
+
export declare function quoteRequestParams(request: QuoteRequest): QuoteRequestParams;
|
|
26
|
+
export type SignableQuoteCheck = {
|
|
27
|
+
ok: true;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
kind: "intent_mismatch" | "pull_mismatch";
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Run both signer checks over a quote that carries an intent to sign.
|
|
35
|
+
*
|
|
36
|
+
* `pullRecipients` is the CALLER's pin (per chain, per method); it is never
|
|
37
|
+
* derived from `/api/capabilities`. Absent, the pull's recipient is left to the
|
|
38
|
+
* machine's own solver allowlist, and the signer still binds chain, token,
|
|
39
|
+
* amount and expiry.
|
|
40
|
+
*/
|
|
41
|
+
export declare function checkSignableQuote(request: QuoteRequest, quote: QuoteResponse, pullRecipients: PinnedPullRecipients | undefined): SignableQuoteCheck;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `@xochi/signer` value checks the machine runs on a signable (pull-route)
|
|
3
|
+
* quote at ingest, adapted to the machine's request and refusal shapes.
|
|
4
|
+
*
|
|
5
|
+
* `assertIntentMatchesRequest` holds the served XochiIntent to the request that
|
|
6
|
+
* produced it (wallet, chains, tokens, recipient, settlement preference, amount,
|
|
7
|
+
* and the intent deadline against the wall clock). `assertPullMatchesIntent`
|
|
8
|
+
* then holds the origin pull to that intent (chain, token, amount, expiry, the
|
|
9
|
+
* Permit2 contract) and, where the caller pinned one, its recipient.
|
|
10
|
+
*
|
|
11
|
+
* Both are additive to the machine's own gates (`validateSolverPin`,
|
|
12
|
+
* `validatePayoutPin`, the recipient echo, the claim-info factory), which keep
|
|
13
|
+
* running where they always ran.
|
|
14
|
+
*/
|
|
15
|
+
import { XochiIntentMismatchError, assertIntentMatchesRequest, assertPullMatchesIntent, } from "@xochi/signer";
|
|
16
|
+
/**
|
|
17
|
+
* The request in the signer's snake_case parameter spelling.
|
|
18
|
+
*
|
|
19
|
+
* Built from the request the machine SENT (already trimmed by
|
|
20
|
+
* `buildQuoteRequest`), never from the panel state, so the checks compare the
|
|
21
|
+
* served terms against the bytes that asked for them. The widget only quotes
|
|
22
|
+
* exact-input, so `swap_kind` is stated rather than left to the signer's default.
|
|
23
|
+
*/
|
|
24
|
+
export function quoteRequestParams(request) {
|
|
25
|
+
return {
|
|
26
|
+
wallet: request.wallet,
|
|
27
|
+
from_chain_id: request.fromChainId,
|
|
28
|
+
to_chain_id: request.toChainId,
|
|
29
|
+
from_token: request.fromToken,
|
|
30
|
+
to_token: request.toToken,
|
|
31
|
+
from_amount: request.fromAmount,
|
|
32
|
+
swap_kind: "exact_input",
|
|
33
|
+
settlement_preference: request.settlementPreference,
|
|
34
|
+
recipient_address: request.recipientAddress,
|
|
35
|
+
refund_address: request.refundAddress,
|
|
36
|
+
stealth_spending_pub_key: request.stealthMetaAddress?.spendingPubKey,
|
|
37
|
+
stealth_viewing_pub_key: request.stealthMetaAddress?.viewingPubKey,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Run both signer checks over a quote that carries an intent to sign.
|
|
42
|
+
*
|
|
43
|
+
* `pullRecipients` is the CALLER's pin (per chain, per method); it is never
|
|
44
|
+
* derived from `/api/capabilities`. Absent, the pull's recipient is left to the
|
|
45
|
+
* machine's own solver allowlist, and the signer still binds chain, token,
|
|
46
|
+
* amount and expiry.
|
|
47
|
+
*/
|
|
48
|
+
export function checkSignableQuote(request, quote, pullRecipients) {
|
|
49
|
+
// The signer reads the wire spelling: `eip712`, `pull_authorization`, and the
|
|
50
|
+
// raw `escrow` envelope, which it refuses (this client runs no escrow binding).
|
|
51
|
+
const served = {
|
|
52
|
+
eip712: quote.eip712Data,
|
|
53
|
+
pull_authorization: quote.pullAuthorization,
|
|
54
|
+
escrow: quote.escrow,
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
assertIntentMatchesRequest(quoteRequestParams(request), served);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return { ok: false, kind: "intent_mismatch", reason: refusalReason(err) };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
assertPullMatchesIntent(served, { expectedPullRecipients: pullRecipients });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return { ok: false, kind: "pull_mismatch", reason: refusalReason(err) };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The signer's own message for its mismatch class. Anything else still refuses
|
|
72
|
+
* (fail closed) and is labelled as unexpected so it is not read as a verdict.
|
|
73
|
+
*/
|
|
74
|
+
function refusalReason(err) {
|
|
75
|
+
if (err instanceof XochiIntentMismatchError)
|
|
76
|
+
return err.message;
|
|
77
|
+
return `verification failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Receipt: what the user is told about a trade, as data.
|
|
3
|
+
*
|
|
4
|
+
* Every slot is a discriminated union, and the shapes are chosen so the three
|
|
5
|
+
* claims the trade receipt must never make cannot even be written down:
|
|
6
|
+
*
|
|
7
|
+
* - A payout floor. `PayoutSlot` has no minimum field at all. The contract is
|
|
8
|
+
* "exactly this, or a full refund" (`min_to_amount` is never enforced on any
|
|
9
|
+
* settlement path), so there is nowhere to put an "estimated min".
|
|
10
|
+
* - A shielded payout with a hidden amount. Every `SettlementDisclosure`
|
|
11
|
+
* variant fixes `amount: "visible"`; Shielded hides the recipient's account,
|
|
12
|
+
* never the amount.
|
|
13
|
+
* - A host-supplied number. Amounts and costs are `Served<...>`, a brand only
|
|
14
|
+
* this package can apply, and only to figures read off the worker's own
|
|
15
|
+
* response. A host (an embedding page, a prop, a query string) has no way to
|
|
16
|
+
* construct one without a cast.
|
|
17
|
+
*/
|
|
18
|
+
import type { SettlementType } from "../shared/index.js";
|
|
19
|
+
import type { QuoteResponse } from "./wire.js";
|
|
20
|
+
import type { SwapPhase } from "./phase.js";
|
|
21
|
+
import type { SignRefusal } from "./state.js";
|
|
22
|
+
declare const SERVED: unique symbol;
|
|
23
|
+
/** A figure read off the solver/worker's own response. Constructed only here. */
|
|
24
|
+
export type Served<T extends string | number> = T & {
|
|
25
|
+
readonly [SERVED]: true;
|
|
26
|
+
};
|
|
27
|
+
/** The one payout promise. */
|
|
28
|
+
export type PayoutSlot = {
|
|
29
|
+
kind: "none";
|
|
30
|
+
}
|
|
31
|
+
/** An anonymous, unsignable price: the same amount, marked as a preview. */
|
|
32
|
+
| {
|
|
33
|
+
kind: "preview";
|
|
34
|
+
amount: Served<string>;
|
|
35
|
+
}
|
|
36
|
+
/** "Receive exactly X, or get a full refund." */
|
|
37
|
+
| {
|
|
38
|
+
kind: "exact";
|
|
39
|
+
amount: Served<string>;
|
|
40
|
+
otherwise: "full_refund";
|
|
41
|
+
};
|
|
42
|
+
/** The one cost figure: the served all-in `cost_bps`, never reconstructed. */
|
|
43
|
+
export type CostSlot = {
|
|
44
|
+
kind: "none";
|
|
45
|
+
}
|
|
46
|
+
/** The guard could not run. "Unavailable", never zero. */
|
|
47
|
+
| {
|
|
48
|
+
kind: "unavailable";
|
|
49
|
+
} | {
|
|
50
|
+
kind: "served";
|
|
51
|
+
bps: Served<number>;
|
|
52
|
+
totalUsd: Served<string> | null;
|
|
53
|
+
};
|
|
54
|
+
/** What the settlement mode hides and what it leaves on-chain. */
|
|
55
|
+
export type SettlementDisclosure = {
|
|
56
|
+
mode: "public";
|
|
57
|
+
recipient: "visible";
|
|
58
|
+
amount: "visible";
|
|
59
|
+
recipientAction: "none";
|
|
60
|
+
} | {
|
|
61
|
+
mode: "stealth";
|
|
62
|
+
recipient: "one_time_address";
|
|
63
|
+
amount: "visible";
|
|
64
|
+
recipientAction: "claim";
|
|
65
|
+
} | {
|
|
66
|
+
mode: "shielded";
|
|
67
|
+
recipient: "private_account";
|
|
68
|
+
amount: "visible";
|
|
69
|
+
recipientAction: "none";
|
|
70
|
+
};
|
|
71
|
+
/** The held quote, as the receipt panel shows it. */
|
|
72
|
+
export type QuoteSlot = {
|
|
73
|
+
kind: "none";
|
|
74
|
+
} | {
|
|
75
|
+
kind: "loading";
|
|
76
|
+
} | {
|
|
77
|
+
kind: "declined";
|
|
78
|
+
reason: string | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A quote is on screen. `refreshing` means a replacement is being fetched
|
|
82
|
+
* while the held one stays mounted (its figures dim rather than vanish).
|
|
83
|
+
*/
|
|
84
|
+
| {
|
|
85
|
+
kind: "ready";
|
|
86
|
+
executable: boolean;
|
|
87
|
+
refreshing: boolean;
|
|
88
|
+
expiresInSec: number | null;
|
|
89
|
+
};
|
|
90
|
+
/** Where the signing ceremony stands. */
|
|
91
|
+
export type SigningSlot = {
|
|
92
|
+
kind: "unavailable";
|
|
93
|
+
}
|
|
94
|
+
/** A deposit route: no signature at all; confirming reveals the address. */
|
|
95
|
+
| {
|
|
96
|
+
kind: "deposit";
|
|
97
|
+
} | {
|
|
98
|
+
kind: "ready";
|
|
99
|
+
signatures: 1 | 2;
|
|
100
|
+
} | {
|
|
101
|
+
kind: "in_wallet";
|
|
102
|
+
step: 1 | 2;
|
|
103
|
+
of: 1 | 2;
|
|
104
|
+
}
|
|
105
|
+
/** Deep-link wallets: signature 1 is held, waiting for the user's tap for 2. */
|
|
106
|
+
| {
|
|
107
|
+
kind: "awaiting_second";
|
|
108
|
+
} | {
|
|
109
|
+
kind: "submitted";
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The Worker answered the execute not-accepted (the solver is busy), and the
|
|
113
|
+
* same signed request is sent again automatically at `retryAt` (unix
|
|
114
|
+
* seconds). This is resend `resend` of at most `of`. The phase stays
|
|
115
|
+
* `submitting`. Suggested copy, which makes no funds claim (the signed
|
|
116
|
+
* request stays executable during the wait): "Solver busy. Resending in Ns
|
|
117
|
+
* (retry r of n)." with "Keep this tab open. If it closes, this tab will not
|
|
118
|
+
* resend the order."
|
|
119
|
+
*/
|
|
120
|
+
| {
|
|
121
|
+
kind: "busy";
|
|
122
|
+
retryAt: number;
|
|
123
|
+
resend: number;
|
|
124
|
+
of: number;
|
|
125
|
+
}
|
|
126
|
+
/** The sign ceremony was refused before funds could move; the quote is still on screen. */
|
|
127
|
+
| {
|
|
128
|
+
kind: "refused";
|
|
129
|
+
refusal: SignRefusal;
|
|
130
|
+
};
|
|
131
|
+
export interface Receipt {
|
|
132
|
+
phase: SwapPhase;
|
|
133
|
+
quote: QuoteSlot;
|
|
134
|
+
payout: PayoutSlot;
|
|
135
|
+
cost: CostSlot;
|
|
136
|
+
settlement: SettlementDisclosure;
|
|
137
|
+
signing: SigningSlot;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The payout promise a quote makes. Only a quote the solver can fill makes one;
|
|
141
|
+
* `minToAmount` is never read.
|
|
142
|
+
*/
|
|
143
|
+
export declare function payoutOf(quote: QuoteResponse | null): PayoutSlot;
|
|
144
|
+
/**
|
|
145
|
+
* The cost a quote states. Absent (or unusable) `cost_bps` is `unavailable`, so
|
|
146
|
+
* a renderer cannot fall back to a figure of its own making.
|
|
147
|
+
*/
|
|
148
|
+
export declare function costOf(quote: QuoteResponse | null): CostSlot;
|
|
149
|
+
/** What a settlement mode hides and what it leaves visible. */
|
|
150
|
+
export declare function settlementDisclosure(mode: SettlementType): SettlementDisclosure;
|
|
151
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Receipt: what the user is told about a trade, as data.
|
|
3
|
+
*
|
|
4
|
+
* Every slot is a discriminated union, and the shapes are chosen so the three
|
|
5
|
+
* claims the trade receipt must never make cannot even be written down:
|
|
6
|
+
*
|
|
7
|
+
* - A payout floor. `PayoutSlot` has no minimum field at all. The contract is
|
|
8
|
+
* "exactly this, or a full refund" (`min_to_amount` is never enforced on any
|
|
9
|
+
* settlement path), so there is nowhere to put an "estimated min".
|
|
10
|
+
* - A shielded payout with a hidden amount. Every `SettlementDisclosure`
|
|
11
|
+
* variant fixes `amount: "visible"`; Shielded hides the recipient's account,
|
|
12
|
+
* never the amount.
|
|
13
|
+
* - A host-supplied number. Amounts and costs are `Served<...>`, a brand only
|
|
14
|
+
* this package can apply, and only to figures read off the worker's own
|
|
15
|
+
* response. A host (an embedding page, a prop, a query string) has no way to
|
|
16
|
+
* construct one without a cast.
|
|
17
|
+
*/
|
|
18
|
+
function served(value) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The payout promise a quote makes. Only a quote the solver can fill makes one;
|
|
23
|
+
* `minToAmount` is never read.
|
|
24
|
+
*/
|
|
25
|
+
export function payoutOf(quote) {
|
|
26
|
+
if (!quote?.canSolve)
|
|
27
|
+
return { kind: "none" };
|
|
28
|
+
const amount = served(quote.toAmount);
|
|
29
|
+
return quote.preview
|
|
30
|
+
? { kind: "preview", amount }
|
|
31
|
+
: { kind: "exact", amount, otherwise: "full_refund" };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The cost a quote states. Absent (or unusable) `cost_bps` is `unavailable`, so
|
|
35
|
+
* a renderer cannot fall back to a figure of its own making.
|
|
36
|
+
*/
|
|
37
|
+
export function costOf(quote) {
|
|
38
|
+
if (!quote)
|
|
39
|
+
return { kind: "none" };
|
|
40
|
+
const bps = quote.costBps;
|
|
41
|
+
if (bps === undefined || !Number.isFinite(bps) || bps < 0)
|
|
42
|
+
return { kind: "unavailable" };
|
|
43
|
+
const totalUsd = quote.feeBreakdown?.totalUsd;
|
|
44
|
+
return { kind: "served", bps: served(bps), totalUsd: totalUsd ? served(totalUsd) : null };
|
|
45
|
+
}
|
|
46
|
+
const DISCLOSURES = {
|
|
47
|
+
public: { mode: "public", recipient: "visible", amount: "visible", recipientAction: "none" },
|
|
48
|
+
stealth: {
|
|
49
|
+
mode: "stealth",
|
|
50
|
+
recipient: "one_time_address",
|
|
51
|
+
amount: "visible",
|
|
52
|
+
recipientAction: "claim",
|
|
53
|
+
},
|
|
54
|
+
shielded: {
|
|
55
|
+
mode: "shielded",
|
|
56
|
+
recipient: "private_account",
|
|
57
|
+
amount: "visible",
|
|
58
|
+
recipientAction: "none",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/** What a settlement mode hides and what it leaves visible. */
|
|
62
|
+
export function settlementDisclosure(mode) {
|
|
63
|
+
return DISCLOSURES[mode];
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipient-echo guard for non-EVM (Tron) destination routes.
|
|
3
|
+
*
|
|
4
|
+
* These routes settle to a user-typed destination address rather than the
|
|
5
|
+
* connected wallet, and a deposit route carries no wallet signature to bind it.
|
|
6
|
+
* Recipient integrity therefore rides on the authenticated quote channel
|
|
7
|
+
* (client <-> Xochi worker): when the client asked for a specific recipient, the
|
|
8
|
+
* solvable quote MUST echo exactly that address. A mismatch (or a missing echo
|
|
9
|
+
* on a solvable quote) means the recipient the quote would bind is not the one
|
|
10
|
+
* the user chose, so the quote is rejected rather than signed/executed. See #113.
|
|
11
|
+
*
|
|
12
|
+
* Base58 (Tron) is case-sensitive, so the comparison is verbatim -- no
|
|
13
|
+
* normalization. EVM routes send no recipient (they settle to the connected
|
|
14
|
+
* wallet), so there is nothing to check and this returns null.
|
|
15
|
+
*/
|
|
16
|
+
export declare function recipientEchoError(requested: string | undefined, quoted: string | undefined): "recipient_unconfirmed" | "recipient_mismatch" | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipient-echo guard for non-EVM (Tron) destination routes.
|
|
3
|
+
*
|
|
4
|
+
* These routes settle to a user-typed destination address rather than the
|
|
5
|
+
* connected wallet, and a deposit route carries no wallet signature to bind it.
|
|
6
|
+
* Recipient integrity therefore rides on the authenticated quote channel
|
|
7
|
+
* (client <-> Xochi worker): when the client asked for a specific recipient, the
|
|
8
|
+
* solvable quote MUST echo exactly that address. A mismatch (or a missing echo
|
|
9
|
+
* on a solvable quote) means the recipient the quote would bind is not the one
|
|
10
|
+
* the user chose, so the quote is rejected rather than signed/executed. See #113.
|
|
11
|
+
*
|
|
12
|
+
* Base58 (Tron) is case-sensitive, so the comparison is verbatim -- no
|
|
13
|
+
* normalization. EVM routes send no recipient (they settle to the connected
|
|
14
|
+
* wallet), so there is nothing to check and this returns null.
|
|
15
|
+
*/
|
|
16
|
+
export function recipientEchoError(requested, quoted) {
|
|
17
|
+
// No recipient requested: EVM route settling to the connected wallet.
|
|
18
|
+
if (!requested)
|
|
19
|
+
return null;
|
|
20
|
+
// Requested a recipient but the solvable quote did not echo one back.
|
|
21
|
+
if (!quoted)
|
|
22
|
+
return "recipient_unconfirmed";
|
|
23
|
+
return requested === quoted ? null : "recipient_mismatch";
|
|
24
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed pin for origin-pull authorizations: binds both WHERE the funds go
|
|
3
|
+
* and HOW MUCH, before the wallet signs.
|
|
4
|
+
*
|
|
5
|
+
* Two axes can move funds on the origin leg. The recipient (pull `to` for
|
|
6
|
+
* ERC-3009, `spender` for Permit2) decides who collects. The amount (`value` for
|
|
7
|
+
* ERC-3009, `permitted.amount` for Permit2) decides how much. The user's
|
|
8
|
+
* signature is the on-chain authority for both (ERC-3009 moves exactly `value`,
|
|
9
|
+
* Permit2 caps the pull at `permitted.amount`), so a pull whose amount exceeds
|
|
10
|
+
* the intended input can overdraw the wallet even to a correctly-pinned
|
|
11
|
+
* recipient. This pin binds the recipient to an operator allowlist AND the amount
|
|
12
|
+
* to the intended input, refusing to sign on either mismatch. The trust anchors
|
|
13
|
+
* are the client's own config and the user's own quoted amount, neither of which
|
|
14
|
+
* the quote server can influence.
|
|
15
|
+
*
|
|
16
|
+
* Defense in depth: the Xochi worker independently refuses to serve a quote whose
|
|
17
|
+
* pull amount does not equal `from_amount` (packages/worker-lib), and the
|
|
18
|
+
* on-chain token / Permit2 contract verifies the signature over the amount
|
|
19
|
+
* actually pulled. So a compromised quote server cannot overdraw even if this
|
|
20
|
+
* client check is bypassed.
|
|
21
|
+
*
|
|
22
|
+
* Permit2 has no on-chain recipient guard (the spender chooses the recipient at
|
|
23
|
+
* call time), so its recipient is ALWAYS fail-closed: an unpinned permit2 pull is
|
|
24
|
+
* rejected. ERC-3009 binds `to` in the signed digest and the token enforces
|
|
25
|
+
* `msg.sender == to`, so an unpinned `to` stays bounded (accepted unless a hard
|
|
26
|
+
* pin is requested). Once an allowlist is configured, both methods enforce it.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors the raxol agent-side pin (`solver_allowed?/2`). See GitHub #333
|
|
29
|
+
* (recipient pin) and the #170 origin-leg amount binding.
|
|
30
|
+
*/
|
|
31
|
+
import type { Eip712TypedData } from "../shared/index.js";
|
|
32
|
+
export interface SolverPinConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Lowercased addresses valid as an ERC-3009 pull `to` (the solver collection
|
|
35
|
+
* EOA, or the verified per-chain XochiPull USDC contract once #591 flips). This
|
|
36
|
+
* class NEVER accepts a Permit2 spender: the two roles are distinct addresses
|
|
37
|
+
* with distinct on-chain authority, so mixing them would let one impersonate the
|
|
38
|
+
* other.
|
|
39
|
+
*/
|
|
40
|
+
erc3009Allowlist: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Lowercased addresses valid as a Permit2 `spender` (the verified
|
|
43
|
+
* XochiPullPermit2 contract, #591 sibling / PULL2). Distinct from the ERC-3009
|
|
44
|
+
* class above so a solver EOA can never stand in as the Permit2 spender.
|
|
45
|
+
*/
|
|
46
|
+
permit2Allowlist: string[];
|
|
47
|
+
/** When true, ERC-3009 also requires a pin (Permit2 always does). */
|
|
48
|
+
requirePin: boolean;
|
|
49
|
+
}
|
|
50
|
+
export type SolverPinField = "pull_to" | "pull_spender" | "pull_type" | "pull_amount" | "payout_amount" | "payout_recipient";
|
|
51
|
+
export type SolverPinResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
field: SolverPinField;
|
|
56
|
+
reason: string;
|
|
57
|
+
};
|
|
58
|
+
/** What the client rendered, and therefore what the user agreed to receive. */
|
|
59
|
+
export interface PayoutPinExpectation {
|
|
60
|
+
/** Destination-token smallest units: the `toAmount` shown in the panel. */
|
|
61
|
+
toAmount: string;
|
|
62
|
+
/**
|
|
63
|
+
* Who the panel said would be paid: the connected wallet on a same-VM route,
|
|
64
|
+
* or the user-entered destination address on a non-EVM one.
|
|
65
|
+
*
|
|
66
|
+
* Typed as possibly-undefined on purpose. The caller's wallet address is
|
|
67
|
+
* `string | undefined`, and the honest handling of "we do not know who this
|
|
68
|
+
* was for" is to refuse, not to assert it away with a cast. Casting also loses
|
|
69
|
+
* to the pre-commit `eslint --fix`, which strips assertions it deems
|
|
70
|
+
* unnecessary.
|
|
71
|
+
*/
|
|
72
|
+
recipient: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validate a served origin-pull authorization against the solver pin. Returns
|
|
76
|
+
* `{ ok: true }` when the wallet may sign, or `{ ok: false, field, reason }` to
|
|
77
|
+
* abort before signing.
|
|
78
|
+
*
|
|
79
|
+
* `expectedAmount` is the intended input amount (origin-token smallest units, the
|
|
80
|
+
* user's own quoted `from_amount`). The pull is rejected unless its collected
|
|
81
|
+
* amount equals it, so a served pull cannot overdraw beyond what the user meant
|
|
82
|
+
* to spend even when the recipient is correctly pinned.
|
|
83
|
+
*/
|
|
84
|
+
export declare function validateSolverPin(pull: Eip712TypedData, config: SolverPinConfig, expectedAmount: bigint): SolverPinResult;
|
|
85
|
+
/**
|
|
86
|
+
* Bind the DESTINATION leg of the intent payload to what the client rendered,
|
|
87
|
+
* before the wallet signs it.
|
|
88
|
+
*
|
|
89
|
+
* `validateSolverPin` above protects the origin leg: how much leaves the wallet
|
|
90
|
+
* and who collects it. This is the mirror for the other end -- how much comes
|
|
91
|
+
* back and who receives it -- and it exists because those two fields are the only
|
|
92
|
+
* ones a user cannot verify by reading a wallet prompt. A payload whose
|
|
93
|
+
* `toAmount` is lower than the number on screen, or whose `recipient` is an
|
|
94
|
+
* address the user never chose, produces a wallet prompt that looks entirely
|
|
95
|
+
* normal.
|
|
96
|
+
*
|
|
97
|
+
* The trust anchor is the same as the origin pin's and that is the whole point:
|
|
98
|
+
* the expectation comes from the client's own rendered quote, never from another
|
|
99
|
+
* field of the same served response. The Xochi worker independently refuses to
|
|
100
|
+
* serve a quote whose `eip712.message.toAmount` disagrees with the `to_amount` it
|
|
101
|
+
* advertises (`servedPayoutMismatch`, packages/worker-lib), so against a healthy
|
|
102
|
+
* backend this check never fires. It covers what that one structurally cannot:
|
|
103
|
+
* anything that reaches the payload after the worker has vouched for it, or a
|
|
104
|
+
* worker that is itself compromised. A firing check here is an incident, not a
|
|
105
|
+
* tuning problem.
|
|
106
|
+
*
|
|
107
|
+
* Fail-closed on a missing or unreadable field: never sign a payout we cannot
|
|
108
|
+
* read.
|
|
109
|
+
*/
|
|
110
|
+
export declare function validatePayoutPin(intent: Eip712TypedData, expected: PayoutPinExpectation): SolverPinResult;
|