@recta/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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Recta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,139 @@
1
- # Temporary Holding Version
1
+ # @recta/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A client for [Recta](https://api.recta.build/llms.txt): before your agent moves stablecoins between blockchains, Recta
4
+ finds a cheaper route it can prove against the route you would use anyway, and charges exactly 5 % of the saving it
5
+ verifies on chain after you execute. No saving, no charge.
6
+
7
+ This package runs Recta's four steps with the wallet your agent already has:
8
+
9
+ ```
10
+ evaluate → authorizeFee → execute → report
11
+ ```
12
+
13
+ It never receives a key. Before it signs or sends anything it checks it locally, and those checks cannot be turned off.
14
+
15
+ ## Install
16
+
17
+ ```
18
+ npm install @recta/client viem
19
+ ```
20
+
21
+ `viem` (>= 2.30, < 3) is the only dependency, and you choose its version.
22
+
23
+ ## Use
24
+
25
+ ```ts
26
+ import { createRectaClient, viemSigner, viemChainReader } from "@recta/client";
27
+
28
+ const recta = createRectaClient({
29
+ signer: viemSigner({ account, walletClientFor: (chainId) => walletClients[chainId] }),
30
+ chain: viemChainReader((chainId) => publicClients[chainId]),
31
+ limits: {
32
+ maxPermitUsdcAtomic: 5_000_000n, // never authorize more than 5 USDC in one fee permit
33
+ maxNativeValueWei: 10n ** 15n, // never attach more than 0.001 native to one route transaction
34
+ minOutputBps: 9900, // required, 1–10000: the route must guarantee the recipient at least 99 % of the amount sent
35
+ },
36
+ });
37
+
38
+ // Dry run: evaluates and checks, signs no permit, sends no transaction.
39
+ const preview = await recta.run(request, { dryRun: true });
40
+
41
+ // The whole flow.
42
+ const result = await recta.run(request);
43
+ ```
44
+
45
+ `request` is Recta's evaluation request (schema in the
46
+ [service descriptor](https://api.recta.build/v1/golden-corridor/commercial-evaluations/schema)); its `sender` must be
47
+ your signer's address. Each step is also available alone: `evaluate`, `authorizeFee`, `check`, `execute`, `report`,
48
+ `ledger`, `payDebt`.
49
+
50
+ Your account must be an EOA: Recta's signed requests are EIP-191 signatures recovered to the sender's address.
51
+
52
+ ### Coinbase CDP accounts
53
+
54
+ A [Coinbase CDP](https://docs.cdp.coinbase.com) EVM server account (an EOA whose key stays in CDP) works through
55
+ `cdpSigner`. This package does not depend on the CDP SDK:
56
+
57
+ ```ts
58
+ import { CdpClient } from "@coinbase/cdp-sdk";
59
+ import { createRectaClient, cdpSigner, viemChainReader } from "@recta/client";
60
+
61
+ const cdp = new CdpClient();
62
+ const account = await cdp.evm.getOrCreateAccount({ name: "my-agent" });
63
+
64
+ const recta = createRectaClient({
65
+ signer: cdpSigner({ account }),
66
+ chain: viemChainReader((chainId) => publicClients[chainId]),
67
+ limits: { maxPermitUsdcAtomic: 5_000_000n, maxNativeValueWei: 10n ** 15n, minOutputBps: 9900 },
68
+ });
69
+ ```
70
+
71
+ CDP signs Recta's requests and the fee permit. On Ethereum, Base, Arbitrum, Optimism, Polygon and Avalanche, CDP also
72
+ sends each route transaction once this package has checked it. On any other chain, pass `walletClientFor` as well:
73
+ CDP then signs the transaction and your RPC sends it.
74
+
75
+ To limit the account to Recta's contracts at CDP's side too, apply the
76
+ [CDP Policy Engine policy Recta serves](https://api.recta.build/v1/golden-corridor/wallet-policy/cdp). CDP refuses
77
+ anything that policy does not allow, and it allows sending only on those six chains: with the policy applied, a route
78
+ on any other chain is refused by CDP unless you add your own rule for it.
79
+
80
+ The saving is measured against `baseline_provider`: the route your agent would use without Recta. Declare the one it
81
+ really uses. If your agent bridges through LI.FI (its public API or an aggregator built on it), declare `lifi`
82
+ wherever the fit check (`GET /v1/golden-corridor/fit`) lists it in `declarable_baselines`; it is refused with
83
+ `latency_class` SECONDS or MINUTES. Recta reads LI.FI's own transaction, its fee included. If that route is already the cheapest, Recta answers with no saving and charges nothing.
84
+
85
+ ## What is checked before signing
86
+
87
+ **Pins.** The package carries Recta's fee addresses in its own code: Permit2, x402's `upto` proxy, USDC on Base,
88
+ Recta's fee recipient and Recta's facilitator. Recta's wallet policy is read live and must agree with them; if it does
89
+ not, nothing is signed.
90
+
91
+ **The fee permit** (Permit2 `PermitWitnessTransferFrom` on Base) is signed only if the domain is Permit2 on Base, the
92
+ types are exactly Recta's, the spender is the `upto` proxy, the token is USDC, `witness.to` is Recta's recipient,
93
+ `witness.facilitator` is Recta's sender, the amount is the decision's cap plus what it carries from earlier unpaid fees
94
+ and is within `maxPermitUsdcAtomic`, and the deadline is in the future and no later than the commercial expiry plus 37
95
+ days. The typed data signed is rebuilt from those checked fields, not taken as the server sent it. Recta settles exactly
96
+ 5 % of the verified saving, never more than the permit.
97
+
98
+ **The fee permit is signed before the route can be reviewed.** Recta hands out a decision's transactions only once its
99
+ fee authorization is stored, so the permit comes first. Its ceiling (`max_fee_authorization` plus earlier unpaid fees,
100
+ never more than `maxPermitUsdcAtomic`) is the most you can owe for a decision; Recta still settles only 5 % of a saving
101
+ it verifies on chain, so a route you refuse below and never execute owes nothing.
102
+
103
+ **Every route transaction** is checked against a route table pinned in this package, generated from Recta's own server
104
+ configuration: for each EVM corridor, the source and destination tokens and the only contracts a route may approve
105
+ and call, with the destination each call must name. The wallet policy Recta serves never adds to it. A transaction is
106
+ sent only if: it is EIP-1559 with an empty access list, on the source chain, from your signer, within your request's
107
+ gas price ceilings; an approval is `approve` of the pinned source token, for exactly the transfer amount, to the pinned
108
+ contract the execution then calls; the execution decodes as that contract's own call (Across `deposit`, CCTP V2
109
+ `depositForBurn` or LayerZero OFT/Stargate `send`) naming your sender, your recipient, the source token and the exact
110
+ amount, the pinned destination (Across chain id and output token, with no message; CCTP domain, completable by
111
+ anyone; LayerZero endpoint id, with no executor options, compose message or OFT command), and a guaranteed output of at least `minOutputBps` of the
112
+ amount (Across `outputAmount`, CCTP amount minus `maxFee`, LayerZero `minAmountLD`); native value is allowed only as
113
+ the declared messaging fee, within `maxNativeValueWei`.
114
+
115
+ `report` returns `stage`: `ACCRUED` (Recta verified and recorded the saving), `PENDING` (report again later) or
116
+ `REFUSED` (Recta will not accrue this execution; see `body.reason_code`).
117
+
118
+ **Debts.** If Recta refuses a new evaluation because an earlier fee could not be collected, `evaluate` throws
119
+ `RectaDebtError` with the offer. The package never signs a debt by itself: call `payDebt(offer)` if you agree (same
120
+ checks, exact amount).
121
+
122
+ ## Limits of this version
123
+
124
+ - EVM sources and EVM recipients only. Solana, Tron and Bitcoin routes, Relay and deBridge are refused.
125
+ - A corridor or contract Recta adds after this release is refused until a new release pins it.
126
+ - Smart accounts (EIP-1271) cannot sign Recta's requests.
127
+
128
+ ## Provenance
129
+
130
+ Every release is published from [RectaBuild/recta](https://github.com/RectaBuild/recta) by GitHub Actions with npm
131
+ provenance and no long-lived token. Check it before you trust a version:
132
+
133
+ ```
134
+ npm audit signatures
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT
@@ -0,0 +1,55 @@
1
+ import { type PinnedRoute } from "./pinned-routes.js";
2
+ import type { RectaTransaction } from "./signer.js";
3
+ /** The fields of the evaluation request this package checks the artifact against. */
4
+ export interface EvaluationRequestView {
5
+ readonly source_network: string;
6
+ readonly source_asset: string;
7
+ readonly destination_network: string;
8
+ readonly destination_asset?: string;
9
+ readonly amount: string;
10
+ readonly sender: string;
11
+ readonly recipient: string;
12
+ readonly execution_authority?: {
13
+ readonly source_max_fee_per_gas_wei?: string;
14
+ readonly source_max_priority_fee_per_gas_wei?: string;
15
+ };
16
+ }
17
+ export interface ArtifactLimits {
18
+ readonly signer: string;
19
+ readonly nowUnix: bigint;
20
+ /** The most native value (wei) this agent agrees to attach to one transaction (LayerZero-based routes pay their
21
+ * messaging fee this way; Across and CCTP attach none). */
22
+ readonly maxNativeValueWei: bigint;
23
+ /** The least the recipient must be guaranteed to receive, in basis points of the amount sent (e.g. 9950 = 99.5 %):
24
+ * Across's `outputAmount`, CCTP's amount minus its `maxFee`, LayerZero's `minAmountLD`. Required. */
25
+ readonly minOutputBps: number;
26
+ }
27
+ export interface CheckedArtifact {
28
+ readonly resultId: string;
29
+ readonly transactions: readonly (RectaTransaction & {
30
+ readonly role: "APPROVAL" | "EXECUTION";
31
+ })[];
32
+ }
33
+ /** The pinned route of a request's corridor, or a refusal. */
34
+ export declare function pinnedRouteOf(request: Pick<EvaluationRequestView, "source_network" | "source_asset" | "destination_network" | "destination_asset">): PinnedRoute;
35
+ /**
36
+ * Checks every transaction of an execution artifact before any is sent, against the request the agent made and the
37
+ * route table pinned in this package (generated from Recta's server configuration; a served wallet policy never adds
38
+ * to it):
39
+ * - the artifact is Recta's, of this decision, and not expired; approvals come first and there is exactly one execution;
40
+ * - every transaction is EIP-1559 with an empty access list, on the source chain, from the signer, within the request's
41
+ * own gas price ceilings;
42
+ * - an approval is `approve` of the pinned source token to a pinned contract of the corridor, for exactly the transfer
43
+ * amount, and its spender is the contract the execution calls;
44
+ * - the execution calls a pinned contract of the corridor and decodes as that contract's protocol call (Across
45
+ * `deposit`, CCTP V2 `depositForBurn`, LayerZero OFT/Stargate `send`) naming the request's sender, recipient, source
46
+ * token and exact amount, the pinned destination (Across chain id and output token, CCTP domain with any caller, OFT
47
+ * endpoint id), and a guaranteed output of at least `minOutputBps` of the amount; its native value is the one the
48
+ * call declares, within the agent's limit.
49
+ * Anything else is refused.
50
+ */
51
+ export declare function checkExecutionArtifact(artifact: unknown, args: {
52
+ readonly resultId: string;
53
+ readonly request: EvaluationRequestView;
54
+ readonly limits: ArtifactLimits;
55
+ }): CheckedArtifact;
@@ -0,0 +1,207 @@
1
+ import { decodeFunctionData, padHex, parseAbi, parseUnits } from "viem";
2
+ import { EXECUTION_ARTIFACT_SCHEMA } from "./constants.js";
3
+ import { RectaCheckError } from "./errors.js";
4
+ import { PINNED_ROUTES } from "./pinned-routes.js";
5
+ const APPROVE_ABI = parseAbi(["function approve(address spender, uint256 amount)"]);
6
+ const ACROSS_DEPOSIT_ABI = parseAbi([
7
+ "function deposit(bytes32 depositor, bytes32 recipient, bytes32 inputToken, bytes32 outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, bytes32 exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityParameter, bytes message)",
8
+ ]);
9
+ const CCTP_DEPOSIT_FOR_BURN_ABI = parseAbi([
10
+ "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold)",
11
+ ]);
12
+ const OFT_SEND_ABI = parseAbi([
13
+ "struct SendParam { uint32 dstEid; bytes32 to; uint256 amountLD; uint256 minAmountLD; bytes extraOptions; bytes composeMsg; bytes oftCmd; }",
14
+ "struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; }",
15
+ "function send(SendParam _sendParam, MessagingFee _fee, address _refundAddress) payable",
16
+ ]);
17
+ const UINT = /^(0|[1-9][0-9]{0,77})$/;
18
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
19
+ const ZERO_WORD = `0x${"0".repeat(64)}`;
20
+ const word = (a) => padHex(a.toLowerCase(), { size: 32 }).toLowerCase();
21
+ const same = (a, b) => typeof a === "string" && a.toLowerCase() === b.toLowerCase();
22
+ /** The pinned route of a request's corridor, or a refusal. */
23
+ export function pinnedRouteOf(request) {
24
+ const key = `${request.source_network}:${request.source_asset}>${request.destination_network}:${request.destination_asset ?? request.source_asset}`;
25
+ const route = Object.hasOwn(PINNED_ROUTES, key) ? PINNED_ROUTES[key] : undefined;
26
+ if (route === undefined)
27
+ throw new RectaCheckError("corridor", `${key} is not an EVM corridor this release of the package knows`);
28
+ return route;
29
+ }
30
+ /**
31
+ * Checks every transaction of an execution artifact before any is sent, against the request the agent made and the
32
+ * route table pinned in this package (generated from Recta's server configuration; a served wallet policy never adds
33
+ * to it):
34
+ * - the artifact is Recta's, of this decision, and not expired; approvals come first and there is exactly one execution;
35
+ * - every transaction is EIP-1559 with an empty access list, on the source chain, from the signer, within the request's
36
+ * own gas price ceilings;
37
+ * - an approval is `approve` of the pinned source token to a pinned contract of the corridor, for exactly the transfer
38
+ * amount, and its spender is the contract the execution calls;
39
+ * - the execution calls a pinned contract of the corridor and decodes as that contract's protocol call (Across
40
+ * `deposit`, CCTP V2 `depositForBurn`, LayerZero OFT/Stargate `send`) naming the request's sender, recipient, source
41
+ * token and exact amount, the pinned destination (Across chain id and output token, CCTP domain with any caller, OFT
42
+ * endpoint id), and a guaranteed output of at least `minOutputBps` of the amount; its native value is the one the
43
+ * call declares, within the agent's limit.
44
+ * Anything else is refused.
45
+ */
46
+ export function checkExecutionArtifact(artifact, args) {
47
+ const fail = (check, message) => { throw new RectaCheckError(check, message); };
48
+ const { request, limits } = args;
49
+ if (!Number.isInteger(limits.minOutputBps) || limits.minOutputBps < 1 || limits.minOutputBps > 10_000)
50
+ fail("limits", "minOutputBps must be an integer from 1 to 10000");
51
+ const a = typeof artifact === "object" && artifact !== null ? artifact : fail("artifact", "the artifact is not an object");
52
+ if (a["schema"] !== EXECUTION_ARTIFACT_SCHEMA)
53
+ fail("artifact", `the artifact schema is ${String(a["schema"])}, not ${EXECUTION_ARTIFACT_SCHEMA}`);
54
+ if (a["result_id"] !== args.resultId)
55
+ fail("artifact", "the artifact belongs to another decision");
56
+ const expiry = typeof a["commercial_expiry_unix"] === "string" && UINT.test(a["commercial_expiry_unix"]) ? BigInt(a["commercial_expiry_unix"]) : fail("artifact", "commercial_expiry_unix is not an integer");
57
+ if (expiry <= limits.nowUnix)
58
+ fail("artifact_expired", "the decision's commercial expiry has passed");
59
+ const signer = limits.signer.toLowerCase();
60
+ if (!same(request.sender, signer))
61
+ fail("sender", "the request's sender is not the signer");
62
+ if (!ADDRESS.test(request.recipient))
63
+ fail("recipient", "this version only checks EVM recipients (0x addresses)");
64
+ const route = pinnedRouteOf(request);
65
+ const amount = parseUnits(request.amount, route.source_decimals);
66
+ const minimum = { amount, bps: BigInt(limits.minOutputBps), route };
67
+ const maxFee = request.execution_authority?.source_max_fee_per_gas_wei;
68
+ const maxPriority = request.execution_authority?.source_max_priority_fee_per_gas_wei;
69
+ const raw = Array.isArray(a["transactions"]) ? a["transactions"] : fail("artifact", "transactions is not a list");
70
+ const out = [];
71
+ let executionTo;
72
+ const approvalSpenders = [];
73
+ for (const [i, item] of raw.entries()) {
74
+ const t = typeof item === "object" && item !== null ? item : fail("transaction", `transaction ${i} is not an object`);
75
+ if (t["role"] !== "APPROVAL" && t["role"] !== "EXECUTION")
76
+ fail("transaction", `transaction ${i} has role ${String(t["role"])}`);
77
+ const role = t["role"];
78
+ if (executionTo !== undefined)
79
+ fail("transaction_order", role === "APPROVAL" ? "an approval follows the execution" : "more than one execution");
80
+ if (t["transaction_type"] !== "EIP1559" || !Array.isArray(t["access_list"]) || t["access_list"].length !== 0)
81
+ fail("transaction_type", `transaction ${i} is not EIP-1559 with an empty access list`);
82
+ const n = (k) => (typeof t[k] === "string" && UINT.test(t[k]) ? BigInt(t[k]) : fail("transaction", `transaction ${i}: ${k} is not an integer`));
83
+ if (n("chain_id") !== BigInt(route.source_chain_id))
84
+ fail("transaction_chain", `transaction ${i} is not on ${request.source_network} (${route.source_chain_id})`);
85
+ if (!same(t["from"], signer))
86
+ fail("transaction_from", `transaction ${i} is not from the signer`);
87
+ const to = typeof t["to"] === "string" && ADDRESS.test(t["to"]) ? t["to"].toLowerCase() : fail("transaction", `transaction ${i}: to is not an address`);
88
+ const data = typeof t["data"] === "string" && /^0x([0-9a-fA-F]{2})*$/.test(t["data"]) ? t["data"] : fail("transaction", `transaction ${i}: data is not hex`);
89
+ const value = n("value_wei");
90
+ const fee = n("max_fee_per_gas_wei");
91
+ const priority = n("max_priority_fee_per_gas_wei");
92
+ if (maxFee !== undefined && fee > BigInt(maxFee))
93
+ fail("transaction_gas_price", `transaction ${i} pays more per gas than the request allows`);
94
+ if (maxPriority !== undefined && priority > BigInt(maxPriority))
95
+ fail("transaction_gas_price", `transaction ${i} tips more per gas than the request allows`);
96
+ if (role === "APPROVAL") {
97
+ if (to !== route.source_token || value !== 0n)
98
+ fail("approval", `transaction ${i} is not an approval on the source token`);
99
+ const d = (() => { try {
100
+ return decodeFunctionData({ abi: APPROVE_ABI, data });
101
+ }
102
+ catch {
103
+ return fail("approval", `transaction ${i} is not approve(spender, amount)`);
104
+ } })();
105
+ const spender = d.args[0].toLowerCase();
106
+ if (!route.contracts.some((c) => c.address === spender))
107
+ fail("approval_spender", `transaction ${i} approves ${spender}, which is not a pinned contract of this corridor`);
108
+ if (d.args[1] !== amount)
109
+ fail("approval_amount", `transaction ${i} approves ${d.args[1]}, not exactly the transfer amount ${amount}`);
110
+ approvalSpenders.push(spender);
111
+ }
112
+ else {
113
+ const candidates = route.contracts.filter((c) => c.address === to);
114
+ if (candidates.length === 0)
115
+ fail("execution_target", `the execution calls ${to}, which is not a pinned contract of this corridor`);
116
+ checkExecutionCall(data, value, candidates, { token: route.source_token, sender: signer, recipient: request.recipient, maxNativeValueWei: limits.maxNativeValueWei, minimum });
117
+ executionTo = to;
118
+ }
119
+ out.push({ role, chainId: route.source_chain_id, to: to, data, value, gas: n("gas_limit"), maxFeePerGas: fee, maxPriorityFeePerGas: priority });
120
+ }
121
+ if (executionTo === undefined)
122
+ fail("transaction_order", "the artifact has no execution");
123
+ if (approvalSpenders.some((s) => s !== executionTo))
124
+ fail("approval_spender", "an approval's spender is not the contract the execution calls");
125
+ return { resultId: args.resultId, transactions: out };
126
+ }
127
+ /** The least output allowed, in output-token units: ⌈amount · bps / 10 000⌉ scaled from source to output decimals. */
128
+ function minimumOutput(m, inOutputUnits) {
129
+ const inSource = (m.amount * m.bps + 9999n) / 10000n;
130
+ if (!inOutputUnits)
131
+ return inSource;
132
+ const d = m.route.output_decimals - m.route.source_decimals;
133
+ return d >= 0 ? inSource * 10n ** BigInt(d) : (inSource + 10n ** BigInt(-d) - 1n) / 10n ** BigInt(-d);
134
+ }
135
+ function checkExecutionCall(data, value, candidates, e) {
136
+ const fail = (check, message) => { throw new RectaCheckError(check, message); };
137
+ const amount = e.minimum.amount;
138
+ const destinations = (protocol) => candidates.filter((c) => c.protocol === protocol).map((c) => c.destination_id);
139
+ const decode = (abi) => { try {
140
+ return decodeFunctionData({ abi: abi, data });
141
+ }
142
+ catch {
143
+ return undefined;
144
+ } };
145
+ const across = destinations("across").length > 0 ? decode(ACROSS_DEPOSIT_ABI) : undefined;
146
+ if (across !== undefined) {
147
+ const [depositor, recipient, inputToken, outputToken, inputAmount, outputAmount, destinationChainId, , , , , message] = across.args;
148
+ if (depositor.toLowerCase() !== word(e.sender) || recipient.toLowerCase() !== word(e.recipient))
149
+ fail("execution_parties", "the Across deposit does not name the request's sender and recipient");
150
+ if (inputToken.toLowerCase() !== word(e.token) || inputAmount !== amount)
151
+ fail("execution_amount", "the Across deposit is not the request's token and amount");
152
+ if (!destinations("across").some((id) => BigInt(id) === destinationChainId))
153
+ fail("execution_destination", `the Across deposit goes to chain ${destinationChainId}, not the request's destination`);
154
+ if (outputToken.toLowerCase() !== word(e.minimum.route.output_token))
155
+ fail("execution_destination", "the Across deposit's output token is not the request's destination token");
156
+ if (message !== "0x")
157
+ fail("execution_destination", "the Across deposit carries a message (the recipient would run it as a handler)");
158
+ if (outputAmount < minimumOutput(e.minimum, true))
159
+ fail("execution_min_output", `the Across deposit guarantees ${outputAmount}, less than your minimum`);
160
+ if (value !== 0n)
161
+ fail("execution_value", "the Across deposit attaches native value");
162
+ return;
163
+ }
164
+ const cctp = destinations("cctp_v2").length > 0 ? decode(CCTP_DEPOSIT_FOR_BURN_ABI) : undefined;
165
+ if (cctp !== undefined) {
166
+ const [burnAmount, domain, mintRecipient, burnToken, destinationCaller, maxFee] = cctp.args;
167
+ if (mintRecipient.toLowerCase() !== word(e.recipient))
168
+ fail("execution_parties", "the CCTP burn does not mint to the request's recipient");
169
+ if (!same(burnToken, e.token) || burnAmount !== amount)
170
+ fail("execution_amount", "the CCTP burn is not the request's token and amount");
171
+ if (!destinations("cctp_v2").includes(domain))
172
+ fail("execution_destination", `the CCTP burn goes to domain ${domain}, not the request's destination`);
173
+ if (destinationCaller.toLowerCase() !== ZERO_WORD)
174
+ fail("execution_destination", "the CCTP burn restricts who may complete it");
175
+ if (maxFee >= burnAmount || burnAmount - maxFee < minimumOutput(e.minimum, true))
176
+ fail("execution_min_output", "the CCTP burn's max fee leaves less than your minimum");
177
+ if (value !== 0n)
178
+ fail("execution_value", "the CCTP burn attaches native value");
179
+ return;
180
+ }
181
+ const oft = destinations("oft").length > 0 ? decode(OFT_SEND_ABI) : undefined;
182
+ if (oft !== undefined) {
183
+ const [param, fee, refund] = oft.args;
184
+ if (param.to.toLowerCase() !== word(e.recipient) || !same(refund, e.sender))
185
+ fail("execution_parties", "the OFT send does not name the request's recipient and refund the sender");
186
+ if (param.amountLD !== amount)
187
+ fail("execution_amount", "the OFT send is not the request's amount");
188
+ if (!destinations("oft").includes(param.dstEid))
189
+ fail("execution_destination", `the OFT send goes to endpoint ${param.dstEid}, not the request's destination`);
190
+ // Recta's own OFT routes are plain sends: no executor options (which could add a native drop to another address),
191
+ // no compose message, no OFT command.
192
+ if (param.composeMsg !== "0x")
193
+ fail("execution_destination", "the OFT send carries a compose message");
194
+ if (param.extraOptions !== "0x")
195
+ fail("execution_destination", "the OFT send carries executor options");
196
+ if (param.oftCmd !== "0x")
197
+ fail("execution_destination", "the OFT send carries an OFT command");
198
+ if (param.minAmountLD < minimumOutput(e.minimum, false))
199
+ fail("execution_min_output", `the OFT send guarantees ${param.minAmountLD}, less than your minimum`);
200
+ if (fee.lzTokenFee !== 0n || fee.nativeFee !== value)
201
+ fail("execution_value", "the OFT send's native fee is not the value it attaches");
202
+ if (value > e.maxNativeValueWei)
203
+ fail("execution_value_limit", `the OFT send attaches ${value} wei, more than your limit ${e.maxNativeValueWei}`);
204
+ return;
205
+ }
206
+ fail("execution_call", "the execution is not the reviewed call of this contract (Across deposit, CCTP V2 depositForBurn, OFT send)");
207
+ }
@@ -0,0 +1,119 @@
1
+ import { type Hex } from "viem";
2
+ import { type CheckedArtifact, type EvaluationRequestView } from "./artifact.js";
3
+ import type { RectaChainReader, RectaSigner, RectaTypedData } from "./signer.js";
4
+ export interface RectaClientOptions {
5
+ readonly signer: RectaSigner;
6
+ readonly chain: RectaChainReader;
7
+ readonly limits: {
8
+ /** The most one fee permit may authorize, in USDC atomic units (6 decimals): e.g. 5_000_000n for 5 USDC. */
9
+ readonly maxPermitUsdcAtomic: bigint;
10
+ /** The most native value (wei) one route transaction may attach (LayerZero messaging fees). */
11
+ readonly maxNativeValueWei: bigint;
12
+ /** The least the recipient must be guaranteed, in basis points of the amount sent (e.g. 9950 = 99.5 %). */
13
+ readonly minOutputBps: number;
14
+ };
15
+ /** Defaults to the global fetch. */
16
+ readonly fetch?: typeof fetch;
17
+ /** Milliseconds since the epoch; defaults to Date.now. */
18
+ readonly now?: () => number;
19
+ }
20
+ /** The text a sender signs for one request (EIP-191), exactly as Recta's server rebuilds it. */
21
+ export declare function signedRequestMessage(f: {
22
+ readonly method: string;
23
+ readonly path: string;
24
+ readonly idempotencyKey: string;
25
+ readonly bodyKeccak256: string;
26
+ readonly issuedAt: number;
27
+ }): string;
28
+ export type FeeOutcome = {
29
+ readonly kind: "NOT_REQUIRED";
30
+ } | {
31
+ readonly kind: "ALREADY_AUTHORIZED";
32
+ } | {
33
+ readonly kind: "AUTHORIZED";
34
+ readonly amountUsdcAtomic: bigint;
35
+ readonly signature: Hex;
36
+ } | {
37
+ readonly kind: "DRY_RUN";
38
+ readonly amountUsdcAtomic: bigint;
39
+ readonly typedData: RectaTypedData;
40
+ };
41
+ /**
42
+ * Recta's flow for an agent that holds its own EOA: evaluate → authorizeFee → execute → report. Every step can be
43
+ * called alone. Nothing is signed or sent before this package's own checks pass (see `checkFeePermit` and
44
+ * `checkExecutionArtifact`); they cannot be turned off.
45
+ */
46
+ export declare function createRectaClient(options: RectaClientOptions): {
47
+ evaluate: (request: EvaluationRequestView & Record<string, unknown>, opts?: {
48
+ readonly idempotencyKey?: string;
49
+ }) => Promise<Record<string, unknown>>;
50
+ authorizeFee: (resultId: string, opts?: {
51
+ readonly dryRun?: boolean;
52
+ }) => Promise<{
53
+ readonly fee: FeeOutcome;
54
+ readonly artifact: unknown;
55
+ }>;
56
+ check: (resultId: string, artifact: unknown, request: EvaluationRequestView) => Promise<CheckedArtifact>;
57
+ execute: (resultId: string, artifact: unknown, request: EvaluationRequestView) => Promise<{
58
+ readonly approvalTxHash?: Hex;
59
+ readonly originTxHash: Hex;
60
+ }>;
61
+ report: (resultId: string, originTxHash: Hex, approvalTxHash?: Hex) => Promise<{
62
+ readonly stage: "ACCRUED" | "PENDING" | "REFUSED";
63
+ readonly body: Record<string, unknown>;
64
+ }>;
65
+ ledger: () => Promise<Record<string, unknown>>;
66
+ payDebt: (offer: unknown) => Promise<Record<string, unknown>>;
67
+ run: (request: EvaluationRequestView & Record<string, unknown>, opts?: {
68
+ readonly dryRun?: boolean;
69
+ readonly idempotencyKey?: string;
70
+ }) => Promise<{
71
+ stage: "NO_ROUTE";
72
+ decision: Record<string, unknown>;
73
+ fee?: never;
74
+ checked?: never;
75
+ } | {
76
+ stage: "DRY_RUN";
77
+ decision: Record<string, unknown>;
78
+ fee: {
79
+ readonly kind: "DRY_RUN";
80
+ readonly amountUsdcAtomic: bigint;
81
+ readonly typedData: RectaTypedData;
82
+ };
83
+ checked?: never;
84
+ } | {
85
+ stage: "DRY_RUN";
86
+ decision: Record<string, unknown>;
87
+ fee: {
88
+ readonly kind: "NOT_REQUIRED";
89
+ } | {
90
+ readonly kind: "ALREADY_AUTHORIZED";
91
+ } | {
92
+ readonly kind: "AUTHORIZED";
93
+ readonly amountUsdcAtomic: bigint;
94
+ readonly signature: Hex;
95
+ };
96
+ checked: CheckedArtifact;
97
+ } | {
98
+ report: {
99
+ readonly stage: "ACCRUED" | "PENDING" | "REFUSED";
100
+ readonly body: Record<string, unknown>;
101
+ };
102
+ approvalTxHash?: Hex;
103
+ originTxHash: Hex;
104
+ stage: "REPORTED";
105
+ decision: Record<string, unknown>;
106
+ fee: {
107
+ readonly kind: "NOT_REQUIRED";
108
+ } | {
109
+ readonly kind: "ALREADY_AUTHORIZED";
110
+ } | {
111
+ readonly kind: "AUTHORIZED";
112
+ readonly amountUsdcAtomic: bigint;
113
+ readonly signature: Hex;
114
+ };
115
+ checked?: never;
116
+ }>;
117
+ walletPolicy: () => Promise<void>;
118
+ };
119
+ export type RectaClient = ReturnType<typeof createRectaClient>;