@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/dist/client.js ADDED
@@ -0,0 +1,190 @@
1
+ import { keccak256 } from "viem";
2
+ import { checkExecutionArtifact } from "./artifact.js";
3
+ import { ISSUED_AT_HEADER, PATHS, RECTA_ORIGIN, SIGNATURE_HEADER, SIGNED_REQUEST_PREFIX, SIGNER_HEADER } from "./constants.js";
4
+ import { RectaCheckError, RectaDebtError, RectaHttpError } from "./errors.js";
5
+ import { checkFeePermit } from "./fee.js";
6
+ import { parseWalletPolicy } from "./policy.js";
7
+ /** The text a sender signs for one request (EIP-191), exactly as Recta's server rebuilds it. */
8
+ export function signedRequestMessage(f) {
9
+ return [
10
+ SIGNED_REQUEST_PREFIX,
11
+ `method: ${f.method.toUpperCase()}`,
12
+ `path: ${f.path}`,
13
+ `idempotency-key: ${f.idempotencyKey}`,
14
+ `body-keccak256: ${f.bodyKeccak256.toLowerCase()}`,
15
+ `issued-at: ${f.issuedAt}`,
16
+ ].join("\n");
17
+ }
18
+ const RESULT_ID = /^[A-Za-z0-9_-]+$/;
19
+ const field = (v, k) => (typeof v === "object" && v !== null ? v[k] : undefined);
20
+ const resultIdOf = (v) => {
21
+ if (!RESULT_ID.test(v))
22
+ throw new RectaCheckError("result_id", "not a Recta result id");
23
+ return v;
24
+ };
25
+ /**
26
+ * Recta's flow for an agent that holds its own EOA: evaluate → authorizeFee → execute → report. Every step can be
27
+ * called alone. Nothing is signed or sent before this package's own checks pass (see `checkFeePermit` and
28
+ * `checkExecutionArtifact`); they cannot be turned off.
29
+ */
30
+ export function createRectaClient(options) {
31
+ const { signer, chain, limits } = options;
32
+ const doFetch = options.fetch ?? fetch;
33
+ const now = options.now ?? Date.now;
34
+ const nowUnix = () => BigInt(Math.floor(now() / 1000));
35
+ const payer = signer.address.toLowerCase();
36
+ let policyChecked = false;
37
+ async function call(method, path, body, idempotencyKey, signed = true) {
38
+ const bytes = body === undefined ? new Uint8Array() : new TextEncoder().encode(JSON.stringify(body));
39
+ const headers = { accept: "application/json" };
40
+ if (body !== undefined)
41
+ headers["content-type"] = "application/json";
42
+ if (idempotencyKey !== undefined)
43
+ headers["idempotency-key"] = idempotencyKey;
44
+ if (signed) {
45
+ const issuedAt = Math.floor(now() / 1000);
46
+ const message = signedRequestMessage({ method, path, idempotencyKey: idempotencyKey ?? "", bodyKeccak256: keccak256(bytes), issuedAt });
47
+ headers[SIGNER_HEADER] = signer.address;
48
+ headers[ISSUED_AT_HEADER] = String(issuedAt);
49
+ headers[SIGNATURE_HEADER] = await signer.signMessage(message);
50
+ }
51
+ const response = await doFetch(`${RECTA_ORIGIN}${path}`, { method, headers, ...(body === undefined ? {} : { body: bytes }) });
52
+ const text = await response.text();
53
+ let parsed = text;
54
+ try {
55
+ parsed = JSON.parse(text);
56
+ }
57
+ catch { /* not JSON: keep the text */ }
58
+ return { status: response.status, body: parsed };
59
+ }
60
+ /** Reads Recta's live wallet policy once and refuses to go on unless its fee block names this package's pins. */
61
+ async function walletPolicy() {
62
+ if (policyChecked)
63
+ return;
64
+ const a = await call("GET", PATHS.walletPolicy, undefined, undefined, false);
65
+ if (a.status !== 200)
66
+ throw new RectaHttpError(a.status, a.body);
67
+ parseWalletPolicy(a.body);
68
+ policyChecked = true;
69
+ }
70
+ /** Step 1: a durable decision (free). Throws RectaDebtError when the sender owes a fee first. */
71
+ async function evaluate(request, opts = {}) {
72
+ if (typeof request.sender !== "string" || request.sender.toLowerCase() !== payer)
73
+ throw new RectaCheckError("sender", "the request's sender must be the signer");
74
+ await walletPolicy();
75
+ const a = await call("POST", PATHS.evaluations, request, opts.idempotencyKey ?? crypto.randomUUID());
76
+ if (a.status === 402)
77
+ throw new RectaDebtError(a.body, field(a.body, "debt_offer") ?? null);
78
+ if (a.status !== 200)
79
+ throw new RectaHttpError(a.status, a.body);
80
+ return a.body;
81
+ }
82
+ /**
83
+ * Step 2: the decision's committed transactions. With Recta's collection on, the artifact first asks for the fee
84
+ * permit: it is checked, rebuilt and signed only if it passes, then stored, and the answer carries the artifact. With
85
+ * `dryRun`, it stops before signing and returns the typed data it would sign.
86
+ */
87
+ async function authorizeFee(resultId, opts = {}) {
88
+ const id = resultIdOf(resultId);
89
+ const artifactPath = `${PATHS.evaluations}/${id}/execution-artifact`;
90
+ for (let attempt = 0; attempt < 2; attempt += 1) {
91
+ const a = await call("GET", artifactPath);
92
+ if (a.status === 200) {
93
+ const authorized = field(field(a.body, "fee_authorization"), "status") === "AUTHORIZED";
94
+ return { fee: { kind: authorized ? "ALREADY_AUTHORIZED" : "NOT_REQUIRED" }, artifact: field(a.body, "artifact") };
95
+ }
96
+ if (a.status !== 402 || field(a.body, "status") !== "SIGNATURE_REQUIRED")
97
+ throw new RectaHttpError(a.status, a.body);
98
+ const expiry = field(a.body, "commercial_expiry_unix");
99
+ const checked = checkFeePermit(field(a.body, "fee_authorization"), {
100
+ payer,
101
+ maxPermitUsdcAtomic: limits.maxPermitUsdcAtomic,
102
+ nowUnix: nowUnix(),
103
+ ...(typeof expiry === "string" && /^[0-9]+$/.test(expiry) ? { commercialExpiryUnix: BigInt(expiry) } : {}),
104
+ });
105
+ if (opts.dryRun === true)
106
+ return { fee: { kind: "DRY_RUN", amountUsdcAtomic: checked.amount, typedData: checked.typedData }, artifact: undefined };
107
+ const signature = await signer.signTypedData(checked.typedData);
108
+ const s = await call("POST", `${PATHS.evaluations}/${id}/fee-authorization`, { signature, amount_usdc_atomic: checked.amount.toString() });
109
+ if (s.status === 200 || s.status === 201)
110
+ return { fee: { kind: "AUTHORIZED", amountUsdcAtomic: checked.amount, signature }, artifact: field(s.body, "artifact") };
111
+ if (!(s.status === 409 && field(s.body, "reason_code") === "OFFER_STALE"))
112
+ throw new RectaHttpError(s.status, s.body);
113
+ }
114
+ throw new RectaHttpError(409, { reason_code: "OFFER_STALE" });
115
+ }
116
+ /** Checks the artifact against the request and the package's pinned routes; nothing is sent. */
117
+ async function check(resultId, artifact, request) {
118
+ await walletPolicy();
119
+ return checkExecutionArtifact(artifact, { resultId: resultIdOf(resultId), request, limits: { signer: payer, nowUnix: nowUnix(), maxNativeValueWei: limits.maxNativeValueWei, minOutputBps: limits.minOutputBps } });
120
+ }
121
+ /** Step 3: checks, then sends the transactions in order, each mined successfully before the next. */
122
+ async function execute(resultId, artifact, request) {
123
+ const checked = await check(resultId, artifact, request);
124
+ let approvalTxHash;
125
+ for (const tx of checked.transactions) {
126
+ const { role, ...send } = tx;
127
+ const hash = await signer.sendTransaction(send);
128
+ if (!(await chain.waitForSuccess(tx.chainId, hash)))
129
+ throw new RectaCheckError("transaction_failed", `${role} transaction ${hash} reverted`);
130
+ if (role === "APPROVAL")
131
+ approvalTxHash = hash;
132
+ else
133
+ return { ...(approvalTxHash === undefined ? {} : { approvalTxHash }), originTxHash: hash };
134
+ }
135
+ throw new RectaCheckError("transaction_order", "the artifact has no execution");
136
+ }
137
+ /**
138
+ * Step 4: reports the executed route; Recta verifies the delivery on chain and records the saving and its 5 %.
139
+ * `stage`: ACCRUED (verified and recorded), PENDING (not yet verifiable: report again later) or REFUSED (Recta will
140
+ * not accrue this execution; see `body.reason_code`).
141
+ */
142
+ async function report(resultId, originTxHash, approvalTxHash) {
143
+ const a = await call("POST", `${PATHS.evaluations}/${resultIdOf(resultId)}/execution`, { origin_tx_hash: originTxHash, ...(approvalTxHash === undefined ? {} : { approval_tx_hash: approvalTxHash }) });
144
+ const body = a.body;
145
+ if (a.status === 200 || a.status === 201)
146
+ return { stage: "ACCRUED", body };
147
+ if (a.status === 202)
148
+ return { stage: "PENDING", body };
149
+ if (a.status === 422)
150
+ return { stage: "REFUSED", body };
151
+ throw new RectaHttpError(a.status, a.body);
152
+ }
153
+ /** The signer's fees and whether each was collected. */
154
+ async function ledger() {
155
+ const a = await call("GET", `${PATHS.accruals}${payer}`);
156
+ if (a.status !== 200)
157
+ throw new RectaHttpError(a.status, a.body);
158
+ return a.body;
159
+ }
160
+ /** Signs and submits a debt permit Recta offered (RectaDebtError.offer), after the same checks as a fee permit. */
161
+ async function payDebt(offer) {
162
+ const checked = checkFeePermit(offer, { payer, maxPermitUsdcAtomic: limits.maxPermitUsdcAtomic, nowUnix: nowUnix() }, "DEBT");
163
+ const debtId = field(offer, "debt_id");
164
+ if (typeof debtId !== "string")
165
+ throw new RectaCheckError("debt", "the offer has no debt_id");
166
+ const signature = await signer.signTypedData(checked.typedData);
167
+ const a = await call("POST", PATHS.feeDebts, { debt_id: debtId, deadline_unix: checked.deadline.toString(), signature });
168
+ if (a.status !== 200 && a.status !== 201)
169
+ throw new RectaHttpError(a.status, a.body);
170
+ return a.body;
171
+ }
172
+ /**
173
+ * The four steps in a row. With `dryRun`, it evaluates and checks but signs no permit and sends no transaction: it
174
+ * stops at the permit (collection on) or after checking the artifact (collection off).
175
+ */
176
+ async function run(request, opts = {}) {
177
+ const decision = await evaluate(request, opts.idempotencyKey === undefined ? {} : { idempotencyKey: opts.idempotencyKey });
178
+ const resultId = field(decision, "result_id");
179
+ if (field(field(decision, "decision"), "state") !== "VERIFIED_POSITIVE" || typeof resultId !== "string")
180
+ return { stage: "NO_ROUTE", decision };
181
+ const { fee, artifact } = await authorizeFee(resultId, opts.dryRun === true ? { dryRun: true } : {});
182
+ if (fee.kind === "DRY_RUN")
183
+ return { stage: "DRY_RUN", decision, fee };
184
+ if (opts.dryRun === true)
185
+ return { stage: "DRY_RUN", decision, fee, checked: await check(resultId, artifact, request) };
186
+ const sent = await execute(resultId, artifact, request);
187
+ return { stage: "REPORTED", decision, fee, ...sent, report: await report(resultId, sent.originTxHash, sent.approvalTxHash) };
188
+ }
189
+ return { evaluate, authorizeFee, check, execute, report, ledger, payDebt, run, walletPolicy };
190
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Values this package pins in its own code instead of trusting the server. A Recta server that served other values
3
+ * (a compromised deploy, a man in the middle, a wrong origin) makes every call refuse before anything is signed: moving
4
+ * the fee anywhere else would need a new release of this package as well.
5
+ *
6
+ * Every value is checked equal to the Recta server's own configuration by a test in the Recta repository.
7
+ */
8
+ export declare const RECTA_ORIGIN = "https://api.recta.build";
9
+ /** The fee: x402 `upto` on Base, a Permit2 `permitWitnessTransferFrom`. All lowercase. */
10
+ export declare const FEE_CHAIN_ID = 8453;
11
+ export declare const PERMIT2 = "0x000000000022d473030f116ddee9f6b43ac78ba3";
12
+ export declare const UPTO_PROXY = "0x4020a4f3b7b90cca423b9fabcc0ce57c6c240002";
13
+ export declare const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
14
+ /** The only address a Recta fee can pay (`witness.to`). */
15
+ export declare const FEE_RECEIVER = "0xae866c10e0f2d942da3cddddda082feb65faf108";
16
+ /** The only address that can settle a Recta fee permit (`witness.facilitator`). */
17
+ export declare const FEE_FACILITATOR = "0x993a2a880d20eae16dc65fec374dcadabef1098a";
18
+ /** A permit may stay valid at most this long after its decision's commercial expiry (37 days). */
19
+ export declare const PERMIT_DEADLINE_AFTER_EXPIRY_SECONDS: bigint;
20
+ export declare const FEE_PERMIT_TYPES: {
21
+ readonly PermitWitnessTransferFrom: readonly [{
22
+ readonly name: "permitted";
23
+ readonly type: "TokenPermissions";
24
+ }, {
25
+ readonly name: "spender";
26
+ readonly type: "address";
27
+ }, {
28
+ readonly name: "nonce";
29
+ readonly type: "uint256";
30
+ }, {
31
+ readonly name: "deadline";
32
+ readonly type: "uint256";
33
+ }, {
34
+ readonly name: "witness";
35
+ readonly type: "Witness";
36
+ }];
37
+ readonly TokenPermissions: readonly [{
38
+ readonly name: "token";
39
+ readonly type: "address";
40
+ }, {
41
+ readonly name: "amount";
42
+ readonly type: "uint256";
43
+ }];
44
+ readonly Witness: readonly [{
45
+ readonly name: "to";
46
+ readonly type: "address";
47
+ }, {
48
+ readonly name: "facilitator";
49
+ readonly type: "address";
50
+ }, {
51
+ readonly name: "validAfter";
52
+ readonly type: "uint256";
53
+ }];
54
+ };
55
+ /** Signed requests (EIP-191 `personal_sign` by the sender's EOA). */
56
+ export declare const SIGNED_REQUEST_PREFIX = "Recta signed request v1";
57
+ export declare const SIGNER_HEADER = "x-recta-signer";
58
+ export declare const ISSUED_AT_HEADER = "x-recta-issued-at";
59
+ export declare const SIGNATURE_HEADER = "x-recta-signature";
60
+ export declare const PATHS: {
61
+ readonly evaluations: "/v1/golden-corridor/commercial-evaluations";
62
+ readonly accruals: "/v1/golden-corridor/commercial-accruals/";
63
+ readonly feeDebts: "/v1/golden-corridor/fee-debts";
64
+ readonly walletPolicy: "/v1/golden-corridor/wallet-policy";
65
+ };
66
+ export declare const WALLET_POLICY_SCHEMA = "RECTA_AGENT_WALLET_POLICY_V1";
67
+ export declare const EXECUTION_ARTIFACT_SCHEMA = "RECTA_COMMERCIAL_EXECUTION_ARTIFACT_V2";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Values this package pins in its own code instead of trusting the server. A Recta server that served other values
3
+ * (a compromised deploy, a man in the middle, a wrong origin) makes every call refuse before anything is signed: moving
4
+ * the fee anywhere else would need a new release of this package as well.
5
+ *
6
+ * Every value is checked equal to the Recta server's own configuration by a test in the Recta repository.
7
+ */
8
+ export const RECTA_ORIGIN = "https://api.recta.build";
9
+ /** The fee: x402 `upto` on Base, a Permit2 `permitWitnessTransferFrom`. All lowercase. */
10
+ export const FEE_CHAIN_ID = 8453;
11
+ export const PERMIT2 = "0x000000000022d473030f116ddee9f6b43ac78ba3";
12
+ export const UPTO_PROXY = "0x4020a4f3b7b90cca423b9fabcc0ce57c6c240002";
13
+ export const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
14
+ /** The only address a Recta fee can pay (`witness.to`). */
15
+ export const FEE_RECEIVER = "0xae866c10e0f2d942da3cddddda082feb65faf108";
16
+ /** The only address that can settle a Recta fee permit (`witness.facilitator`). */
17
+ export const FEE_FACILITATOR = "0x993a2a880d20eae16dc65fec374dcadabef1098a";
18
+ /** A permit may stay valid at most this long after its decision's commercial expiry (37 days). */
19
+ export const PERMIT_DEADLINE_AFTER_EXPIRY_SECONDS = 37n * 86400n;
20
+ export const FEE_PERMIT_TYPES = {
21
+ PermitWitnessTransferFrom: [
22
+ { name: "permitted", type: "TokenPermissions" },
23
+ { name: "spender", type: "address" },
24
+ { name: "nonce", type: "uint256" },
25
+ { name: "deadline", type: "uint256" },
26
+ { name: "witness", type: "Witness" },
27
+ ],
28
+ TokenPermissions: [
29
+ { name: "token", type: "address" },
30
+ { name: "amount", type: "uint256" },
31
+ ],
32
+ Witness: [
33
+ { name: "to", type: "address" },
34
+ { name: "facilitator", type: "address" },
35
+ { name: "validAfter", type: "uint256" },
36
+ ],
37
+ };
38
+ /** Signed requests (EIP-191 `personal_sign` by the sender's EOA). */
39
+ export const SIGNED_REQUEST_PREFIX = "Recta signed request v1";
40
+ export const SIGNER_HEADER = "x-recta-signer";
41
+ export const ISSUED_AT_HEADER = "x-recta-issued-at";
42
+ export const SIGNATURE_HEADER = "x-recta-signature";
43
+ export const PATHS = {
44
+ evaluations: "/v1/golden-corridor/commercial-evaluations",
45
+ accruals: "/v1/golden-corridor/commercial-accruals/",
46
+ feeDebts: "/v1/golden-corridor/fee-debts",
47
+ walletPolicy: "/v1/golden-corridor/wallet-policy",
48
+ };
49
+ export const WALLET_POLICY_SCHEMA = "RECTA_AGENT_WALLET_POLICY_V1";
50
+ export const EXECUTION_ARTIFACT_SCHEMA = "RECTA_COMMERCIAL_EXECUTION_ARTIFACT_V2";
@@ -0,0 +1,21 @@
1
+ /** A local check refused to sign or send: nothing was signed or sent. `check` names the check. */
2
+ export declare class RectaCheckError extends Error {
3
+ readonly check: string;
4
+ readonly name: string;
5
+ constructor(check: string, message: string);
6
+ }
7
+ /** Recta answered with an error. `body` is Recta's JSON answer when it sent one. */
8
+ export declare class RectaHttpError extends Error {
9
+ readonly status: number;
10
+ readonly body: unknown;
11
+ readonly name: string;
12
+ readonly reasonCode: string | undefined;
13
+ constructor(status: number, body: unknown);
14
+ }
15
+ /** A new evaluation was refused because the sender owes a fee: `offer` is the debt permit Recta offers (or null while
16
+ * a pull is pending). Pay it with `payDebt(offer)` only if you agree; this package never signs a debt by itself. */
17
+ export declare class RectaDebtError extends RectaHttpError {
18
+ readonly offer: unknown;
19
+ readonly name: string;
20
+ constructor(body: unknown, offer: unknown);
21
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ /** A local check refused to sign or send: nothing was signed or sent. `check` names the check. */
2
+ export class RectaCheckError extends Error {
3
+ check;
4
+ name = "RectaCheckError";
5
+ constructor(check, message) {
6
+ super(`${check}: ${message}`);
7
+ this.check = check;
8
+ }
9
+ }
10
+ /** Recta answered with an error. `body` is Recta's JSON answer when it sent one. */
11
+ export class RectaHttpError extends Error {
12
+ status;
13
+ body;
14
+ name = "RectaHttpError";
15
+ reasonCode;
16
+ constructor(status, body) {
17
+ const reason = typeof body === "object" && body !== null && typeof body["reason_code"] === "string" ? body["reason_code"] : undefined;
18
+ super(`Recta answered HTTP ${status}${reason === undefined ? "" : ` (${reason})`}`);
19
+ this.status = status;
20
+ this.body = body;
21
+ this.reasonCode = reason;
22
+ }
23
+ }
24
+ /** A new evaluation was refused because the sender owes a fee: `offer` is the debt permit Recta offers (or null while
25
+ * a pull is pending). Pay it with `payDebt(offer)` only if you agree; this package never signs a debt by itself. */
26
+ export class RectaDebtError extends RectaHttpError {
27
+ offer;
28
+ name = "RectaDebtError";
29
+ constructor(body, offer) {
30
+ super(402, body);
31
+ this.offer = offer;
32
+ }
33
+ }
package/dist/fee.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { RectaTypedData } from "./signer.js";
2
+ export interface FeePermitLimits {
3
+ /** The payer: the account that signs. */
4
+ readonly payer: string;
5
+ /** The most this agent agrees to authorize in one permit, in USDC atomic units (6 decimals). Required: a permit's
6
+ * amount also covers earlier fees not yet collected, which Recta's server, not the chain, bounds. */
7
+ readonly maxPermitUsdcAtomic: bigint;
8
+ readonly nowUnix: bigint;
9
+ /** The decision's commercial expiry when the caller knows it from elsewhere (the artifact): must agree. */
10
+ readonly commercialExpiryUnix?: bigint;
11
+ }
12
+ /**
13
+ * Checks a fee permit Recta offers (`fee_authorization` of an execution artifact, or a debt's `debt_offer`) against
14
+ * the pinned values and the agent's own limit, and returns the typed data to sign, rebuilt from the checked fields
15
+ * only (never the server's object as it came).
16
+ *
17
+ * Refuses unless: the domain is Permit2 on Base; the types are exactly Recta's; the spender is x402's `upto` proxy; the
18
+ * token is USDC on Base; `witness.to` is Recta's receiver; `witness.facilitator` is Recta's sender; the amount is the
19
+ * offer's cap plus what it carries (for a decision) and at most `maxPermitUsdcAtomic`; and the deadline is in the
20
+ * future and no later than the commercial expiry plus 37 days (for a decision).
21
+ */
22
+ export declare function checkFeePermit(offer: unknown, limits: FeePermitLimits, kind?: "DECISION" | "DEBT"): {
23
+ readonly typedData: RectaTypedData;
24
+ readonly amount: bigint;
25
+ readonly deadline: bigint;
26
+ };
package/dist/fee.js ADDED
@@ -0,0 +1,98 @@
1
+ import { FEE_CHAIN_ID, FEE_FACILITATOR, FEE_PERMIT_TYPES, FEE_RECEIVER, PERMIT2, PERMIT_DEADLINE_AFTER_EXPIRY_SECONDS, UPTO_PROXY, USDC_BASE } from "./constants.js";
2
+ import { RectaCheckError } from "./errors.js";
3
+ const UINT = /^(0|[1-9][0-9]{0,77})$/;
4
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
5
+ const obj = (v, check, what) => {
6
+ if (typeof v !== "object" || v === null || Array.isArray(v))
7
+ throw new RectaCheckError(check, `${what} is not an object`);
8
+ return v;
9
+ };
10
+ const uint = (v, check, what) => {
11
+ if (typeof v !== "string" || !UINT.test(v))
12
+ throw new RectaCheckError(check, `${what} is not a canonical unsigned integer`);
13
+ const n = BigInt(v);
14
+ if (n >= 2n ** 256n)
15
+ throw new RectaCheckError(check, `${what} exceeds uint256`);
16
+ return n;
17
+ };
18
+ const address = (v, check, what) => {
19
+ if (typeof v !== "string" || !ADDRESS.test(v))
20
+ throw new RectaCheckError(check, `${what} is not an address`);
21
+ return v.toLowerCase();
22
+ };
23
+ const expect = (actual, pinned, check, what) => {
24
+ if (actual !== pinned)
25
+ throw new RectaCheckError(check, `${what} is ${actual}, not ${pinned}`);
26
+ };
27
+ /**
28
+ * Checks a fee permit Recta offers (`fee_authorization` of an execution artifact, or a debt's `debt_offer`) against
29
+ * the pinned values and the agent's own limit, and returns the typed data to sign, rebuilt from the checked fields
30
+ * only (never the server's object as it came).
31
+ *
32
+ * Refuses unless: the domain is Permit2 on Base; the types are exactly Recta's; the spender is x402's `upto` proxy; the
33
+ * token is USDC on Base; `witness.to` is Recta's receiver; `witness.facilitator` is Recta's sender; the amount is the
34
+ * offer's cap plus what it carries (for a decision) and at most `maxPermitUsdcAtomic`; and the deadline is in the
35
+ * future and no later than the commercial expiry plus 37 days (for a decision).
36
+ */
37
+ export function checkFeePermit(offer, limits, kind = "DECISION") {
38
+ const o = obj(offer, "fee_offer", "the fee offer");
39
+ const td = obj(o["typed_data"], "fee_typed_data", "typed_data");
40
+ const domain = obj(td["domain"], "fee_domain", "typed_data.domain");
41
+ if (domain["name"] !== "Permit2")
42
+ throw new RectaCheckError("fee_domain", "the domain is not Permit2's");
43
+ if (domain["chainId"] !== FEE_CHAIN_ID && domain["chainId"] !== String(FEE_CHAIN_ID))
44
+ throw new RectaCheckError("fee_domain", `the chain is ${String(domain["chainId"])}, not Base (${FEE_CHAIN_ID})`);
45
+ expect(address(domain["verifyingContract"], "fee_domain", "verifyingContract"), PERMIT2, "fee_domain", "the verifying contract");
46
+ if (td["primaryType"] !== "PermitWitnessTransferFrom" || JSON.stringify(td["types"]) !== JSON.stringify(FEE_PERMIT_TYPES))
47
+ throw new RectaCheckError("fee_types", "the typed data is not exactly Recta's PermitWitnessTransferFrom");
48
+ const m = obj(td["message"], "fee_message", "typed_data.message");
49
+ const permitted = obj(m["permitted"], "fee_message", "message.permitted");
50
+ const witness = obj(m["witness"], "fee_message", "message.witness");
51
+ expect(address(m["spender"], "fee_spender", "spender"), UPTO_PROXY, "fee_spender", "the spender");
52
+ expect(address(permitted["token"], "fee_token", "token"), USDC_BASE, "fee_token", "the token");
53
+ expect(address(witness["to"], "fee_recipient", "witness.to"), FEE_RECEIVER, "fee_recipient", "the fee recipient");
54
+ expect(address(witness["facilitator"], "fee_facilitator", "witness.facilitator"), FEE_FACILITATOR, "fee_facilitator", "the facilitator");
55
+ const amount = uint(permitted["amount"], "fee_amount", "the amount");
56
+ const nonce = uint(m["nonce"], "fee_message", "the nonce");
57
+ const deadline = uint(m["deadline"], "fee_deadline", "the deadline");
58
+ const validAfter = uint(witness["validAfter"], "fee_message", "validAfter");
59
+ const permit = o["permit"] === undefined ? undefined : obj(o["permit"], "fee_offer", "permit");
60
+ if (permit?.["payer"] !== undefined && address(permit["payer"], "fee_payer", "permit.payer") !== limits.payer.toLowerCase())
61
+ throw new RectaCheckError("fee_payer", "the permit is offered to another payer");
62
+ if (kind === "DECISION") {
63
+ const cap = uint(o["max_fee_authorization_usdc_atomic"], "fee_amount", "max_fee_authorization_usdc_atomic");
64
+ const carried = uint(o["carried_usdc_atomic"], "fee_amount", "carried_usdc_atomic");
65
+ if (amount !== cap + carried)
66
+ throw new RectaCheckError("fee_amount", `the permit amount ${amount} is not the cap ${cap} plus carried ${carried}`);
67
+ const expiry = uint(o["commercial_expiry_unix"], "fee_deadline", "commercial_expiry_unix");
68
+ if (limits.commercialExpiryUnix !== undefined && expiry !== limits.commercialExpiryUnix)
69
+ throw new RectaCheckError("fee_deadline", "the offer's commercial expiry is not the artifact's");
70
+ if (deadline > expiry + PERMIT_DEADLINE_AFTER_EXPIRY_SECONDS)
71
+ throw new RectaCheckError("fee_deadline", "the deadline is later than the commercial expiry plus 37 days");
72
+ }
73
+ else {
74
+ const owed = uint(o["amount_usdc_atomic"], "fee_amount", "amount_usdc_atomic");
75
+ if (amount !== owed)
76
+ throw new RectaCheckError("fee_amount", `the permit amount ${amount} is not the debt ${owed}`);
77
+ }
78
+ if (amount > limits.maxPermitUsdcAtomic)
79
+ throw new RectaCheckError("fee_amount_limit", `the permit amount ${amount} exceeds your limit ${limits.maxPermitUsdcAtomic}`);
80
+ if (deadline <= limits.nowUnix)
81
+ throw new RectaCheckError("fee_deadline", "the deadline has passed");
82
+ return {
83
+ amount,
84
+ deadline,
85
+ typedData: {
86
+ domain: { name: "Permit2", chainId: FEE_CHAIN_ID, verifyingContract: PERMIT2 },
87
+ types: FEE_PERMIT_TYPES,
88
+ primaryType: "PermitWitnessTransferFrom",
89
+ message: {
90
+ permitted: { token: USDC_BASE, amount },
91
+ spender: UPTO_PROXY,
92
+ nonce,
93
+ deadline,
94
+ witness: { to: FEE_RECEIVER, facilitator: FEE_FACILITATOR, validAfter },
95
+ },
96
+ },
97
+ };
98
+ }
@@ -0,0 +1,8 @@
1
+ export { createRectaClient, signedRequestMessage, type RectaClient, type RectaClientOptions, type FeeOutcome } from "./client.js";
2
+ export { checkFeePermit, type FeePermitLimits } from "./fee.js";
3
+ export { checkExecutionArtifact, pinnedRouteOf, type CheckedArtifact, type EvaluationRequestView, type ArtifactLimits } from "./artifact.js";
4
+ export { PINNED_ROUTES, type PinnedRoute } from "./pinned-routes.js";
5
+ export { parseWalletPolicy, type WalletPolicy, type WalletPolicyNetwork } from "./policy.js";
6
+ export { viemSigner, viemChainReader, cdpSigner, CDP_SEND_NETWORKS, type CdpEvmAccountLike, type CdpSendNetwork, type RectaSigner, type RectaChainReader, type RectaTransaction, type RectaTypedData } from "./signer.js";
7
+ export { RectaCheckError, RectaHttpError, RectaDebtError } from "./errors.js";
8
+ export * as pins from "./constants.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { createRectaClient, signedRequestMessage } from "./client.js";
2
+ export { checkFeePermit } from "./fee.js";
3
+ export { checkExecutionArtifact, pinnedRouteOf } from "./artifact.js";
4
+ export { PINNED_ROUTES } from "./pinned-routes.js";
5
+ export { parseWalletPolicy } from "./policy.js";
6
+ export { viemSigner, viemChainReader, cdpSigner, CDP_SEND_NETWORKS } from "./signer.js";
7
+ export { RectaCheckError, RectaHttpError, RectaDebtError } from "./errors.js";
8
+ export * as pins from "./constants.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * GENERATED by scripts/generate-client-pinned-routes.ts from Recta's server configuration; do not edit by hand.
3
+ * Every EVM-to-EVM commercial corridor: source and output tokens, and the only contracts a route may approve and call on
4
+ * the source network, with the destination identifier each call must name (Across chain id, CCTP V2 domain, LayerZero
5
+ * endpoint id). A served wallet policy never adds to this list: a contract missing here is refused.
6
+ */
7
+ export interface PinnedRoute {
8
+ readonly source_chain_id: number;
9
+ readonly source_token: string;
10
+ readonly source_decimals: number;
11
+ readonly output_token: string;
12
+ readonly output_decimals: number;
13
+ readonly contracts: readonly {
14
+ readonly protocol: "across" | "cctp_v2" | "oft";
15
+ readonly address: string;
16
+ readonly destination_id: number;
17
+ }[];
18
+ }
19
+ export declare const PINNED_ROUTES: Readonly<Record<string, PinnedRoute>>;