@recta/client 0.1.1 → 0.1.4
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/README.md +58 -3
- package/dist/client.d.ts +18 -0
- package/dist/client.js +81 -12
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/funds.d.ts +88 -0
- package/dist/funds.js +77 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/signer.d.ts +9 -2
- package/dist/signer.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ It never receives a key. Before it signs or sends anything it checks it locally,
|
|
|
18
18
|
npm install @recta/client viem
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`viem` (>= 2.
|
|
21
|
+
`viem` (>= 2.38.3, < 3) is the only dependency, and you choose its version.
|
|
22
22
|
|
|
23
23
|
## Use
|
|
24
24
|
|
|
@@ -33,9 +33,12 @@ const recta = createRectaClient({
|
|
|
33
33
|
maxNativeValueWei: 10n ** 15n, // never attach more than 0.001 native to one route transaction
|
|
34
34
|
minOutputBps: 9900, // required, 1–10000: the route must guarantee the recipient at least 99 % of the amount sent
|
|
35
35
|
},
|
|
36
|
+
referrer: "0x…", // optional: the integrator that ships this agent (see "Referral for integrators")
|
|
36
37
|
});
|
|
37
38
|
|
|
38
|
-
// Dry run: evaluates and checks, signs no permit, sends no transaction.
|
|
39
|
+
// Dry run: evaluates and checks, signs no permit, sends no transaction. With Recta's collection on it stops at the
|
|
40
|
+
// fee permit, before the transactions exist (Recta hands them out only after the permit is signed): `preview.fee`
|
|
41
|
+
// carries the permit it would sign and `funds`, whether this wallet can pay it (see "Paying the fee").
|
|
39
42
|
const preview = await recta.run(request, { dryRun: true });
|
|
40
43
|
|
|
41
44
|
// The whole flow.
|
|
@@ -45,10 +48,22 @@ const result = await recta.run(request);
|
|
|
45
48
|
`request` is Recta's evaluation request (schema in the
|
|
46
49
|
[service descriptor](https://api.recta.build/v1/golden-corridor/commercial-evaluations/schema)); its `sender` must be
|
|
47
50
|
your signer's address. Each step is also available alone: `evaluate`, `authorizeFee`, `check`, `execute`, `report`,
|
|
48
|
-
`ledger`, `payDebt`.
|
|
51
|
+
`ledger`, `payDebt`, `feeFunds`, `feeApproval`.
|
|
49
52
|
|
|
50
53
|
Your account must be an EOA: Recta's signed requests are EIP-191 signatures recovered to the sender's address.
|
|
51
54
|
|
|
55
|
+
## Referral for integrators
|
|
56
|
+
|
|
57
|
+
If you ship Recta inside a plugin, framework or agent template, set your own EVM address once as the `referrer` and
|
|
58
|
+
every agent that runs your integration carries it. Recta fixes it at the agent's first signed evaluation and never
|
|
59
|
+
changes it afterwards. You earn 20 % of the fees Recta actually collects from those agents (confirmed on chain) for
|
|
60
|
+
12 months, up to 200 USDC per agent, whether or not you pay fees yourself. One level only. The agent still pays exactly
|
|
61
|
+
5 % of its verified saving: your share comes out of Recta's 5 %, never on top of it.
|
|
62
|
+
|
|
63
|
+
The referrer travels in the query of the agent's own signed requests, so nobody relaying them can add, drop or change
|
|
64
|
+
it. It is ignored when it is the agent's own address. Terms and your balance: `GET /v1/referrals/balance`, signed by
|
|
65
|
+
your address, once Recta's referral program is on (it starts together with fee collection).
|
|
66
|
+
|
|
52
67
|
### Coinbase CDP accounts
|
|
53
68
|
|
|
54
69
|
A [Coinbase CDP](https://docs.cdp.coinbase.com) EVM server account (an EOA whose key stays in CDP) works through
|
|
@@ -82,6 +97,42 @@ really uses. If your agent bridges through LI.FI (its public API or an aggregato
|
|
|
82
97
|
wherever the fit check (`GET /v1/golden-corridor/fit`) lists it in `declarable_baselines`; it is refused with
|
|
83
98
|
`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
99
|
|
|
100
|
+
## Paying the fee: USDC and the Permit2 allowance on Base
|
|
101
|
+
|
|
102
|
+
Recta's fee is collected in USDC on Base through Permit2. Signing the permit is not enough: when Recta settles it,
|
|
103
|
+
Permit2 pulls the fee with USDC's own `transferFrom`, so the wallet also needs, on Base, (1) at least the permit's amount
|
|
104
|
+
in USDC and (2) an ERC-20 allowance from USDC to Permit2 (`0x000000000022D473030F116dDEE9F6B43aC78BA3`) of at least that
|
|
105
|
+
amount. A new wallet has neither.
|
|
106
|
+
|
|
107
|
+
Before it signs a fee or debt permit, the client reads both through `chain.readFeeFunds` (on Base: `viemChainReader`
|
|
108
|
+
uses `publicClients[8453]`) and refuses, with `RectaCheckError` `fee_balance` or `fee_allowance`, when either is short.
|
|
109
|
+
Nothing is signed and no route transaction is sent, so a route never executes with a fee Recta cannot collect.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const funds = await recta.feeFunds(); // { ready, balanceUsdcAtomic, permit2AllowanceUsdcAtomic, missing }
|
|
113
|
+
if (funds.missing.includes("allowance")) {
|
|
114
|
+
const approval = recta.feeApproval(); // USDC.approve(Permit2, maxPermitUsdcAtomic) on Base, for you to send
|
|
115
|
+
// send `approval` (chainId, to, data, value) with your own wallet
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The approval is bounded: `feeApproval` defaults to, and may not exceed, `maxPermitUsdcAtomic`, and
|
|
120
|
+
`permit2ApprovalTransaction` refuses 0 and any "unlimited" amount. The client never approves by itself. Each collected
|
|
121
|
+
fee lowers the allowance; approve again when `feeFunds` reports it short. The check is a snapshot at signing: a wallet
|
|
122
|
+
that later spends its USDC or revokes the allowance leaves the fee unpaid, which Recta records as a debt to settle
|
|
123
|
+
before the next evaluation (`payDebt`).
|
|
124
|
+
|
|
125
|
+
When the route itself starts from USDC on Base, the transfer spends the same balance before the fee is pulled, so the
|
|
126
|
+
balance must cover the transfer plus the fee: `run` passes the request to `authorizeFee` for that (pass it yourself as
|
|
127
|
+
`authorizeFee(id, { request })` when calling the steps alone), and `execute` checks the funds again, for every fee
|
|
128
|
+
this client authorized or found authorized, right before the first transaction. A reader error never carries the RPC
|
|
129
|
+
URL (it may hold an API key).
|
|
130
|
+
|
|
131
|
+
Since 0.1.4 `RectaChainReader` requires `readFeeFunds`; `createRectaClient` refuses a reader without it.
|
|
132
|
+
|
|
133
|
+
A custom `RectaChainReader` must implement `readFeeFunds(owner)`; when it cannot read Base it must throw, and the permit
|
|
134
|
+
is then not signed (a dry run reports `funds: null` and the reason).
|
|
135
|
+
|
|
85
136
|
## What is checked before signing
|
|
86
137
|
|
|
87
138
|
**Pins.** The package carries Recta's fee addresses in its own code: Permit2, x402's `upto` proxy, USDC on Base,
|
|
@@ -119,6 +170,10 @@ the declared messaging fee, within `maxNativeValueWei`.
|
|
|
119
170
|
`RectaDebtError` with the offer. The package never signs a debt by itself: call `payDebt(offer)` if you agree (same
|
|
120
171
|
checks, exact amount).
|
|
121
172
|
|
|
173
|
+
## What each request identifies
|
|
174
|
+
|
|
175
|
+
Every request this package sends carries the User-Agent `recta-client/<version>`, so Recta can count real use of the package apart from other clients. It names the package and its version only: never an address, a key or a request body.
|
|
176
|
+
|
|
122
177
|
## Limits of this version
|
|
123
178
|
|
|
124
179
|
- EVM sources and EVM recipients only. Solana, Tron and Bitcoin routes, Relay and deBridge are refused.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Hex } from "viem";
|
|
2
2
|
import { type CheckedArtifact, type EvaluationRequestView } from "./artifact.js";
|
|
3
|
+
import { type FeeReadiness, type Permit2Approval } from "./funds.js";
|
|
3
4
|
import type { RectaChainReader, RectaSigner, RectaTypedData } from "./signer.js";
|
|
4
5
|
export interface RectaClientOptions {
|
|
5
6
|
readonly signer: RectaSigner;
|
|
@@ -12,6 +13,14 @@ export interface RectaClientOptions {
|
|
|
12
13
|
/** The least the recipient must be guaranteed, in basis points of the amount sent (e.g. 9950 = 99.5 %). */
|
|
13
14
|
readonly minOutputBps: number;
|
|
14
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Who referred this agent to Recta (OD-REF-1): an EVM address, usually the author of the plugin, framework or template
|
|
18
|
+
* that ships this client. Sent as `?referrer=` on the signed evaluation and fee-authorization requests, so it is part
|
|
19
|
+
* of what the agent's own key signs. Recta fixes it once, at the agent's first signed evaluation, and never changes it;
|
|
20
|
+
* the agent still pays exactly 5 % of its verified saving (the referrer's share comes out of Recta's 5 %). Ignored
|
|
21
|
+
* when it is the signer's own address (nobody can refer itself).
|
|
22
|
+
*/
|
|
23
|
+
readonly referrer?: string;
|
|
15
24
|
/** Defaults to the global fetch. */
|
|
16
25
|
readonly fetch?: typeof fetch;
|
|
17
26
|
/** Milliseconds since the epoch; defaults to Date.now. */
|
|
@@ -25,6 +34,8 @@ export declare function signedRequestMessage(f: {
|
|
|
25
34
|
readonly bodyKeccak256: string;
|
|
26
35
|
readonly issuedAt: number;
|
|
27
36
|
}): string;
|
|
37
|
+
/** The `?referrer=` suffix for a configured referrer: lower-cased, empty when none or when it is the payer itself. */
|
|
38
|
+
export declare function referrerQuery(referrer: string | undefined, payer: string): string;
|
|
28
39
|
export type FeeOutcome = {
|
|
29
40
|
readonly kind: "NOT_REQUIRED";
|
|
30
41
|
} | {
|
|
@@ -37,6 +48,8 @@ export type FeeOutcome = {
|
|
|
37
48
|
readonly kind: "DRY_RUN";
|
|
38
49
|
readonly amountUsdcAtomic: bigint;
|
|
39
50
|
readonly typedData: RectaTypedData;
|
|
51
|
+
readonly funds: FeeReadiness | null;
|
|
52
|
+
readonly fundsError?: string;
|
|
40
53
|
};
|
|
41
54
|
/**
|
|
42
55
|
* Recta's flow for an agent that holds its own EOA: evaluate → authorizeFee → execute → report. Every step can be
|
|
@@ -49,6 +62,7 @@ export declare function createRectaClient(options: RectaClientOptions): {
|
|
|
49
62
|
}) => Promise<Record<string, unknown>>;
|
|
50
63
|
authorizeFee: (resultId: string, opts?: {
|
|
51
64
|
readonly dryRun?: boolean;
|
|
65
|
+
readonly request?: EvaluationRequestView;
|
|
52
66
|
}) => Promise<{
|
|
53
67
|
readonly fee: FeeOutcome;
|
|
54
68
|
readonly artifact: unknown;
|
|
@@ -79,6 +93,8 @@ export declare function createRectaClient(options: RectaClientOptions): {
|
|
|
79
93
|
readonly kind: "DRY_RUN";
|
|
80
94
|
readonly amountUsdcAtomic: bigint;
|
|
81
95
|
readonly typedData: RectaTypedData;
|
|
96
|
+
readonly funds: FeeReadiness | null;
|
|
97
|
+
readonly fundsError?: string;
|
|
82
98
|
};
|
|
83
99
|
checked?: never;
|
|
84
100
|
} | {
|
|
@@ -115,5 +131,7 @@ export declare function createRectaClient(options: RectaClientOptions): {
|
|
|
115
131
|
checked?: never;
|
|
116
132
|
}>;
|
|
117
133
|
walletPolicy: () => Promise<void>;
|
|
134
|
+
feeFunds: (requiredUsdcAtomic?: bigint) => Promise<FeeReadiness>;
|
|
135
|
+
feeApproval: (amountUsdcAtomic?: bigint) => Permit2Approval;
|
|
118
136
|
};
|
|
119
137
|
export type RectaClient = ReturnType<typeof createRectaClient>;
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { keccak256 } from "viem";
|
|
2
2
|
import { checkExecutionArtifact } from "./artifact.js";
|
|
3
|
-
import { ISSUED_AT_HEADER, PATHS, RECTA_ORIGIN, SIGNATURE_HEADER, SIGNED_REQUEST_PREFIX, SIGNER_HEADER } from "./constants.js";
|
|
3
|
+
import { ISSUED_AT_HEADER, PATHS, RECTA_ORIGIN, SIGNATURE_HEADER, SIGNED_REQUEST_PREFIX, SIGNER_HEADER, USER_AGENT } from "./constants.js";
|
|
4
4
|
import { RectaCheckError, RectaDebtError, RectaHttpError } from "./errors.js";
|
|
5
5
|
import { checkFeePermit } from "./fee.js";
|
|
6
|
+
import { assertFeeReady, feeReadiness, permit2ApprovalTransaction, routeReserveOf, safeErrorText } from "./funds.js";
|
|
6
7
|
import { parseWalletPolicy } from "./policy.js";
|
|
7
8
|
/** The text a sender signs for one request (EIP-191), exactly as Recta's server rebuilds it. */
|
|
8
9
|
export function signedRequestMessage(f) {
|
|
@@ -16,6 +17,16 @@ export function signedRequestMessage(f) {
|
|
|
16
17
|
].join("\n");
|
|
17
18
|
}
|
|
18
19
|
const RESULT_ID = /^[A-Za-z0-9_-]+$/;
|
|
20
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
21
|
+
/** The `?referrer=` suffix for a configured referrer: lower-cased, empty when none or when it is the payer itself. */
|
|
22
|
+
export function referrerQuery(referrer, payer) {
|
|
23
|
+
if (referrer === undefined)
|
|
24
|
+
return "";
|
|
25
|
+
if (!ADDRESS.test(referrer))
|
|
26
|
+
throw new RectaCheckError("referrer", "the referrer must be a 0x-prefixed 20-byte EVM address");
|
|
27
|
+
const r = referrer.toLowerCase();
|
|
28
|
+
return r === payer.toLowerCase() ? "" : `?referrer=${r}`;
|
|
29
|
+
}
|
|
19
30
|
const field = (v, k) => (typeof v === "object" && v !== null ? v[k] : undefined);
|
|
20
31
|
const resultIdOf = (v) => {
|
|
21
32
|
if (!RESULT_ID.test(v))
|
|
@@ -33,10 +44,19 @@ export function createRectaClient(options) {
|
|
|
33
44
|
const now = options.now ?? Date.now;
|
|
34
45
|
const nowUnix = () => BigInt(Math.floor(now() / 1000));
|
|
35
46
|
const payer = signer.address.toLowerCase();
|
|
47
|
+
const referral = referrerQuery(options.referrer, payer);
|
|
48
|
+
if (typeof chain.readFeeFunds !== "function") {
|
|
49
|
+
// Since 0.1.4 (ASTRA T2) a reader must read the fee funds on Base; an older custom reader fails here, clearly.
|
|
50
|
+
throw new RectaCheckError("chain_reader", "the chain reader has no readFeeFunds(owner): since @recta/client 0.1.4 it must read the payer's USDC balance and Permit2 allowance on Base (viemChainReader does)");
|
|
51
|
+
}
|
|
52
|
+
/** The fee permit amount of each decision this client saw authorized: execute() checks the funds against it again
|
|
53
|
+
* right before the first transaction (Fable B-1/M-1 on recta #12). */
|
|
54
|
+
const feeAmounts = new Map();
|
|
55
|
+
const parseAtomic = (v) => (typeof v === "string" && /^(0|[1-9][0-9]{0,30})$/.test(v) ? BigInt(v) : undefined);
|
|
36
56
|
let policyChecked = false;
|
|
37
57
|
async function call(method, path, body, idempotencyKey, signed = true) {
|
|
38
58
|
const bytes = body === undefined ? new Uint8Array() : new TextEncoder().encode(JSON.stringify(body));
|
|
39
|
-
const headers = { accept: "application/json" };
|
|
59
|
+
const headers = { accept: "application/json", "user-agent": USER_AGENT };
|
|
40
60
|
if (body !== undefined)
|
|
41
61
|
headers["content-type"] = "application/json";
|
|
42
62
|
if (idempotencyKey !== undefined)
|
|
@@ -72,7 +92,7 @@ export function createRectaClient(options) {
|
|
|
72
92
|
if (typeof request.sender !== "string" || request.sender.toLowerCase() !== payer)
|
|
73
93
|
throw new RectaCheckError("sender", "the request's sender must be the signer");
|
|
74
94
|
await walletPolicy();
|
|
75
|
-
const a = await call("POST", PATHS.evaluations
|
|
95
|
+
const a = await call("POST", `${PATHS.evaluations}${referral}`, request, opts.idempotencyKey ?? crypto.randomUUID());
|
|
76
96
|
if (a.status === 402)
|
|
77
97
|
throw new RectaDebtError(a.body, field(a.body, "debt_offer") ?? null);
|
|
78
98
|
if (a.status !== 200)
|
|
@@ -81,17 +101,32 @@ export function createRectaClient(options) {
|
|
|
81
101
|
}
|
|
82
102
|
/**
|
|
83
103
|
* 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
|
|
85
|
-
*
|
|
104
|
+
* permit: it is checked, rebuilt, and signed only if it passes and the payer holds the permit's amount in USDC on
|
|
105
|
+
* Base with at least that allowance to Permit2 (otherwise RectaCheckError `fee_balance` / `fee_allowance`, nothing
|
|
106
|
+
* signed; see `feeApproval`); then it is stored and the answer carries the artifact. With `dryRun` and collection on,
|
|
107
|
+
* it stops at the permit, before the artifact exists, and returns the typed data it would sign and the payer's funds:
|
|
108
|
+
* the transactions can only be inspected after the permit is signed. Pass the evaluation's `request` so a route that
|
|
109
|
+
* starts from USDC on Base reserves its own amount from the balance; `execute` checks the funds again either way.
|
|
86
110
|
*/
|
|
87
111
|
async function authorizeFee(resultId, opts = {}) {
|
|
88
112
|
const id = resultIdOf(resultId);
|
|
89
113
|
const artifactPath = `${PATHS.evaluations}/${id}/execution-artifact`;
|
|
114
|
+
const reserve = opts.request === undefined ? 0n : routeReserveOf(opts.request);
|
|
90
115
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
91
116
|
const a = await call("GET", artifactPath);
|
|
92
117
|
if (a.status === 200) {
|
|
93
|
-
const
|
|
94
|
-
|
|
118
|
+
const fa = field(a.body, "fee_authorization");
|
|
119
|
+
if (field(fa, "status") === "AUTHORIZED") {
|
|
120
|
+
// A permit stored earlier: its amount is the stored cap plus what it carries; the funds are checked again
|
|
121
|
+
// before execute() sends anything.
|
|
122
|
+
const cap = parseAtomic(field(fa, "max_fee_authorization_usdc_atomic"));
|
|
123
|
+
const carried = parseAtomic(field(fa, "carried_usdc_atomic"));
|
|
124
|
+
if (cap === undefined || carried === undefined)
|
|
125
|
+
throw new RectaCheckError("fee_amount", "the stored fee authorization has no readable amount");
|
|
126
|
+
feeAmounts.set(id, cap + carried);
|
|
127
|
+
return { fee: { kind: "ALREADY_AUTHORIZED" }, artifact: field(a.body, "artifact") };
|
|
128
|
+
}
|
|
129
|
+
return { fee: { kind: "NOT_REQUIRED" }, artifact: field(a.body, "artifact") };
|
|
95
130
|
}
|
|
96
131
|
if (a.status !== 402 || field(a.body, "status") !== "SIGNATURE_REQUIRED")
|
|
97
132
|
throw new RectaHttpError(a.status, a.body);
|
|
@@ -102,10 +137,24 @@ export function createRectaClient(options) {
|
|
|
102
137
|
nowUnix: nowUnix(),
|
|
103
138
|
...(typeof expiry === "string" && /^[0-9]+$/.test(expiry) ? { commercialExpiryUnix: BigInt(expiry) } : {}),
|
|
104
139
|
});
|
|
105
|
-
if (opts.dryRun === true)
|
|
106
|
-
|
|
140
|
+
if (opts.dryRun === true) {
|
|
141
|
+
// A preview reports the funds; a reader that cannot read Base yields `funds: null` and the reason, never a signature.
|
|
142
|
+
let funds = null;
|
|
143
|
+
let fundsError;
|
|
144
|
+
try {
|
|
145
|
+
funds = feeReadiness(await chain.readFeeFunds(signer.address), checked.amount, reserve);
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
fundsError = safeErrorText(e);
|
|
149
|
+
}
|
|
150
|
+
return { fee: { kind: "DRY_RUN", amountUsdcAtomic: checked.amount, typedData: checked.typedData, funds, ...(fundsError === undefined ? {} : { fundsError }) }, artifact: undefined };
|
|
151
|
+
}
|
|
152
|
+
// ASTRA T2: never sign a fee Recta could not collect (no USDC or no Permit2 allowance on Base).
|
|
153
|
+
assertFeeReady(feeReadiness(await chain.readFeeFunds(signer.address), checked.amount, reserve));
|
|
107
154
|
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() });
|
|
155
|
+
const s = await call("POST", `${PATHS.evaluations}/${id}/fee-authorization${referral}`, { signature, amount_usdc_atomic: checked.amount.toString() });
|
|
156
|
+
if (s.status === 200 || s.status === 201)
|
|
157
|
+
feeAmounts.set(id, checked.amount);
|
|
109
158
|
if (s.status === 200 || s.status === 201)
|
|
110
159
|
return { fee: { kind: "AUTHORIZED", amountUsdcAtomic: checked.amount, signature }, artifact: field(s.body, "artifact") };
|
|
111
160
|
if (!(s.status === 409 && field(s.body, "reason_code") === "OFFER_STALE"))
|
|
@@ -120,6 +169,11 @@ export function createRectaClient(options) {
|
|
|
120
169
|
}
|
|
121
170
|
/** Step 3: checks, then sends the transactions in order, each mined successfully before the next. */
|
|
122
171
|
async function execute(resultId, artifact, request) {
|
|
172
|
+
// The last moment before anything is sent: with a fee authorized for this decision, the payer must still hold it
|
|
173
|
+
// in USDC on Base (plus the transfer itself when it starts from Base USDC) with the Permit2 allowance for it.
|
|
174
|
+
const fee = feeAmounts.get(resultIdOf(resultId));
|
|
175
|
+
if (fee !== undefined)
|
|
176
|
+
assertFeeReady(feeReadiness(await chain.readFeeFunds(signer.address), fee, routeReserveOf(request)));
|
|
123
177
|
const checked = await check(resultId, artifact, request);
|
|
124
178
|
let approvalTxHash;
|
|
125
179
|
for (const tx of checked.transactions) {
|
|
@@ -163,6 +217,7 @@ export function createRectaClient(options) {
|
|
|
163
217
|
const debtId = field(offer, "debt_id");
|
|
164
218
|
if (typeof debtId !== "string")
|
|
165
219
|
throw new RectaCheckError("debt", "the offer has no debt_id");
|
|
220
|
+
assertFeeReady(feeReadiness(await chain.readFeeFunds(signer.address), checked.amount));
|
|
166
221
|
const signature = await signer.signTypedData(checked.typedData);
|
|
167
222
|
const a = await call("POST", PATHS.feeDebts, { debt_id: debtId, deadline_unix: checked.deadline.toString(), signature });
|
|
168
223
|
if (a.status !== 200 && a.status !== 201)
|
|
@@ -178,7 +233,7 @@ export function createRectaClient(options) {
|
|
|
178
233
|
const resultId = field(decision, "result_id");
|
|
179
234
|
if (field(field(decision, "decision"), "state") !== "VERIFIED_POSITIVE" || typeof resultId !== "string")
|
|
180
235
|
return { stage: "NO_ROUTE", decision };
|
|
181
|
-
const { fee, artifact } = await authorizeFee(resultId, opts.dryRun === true ? { dryRun: true } : {});
|
|
236
|
+
const { fee, artifact } = await authorizeFee(resultId, opts.dryRun === true ? { dryRun: true, request } : { request });
|
|
182
237
|
if (fee.kind === "DRY_RUN")
|
|
183
238
|
return { stage: "DRY_RUN", decision, fee };
|
|
184
239
|
if (opts.dryRun === true)
|
|
@@ -186,5 +241,19 @@ export function createRectaClient(options) {
|
|
|
186
241
|
const sent = await execute(resultId, artifact, request);
|
|
187
242
|
return { stage: "REPORTED", decision, fee, ...sent, report: await report(resultId, sent.originTxHash, sent.approvalTxHash) };
|
|
188
243
|
}
|
|
189
|
-
|
|
244
|
+
/** The payer's USDC balance and Permit2 allowance on Base against an amount (default: this client's permit limit). */
|
|
245
|
+
async function feeFunds(requiredUsdcAtomic = limits.maxPermitUsdcAtomic) {
|
|
246
|
+
return feeReadiness(await chain.readFeeFunds(signer.address), requiredUsdcAtomic);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The bounded approval `USDC.approve(Permit2, amount)` on Base, for the operator to send (`sendTransaction` is not
|
|
250
|
+
* called here: this client never approves by itself). The amount defaults to, and may not exceed, this client's
|
|
251
|
+
* permit limit, so the allowance never reaches past what one permit may authorize.
|
|
252
|
+
*/
|
|
253
|
+
function feeApproval(amountUsdcAtomic = limits.maxPermitUsdcAtomic) {
|
|
254
|
+
if (amountUsdcAtomic > limits.maxPermitUsdcAtomic)
|
|
255
|
+
throw new RectaCheckError("fee_approval", `the approval ${amountUsdcAtomic} exceeds your permit limit ${limits.maxPermitUsdcAtomic}`);
|
|
256
|
+
return permit2ApprovalTransaction(amountUsdcAtomic);
|
|
257
|
+
}
|
|
258
|
+
return { evaluate, authorizeFee, check, execute, report, ledger, payDebt, run, walletPolicy, feeFunds, feeApproval };
|
|
190
259
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* Every value is checked equal to the Recta server's own configuration by a test in the Recta repository.
|
|
7
7
|
*/
|
|
8
8
|
export declare const RECTA_ORIGIN = "https://api.recta.build";
|
|
9
|
+
/** This package's version (equal to package.json, checked in CI). */
|
|
10
|
+
export declare const CLIENT_VERSION = "0.1.4";
|
|
11
|
+
/** Sent on every request so Recta can count real use of this package apart from other clients (no address, no key). */
|
|
12
|
+
export declare const USER_AGENT = "recta-client/0.1.4";
|
|
9
13
|
/** The fee: x402 `upto` on Base, a Permit2 `permitWitnessTransferFrom`. All lowercase. */
|
|
10
14
|
export declare const FEE_CHAIN_ID = 8453;
|
|
11
15
|
export declare const PERMIT2 = "0x000000000022d473030f116ddee9f6b43ac78ba3";
|
package/dist/constants.js
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* Every value is checked equal to the Recta server's own configuration by a test in the Recta repository.
|
|
7
7
|
*/
|
|
8
8
|
export const RECTA_ORIGIN = "https://api.recta.build";
|
|
9
|
+
/** This package's version (equal to package.json, checked in CI). */
|
|
10
|
+
export const CLIENT_VERSION = "0.1.4";
|
|
11
|
+
/** Sent on every request so Recta can count real use of this package apart from other clients (no address, no key). */
|
|
12
|
+
export const USER_AGENT = `recta-client/${CLIENT_VERSION}`;
|
|
9
13
|
/** The fee: x402 `upto` on Base, a Permit2 `permitWitnessTransferFrom`. All lowercase. */
|
|
10
14
|
export const FEE_CHAIN_ID = 8453;
|
|
11
15
|
export const PERMIT2 = "0x000000000022d473030f116ddee9f6b43ac78ba3";
|
package/dist/funds.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type Hex } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* Whether a fee permit can actually be collected (ASTRA T2). Signing a Permit2 permit is not enough: when Recta settles
|
|
4
|
+
* it, Permit2 pulls the fee with USDC's own `transferFrom`, which needs (1) the payer's USDC balance on Base and (2) the
|
|
5
|
+
* payer's ERC-20 allowance from USDC to Permit2. A permit signed without them lets the route execute and then the fee
|
|
6
|
+
* fail, which leaves a debt that blocks the next evaluation. This package therefore reads both before it signs a fee or
|
|
7
|
+
* debt permit and refuses when either is short. It never approves anything by itself.
|
|
8
|
+
*/
|
|
9
|
+
/** What the payer holds on Base, read at signing time (both in USDC atomic units, 6 decimals). */
|
|
10
|
+
export interface FeeFunds {
|
|
11
|
+
readonly balanceUsdcAtomic: bigint;
|
|
12
|
+
readonly permit2AllowanceUsdcAtomic: bigint;
|
|
13
|
+
}
|
|
14
|
+
export interface FeeReadiness {
|
|
15
|
+
readonly ready: boolean;
|
|
16
|
+
/** The permit's amount: the most this fee can pull. */
|
|
17
|
+
readonly requiredUsdcAtomic: bigint;
|
|
18
|
+
/** USDC on Base the route itself will spend before the fee is pulled (a route that starts from Base USDC); the
|
|
19
|
+
* balance must cover it plus the fee (Fable B-1 on recta #12). */
|
|
20
|
+
readonly reservedForRouteUsdcAtomic: bigint;
|
|
21
|
+
readonly balanceUsdcAtomic: bigint;
|
|
22
|
+
readonly permit2AllowanceUsdcAtomic: bigint;
|
|
23
|
+
/** What is short, if anything: `balance` (hold more USDC on Base), `allowance` (approve Permit2 for at least the amount). */
|
|
24
|
+
readonly missing: readonly ("balance" | "allowance")[];
|
|
25
|
+
}
|
|
26
|
+
export declare const ERC20_READ_ABI: readonly [{
|
|
27
|
+
readonly type: "function";
|
|
28
|
+
readonly name: "balanceOf";
|
|
29
|
+
readonly stateMutability: "view";
|
|
30
|
+
readonly inputs: readonly [{
|
|
31
|
+
readonly name: "owner";
|
|
32
|
+
readonly type: "address";
|
|
33
|
+
}];
|
|
34
|
+
readonly outputs: readonly [{
|
|
35
|
+
readonly name: "";
|
|
36
|
+
readonly type: "uint256";
|
|
37
|
+
}];
|
|
38
|
+
}, {
|
|
39
|
+
readonly type: "function";
|
|
40
|
+
readonly name: "allowance";
|
|
41
|
+
readonly stateMutability: "view";
|
|
42
|
+
readonly inputs: readonly [{
|
|
43
|
+
readonly name: "owner";
|
|
44
|
+
readonly type: "address";
|
|
45
|
+
}, {
|
|
46
|
+
readonly name: "spender";
|
|
47
|
+
readonly type: "address";
|
|
48
|
+
}];
|
|
49
|
+
readonly outputs: readonly [{
|
|
50
|
+
readonly name: "";
|
|
51
|
+
readonly type: "uint256";
|
|
52
|
+
}];
|
|
53
|
+
}];
|
|
54
|
+
/**
|
|
55
|
+
* The fee's amount against the payer's funds. `reservedForRouteUsdcAtomic`: USDC on Base the route will spend first
|
|
56
|
+
* (see `routeReserveOf`); the balance must cover it plus the fee. The allowance only needs to cover the fee: a route's
|
|
57
|
+
* own approval goes to the bridge, not to Permit2.
|
|
58
|
+
*/
|
|
59
|
+
export declare function feeReadiness(funds: FeeFunds, requiredUsdcAtomic: bigint, reservedForRouteUsdcAtomic?: bigint): FeeReadiness;
|
|
60
|
+
/**
|
|
61
|
+
* USDC on Base that a route spends before Recta pulls its fee: the request's amount when the route starts from USDC on
|
|
62
|
+
* Base (the same balance pays both), otherwise 0. Throws when such an amount is not a plain decimal: fail closed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function routeReserveOf(request: {
|
|
65
|
+
readonly source_network: string;
|
|
66
|
+
readonly source_asset: string;
|
|
67
|
+
readonly amount: string;
|
|
68
|
+
}): bigint;
|
|
69
|
+
/** A read error's text without what an RPC URL may carry (an API key in the path or query): viem's short message,
|
|
70
|
+
* and any URL replaced (Fable M-2 on recta #12). */
|
|
71
|
+
export declare function safeErrorText(e: unknown): string;
|
|
72
|
+
/** Throws a RectaCheckError naming what is short and how to fix it; nothing has been signed when it throws. */
|
|
73
|
+
export declare function assertFeeReady(r: FeeReadiness): void;
|
|
74
|
+
/** The one transaction that lets Permit2 pull up to `amountUsdcAtomic` of USDC on Base: `USDC.approve(Permit2, amount)`. */
|
|
75
|
+
export interface Permit2Approval {
|
|
76
|
+
readonly chainId: number;
|
|
77
|
+
readonly to: Hex;
|
|
78
|
+
readonly data: Hex;
|
|
79
|
+
readonly value: bigint;
|
|
80
|
+
readonly amountUsdcAtomic: bigint;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds the exact approval for the agent's operator to send with their own wallet. Bounded on purpose: it refuses 0
|
|
84
|
+
* and anything of 2^128 or more, so an "unlimited" approval (2^256 − 1, 2^160 − 1) cannot be built here. Approving
|
|
85
|
+
* replaces, not adds to, the current allowance; each collected fee lowers it, so approve again when a check reports
|
|
86
|
+
* `allowance` short.
|
|
87
|
+
*/
|
|
88
|
+
export declare function permit2ApprovalTransaction(amountUsdcAtomic: bigint): Permit2Approval;
|
package/dist/funds.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { encodeFunctionData } from "viem";
|
|
2
|
+
import { FEE_CHAIN_ID, PERMIT2, USDC_BASE } from "./constants.js";
|
|
3
|
+
import { RectaCheckError } from "./errors.js";
|
|
4
|
+
export const ERC20_READ_ABI = [
|
|
5
|
+
{ type: "function", name: "balanceOf", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ name: "", type: "uint256" }] },
|
|
6
|
+
{ type: "function", name: "allowance", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ name: "", type: "uint256" }] },
|
|
7
|
+
];
|
|
8
|
+
const APPROVE_ABI = [
|
|
9
|
+
{ type: "function", name: "approve", stateMutability: "nonpayable", inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ name: "", type: "bool" }] },
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* The fee's amount against the payer's funds. `reservedForRouteUsdcAtomic`: USDC on Base the route will spend first
|
|
13
|
+
* (see `routeReserveOf`); the balance must cover it plus the fee. The allowance only needs to cover the fee: a route's
|
|
14
|
+
* own approval goes to the bridge, not to Permit2.
|
|
15
|
+
*/
|
|
16
|
+
export function feeReadiness(funds, requiredUsdcAtomic, reservedForRouteUsdcAtomic = 0n) {
|
|
17
|
+
const missing = [];
|
|
18
|
+
if (funds.balanceUsdcAtomic < requiredUsdcAtomic + reservedForRouteUsdcAtomic)
|
|
19
|
+
missing.push("balance");
|
|
20
|
+
if (funds.permit2AllowanceUsdcAtomic < requiredUsdcAtomic)
|
|
21
|
+
missing.push("allowance");
|
|
22
|
+
return { ready: missing.length === 0, requiredUsdcAtomic, reservedForRouteUsdcAtomic, balanceUsdcAtomic: funds.balanceUsdcAtomic, permit2AllowanceUsdcAtomic: funds.permit2AllowanceUsdcAtomic, missing };
|
|
23
|
+
}
|
|
24
|
+
const DECIMAL_USDC = /^(0|[1-9][0-9]{0,17})(\.[0-9]{1,6})?$/;
|
|
25
|
+
/**
|
|
26
|
+
* USDC on Base that a route spends before Recta pulls its fee: the request's amount when the route starts from USDC on
|
|
27
|
+
* Base (the same balance pays both), otherwise 0. Throws when such an amount is not a plain decimal: fail closed.
|
|
28
|
+
*/
|
|
29
|
+
export function routeReserveOf(request) {
|
|
30
|
+
if (request.source_network !== "base" || request.source_asset.toUpperCase() !== "USDC")
|
|
31
|
+
return 0n;
|
|
32
|
+
if (!DECIMAL_USDC.test(request.amount))
|
|
33
|
+
throw new RectaCheckError("amount", "the request's amount is not a decimal USDC amount");
|
|
34
|
+
const [whole, frac = ""] = request.amount.split(".");
|
|
35
|
+
return BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
|
|
36
|
+
}
|
|
37
|
+
/** A read error's text without what an RPC URL may carry (an API key in the path or query): viem's short message,
|
|
38
|
+
* and any URL replaced (Fable M-2 on recta #12). */
|
|
39
|
+
export function safeErrorText(e) {
|
|
40
|
+
const short = typeof e === "object" && e !== null && typeof e.shortMessage === "string" ? e.shortMessage : e instanceof Error ? e.message : String(e);
|
|
41
|
+
return short.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi, "<url>").split("\n")[0].slice(0, 300);
|
|
42
|
+
}
|
|
43
|
+
const usdc = (atomic) => {
|
|
44
|
+
const whole = atomic / 1000000n;
|
|
45
|
+
const frac = (atomic % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
46
|
+
return frac === "" ? whole.toString() : `${whole}.${frac}`;
|
|
47
|
+
};
|
|
48
|
+
/** Throws a RectaCheckError naming what is short and how to fix it; nothing has been signed when it throws. */
|
|
49
|
+
export function assertFeeReady(r) {
|
|
50
|
+
if (r.ready)
|
|
51
|
+
return;
|
|
52
|
+
const parts = [];
|
|
53
|
+
if (r.missing.includes("balance"))
|
|
54
|
+
parts.push(`hold at least ${usdc(r.requiredUsdcAtomic + r.reservedForRouteUsdcAtomic)} USDC on Base (now ${usdc(r.balanceUsdcAtomic)}${r.reservedForRouteUsdcAtomic > 0n ? `; ${usdc(r.reservedForRouteUsdcAtomic)} of it is the transfer itself, which leaves Base first` : ""})`);
|
|
55
|
+
if (r.missing.includes("allowance"))
|
|
56
|
+
parts.push(`approve Permit2 (${PERMIT2}) to spend at least ${usdc(r.requiredUsdcAtomic)} USDC on Base (now ${usdc(r.permit2AllowanceUsdcAtomic)}); permit2ApprovalTransaction(amount) builds that exact, bounded approval for you to send`);
|
|
57
|
+
throw new RectaCheckError(r.missing.includes("allowance") ? "fee_allowance" : "fee_balance", `Recta could not collect this fee: ${parts.join("; ")}. Nothing was signed.`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Builds the exact approval for the agent's operator to send with their own wallet. Bounded on purpose: it refuses 0
|
|
61
|
+
* and anything of 2^128 or more, so an "unlimited" approval (2^256 − 1, 2^160 − 1) cannot be built here. Approving
|
|
62
|
+
* replaces, not adds to, the current allowance; each collected fee lowers it, so approve again when a check reports
|
|
63
|
+
* `allowance` short.
|
|
64
|
+
*/
|
|
65
|
+
export function permit2ApprovalTransaction(amountUsdcAtomic) {
|
|
66
|
+
if (amountUsdcAtomic <= 0n)
|
|
67
|
+
throw new RectaCheckError("fee_approval", "the approval must be more than 0");
|
|
68
|
+
if (amountUsdcAtomic >= 2n ** 128n)
|
|
69
|
+
throw new RectaCheckError("fee_approval", "refusing an unlimited approval: approve a bounded amount");
|
|
70
|
+
return {
|
|
71
|
+
chainId: FEE_CHAIN_ID,
|
|
72
|
+
to: USDC_BASE,
|
|
73
|
+
data: encodeFunctionData({ abi: APPROVE_ABI, functionName: "approve", args: [PERMIT2, amountUsdcAtomic] }),
|
|
74
|
+
value: 0n,
|
|
75
|
+
amountUsdcAtomic,
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { createRectaClient, signedRequestMessage, type RectaClient, type RectaClientOptions, type FeeOutcome } from "./client.js";
|
|
1
|
+
export { createRectaClient, signedRequestMessage, referrerQuery, type RectaClient, type RectaClientOptions, type FeeOutcome } from "./client.js";
|
|
2
2
|
export { checkFeePermit, type FeePermitLimits } from "./fee.js";
|
|
3
|
+
export { feeReadiness, assertFeeReady, permit2ApprovalTransaction, routeReserveOf, safeErrorText, ERC20_READ_ABI, type FeeFunds, type FeeReadiness, type Permit2Approval } from "./funds.js";
|
|
3
4
|
export { checkExecutionArtifact, pinnedRouteOf, type CheckedArtifact, type EvaluationRequestView, type ArtifactLimits } from "./artifact.js";
|
|
4
5
|
export { PINNED_ROUTES, type PinnedRoute } from "./pinned-routes.js";
|
|
5
6
|
export { parseWalletPolicy, type WalletPolicy, type WalletPolicyNetwork } from "./policy.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { createRectaClient, signedRequestMessage } from "./client.js";
|
|
1
|
+
export { createRectaClient, signedRequestMessage, referrerQuery } from "./client.js";
|
|
2
2
|
export { checkFeePermit } from "./fee.js";
|
|
3
|
+
export { feeReadiness, assertFeeReady, permit2ApprovalTransaction, routeReserveOf, safeErrorText, ERC20_READ_ABI } from "./funds.js";
|
|
3
4
|
export { checkExecutionArtifact, pinnedRouteOf } from "./artifact.js";
|
|
4
5
|
export { PINNED_ROUTES } from "./pinned-routes.js";
|
|
5
6
|
export { parseWalletPolicy } from "./policy.js";
|
package/dist/signer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Account, Chain, Hex, PublicClient, Transport, TransactionSerializable, WalletClient } from "viem";
|
|
2
|
+
import { type FeeFunds } from "./funds.js";
|
|
2
3
|
/** One EIP-1559 transaction of an execution artifact, as this package asks a signer to send it. */
|
|
3
4
|
export interface RectaTransaction {
|
|
4
5
|
readonly chainId: number;
|
|
@@ -34,10 +35,16 @@ export interface RectaSigner {
|
|
|
34
35
|
/** Sends one transaction on `tx.chainId` and returns its hash. */
|
|
35
36
|
sendTransaction(tx: RectaTransaction): Promise<Hex>;
|
|
36
37
|
}
|
|
37
|
-
/** Read access to the
|
|
38
|
+
/** Read access to the chains (the agent's own RPC): receipts, and the payer's USDC on Base before a fee is signed. */
|
|
38
39
|
export interface RectaChainReader {
|
|
39
40
|
/** Waits until the transaction is mined and returns whether it succeeded. */
|
|
40
41
|
waitForSuccess(chainId: number, hash: Hex): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* The payer's USDC balance on Base and its ERC-20 allowance to Permit2 (ASTRA T2): read before every fee or debt
|
|
44
|
+
* permit is signed, which is refused when either is below the permit's amount. A reader that cannot answer (no Base
|
|
45
|
+
* RPC) must throw: the permit is then not signed.
|
|
46
|
+
*/
|
|
47
|
+
readFeeFunds(owner: Hex): Promise<FeeFunds>;
|
|
41
48
|
}
|
|
42
49
|
/** A RectaSigner from a viem account and one wallet client per chain. */
|
|
43
50
|
export declare function viemSigner(args: {
|
|
@@ -100,5 +107,5 @@ export declare function cdpSigner(args: {
|
|
|
100
107
|
readonly account: CdpEvmAccountLike;
|
|
101
108
|
readonly walletClientFor?: (chainId: number) => WalletClient<Transport, Chain | undefined, Account | undefined>;
|
|
102
109
|
}): RectaSigner;
|
|
103
|
-
/** A RectaChainReader from one viem public client per chain. */
|
|
110
|
+
/** A RectaChainReader from one viem public client per chain (`publicClientFor(8453)` reads the fee funds on Base). */
|
|
104
111
|
export declare function viemChainReader(publicClientFor: (chainId: number) => PublicClient): RectaChainReader;
|
package/dist/signer.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { toAccount } from "viem/accounts";
|
|
2
|
+
import { FEE_CHAIN_ID, PERMIT2, USDC_BASE } from "./constants.js";
|
|
3
|
+
import { ERC20_READ_ABI, safeErrorText } from "./funds.js";
|
|
2
4
|
/** A RectaSigner from a viem account and one wallet client per chain. */
|
|
3
5
|
export function viemSigner(args) {
|
|
4
6
|
const { account } = args;
|
|
@@ -85,9 +87,26 @@ export function cdpSigner(args) {
|
|
|
85
87
|
},
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
|
-
/** A RectaChainReader from one viem public client per chain. */
|
|
90
|
+
/** A RectaChainReader from one viem public client per chain (`publicClientFor(8453)` reads the fee funds on Base). */
|
|
89
91
|
export function viemChainReader(publicClientFor) {
|
|
90
92
|
return {
|
|
91
93
|
waitForSuccess: async (chainId, hash) => (await publicClientFor(chainId).waitForTransactionReceipt({ hash })).status === "success",
|
|
94
|
+
readFeeFunds: async (owner) => {
|
|
95
|
+
const base = publicClientFor(FEE_CHAIN_ID);
|
|
96
|
+
const chainId = await base.getChainId().catch((e) => { throw new Error(`reading Base's chain id failed: ${safeErrorText(e)}`); });
|
|
97
|
+
if (chainId !== FEE_CHAIN_ID)
|
|
98
|
+
throw new Error(`publicClientFor(${FEE_CHAIN_ID}) returned a client on chain ${chainId}`);
|
|
99
|
+
try {
|
|
100
|
+
const [balanceUsdcAtomic, permit2AllowanceUsdcAtomic] = await Promise.all([
|
|
101
|
+
base.readContract({ address: USDC_BASE, abi: ERC20_READ_ABI, functionName: "balanceOf", args: [owner] }),
|
|
102
|
+
base.readContract({ address: USDC_BASE, abi: ERC20_READ_ABI, functionName: "allowance", args: [owner, PERMIT2] }),
|
|
103
|
+
]);
|
|
104
|
+
return { balanceUsdcAtomic, permit2AllowanceUsdcAtomic };
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
// viem's full message names the RPC URL, which may carry an API key: never pass it on (Fable M-2).
|
|
108
|
+
throw new Error(`reading USDC on Base failed: ${safeErrorText(e)}`);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
92
111
|
};
|
|
93
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@recta/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Client for Recta (api.recta.build): evaluate a cross-chain stablecoin transfer, authorize Recta's 5 %-of-verified-saving fee, execute the committed route and report it, with local checks before every signature.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Recta (https://recta.build)",
|