@recta/client 0.0.0-stage → 0.1.0
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 +21 -0
- package/README.md +136 -2
- package/dist/artifact.d.ts +55 -0
- package/dist/artifact.js +207 -0
- package/dist/client.d.ts +119 -0
- package/dist/client.js +190 -0
- package/dist/constants.d.ts +67 -0
- package/dist/constants.js +50 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +33 -0
- package/dist/fee.d.ts +26 -0
- package/dist/fee.js +98 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/pinned-routes.d.ts +19 -0
- package/dist/pinned-routes.js +3431 -0
- package/dist/policy.d.ts +21 -0
- package/dist/policy.js +47 -0
- package/dist/signer.d.ts +104 -0
- package/dist/signer.js +93 -0
- package/package.json +52 -4
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface WalletPolicyNetwork {
|
|
2
|
+
readonly network: string;
|
|
3
|
+
readonly chain_id: number;
|
|
4
|
+
readonly approvals: readonly {
|
|
5
|
+
readonly asset: string;
|
|
6
|
+
readonly token: string;
|
|
7
|
+
readonly spenders: readonly string[];
|
|
8
|
+
}[];
|
|
9
|
+
readonly call_targets: readonly {
|
|
10
|
+
readonly address: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
export interface WalletPolicy {
|
|
14
|
+
readonly networks: readonly WalletPolicyNetwork[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reads Recta's wallet policy (`/v1/golden-corridor/wallet-policy`) into the shape the artifact checks use, and refuses
|
|
18
|
+
* it unless its fee block names exactly this package's pinned network, Permit2, spender, token and recipient. A policy
|
|
19
|
+
* that disagrees with the pins means the server is not the Recta this release was built for: nothing is signed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseWalletPolicy(body: unknown): WalletPolicy;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FEE_CHAIN_ID, FEE_RECEIVER, PERMIT2, UPTO_PROXY, USDC_BASE, WALLET_POLICY_SCHEMA } from "./constants.js";
|
|
2
|
+
import { RectaCheckError } from "./errors.js";
|
|
3
|
+
const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
4
|
+
/**
|
|
5
|
+
* Reads Recta's wallet policy (`/v1/golden-corridor/wallet-policy`) into the shape the artifact checks use, and refuses
|
|
6
|
+
* it unless its fee block names exactly this package's pinned network, Permit2, spender, token and recipient. A policy
|
|
7
|
+
* that disagrees with the pins means the server is not the Recta this release was built for: nothing is signed.
|
|
8
|
+
*/
|
|
9
|
+
export function parseWalletPolicy(body) {
|
|
10
|
+
const fail = (message) => { throw new RectaCheckError("wallet_policy", message); };
|
|
11
|
+
const p = typeof body === "object" && body !== null ? body : fail("not an object");
|
|
12
|
+
if (p["schema"] !== WALLET_POLICY_SCHEMA)
|
|
13
|
+
fail(`schema ${String(p["schema"])}, not ${WALLET_POLICY_SCHEMA}`);
|
|
14
|
+
const fee = typeof p["recta_fee"] === "object" && p["recta_fee"] !== null ? p["recta_fee"] : fail("no recta_fee");
|
|
15
|
+
const pinned = [
|
|
16
|
+
["chain_id", fee["chain_id"], FEE_CHAIN_ID],
|
|
17
|
+
["permit2", lower(fee["permit2"]), PERMIT2],
|
|
18
|
+
["spender", lower(fee["spender"]), UPTO_PROXY],
|
|
19
|
+
["token", lower(fee["token"]), USDC_BASE],
|
|
20
|
+
["recipient", lower(fee["recipient"]), FEE_RECEIVER],
|
|
21
|
+
];
|
|
22
|
+
for (const [field, served, pin] of pinned)
|
|
23
|
+
if (served !== pin)
|
|
24
|
+
fail(`recta_fee.${field} is ${String(served)}, not the pinned ${pin}`);
|
|
25
|
+
const networks = Array.isArray(p["networks"]) ? p["networks"] : fail("networks is not a list");
|
|
26
|
+
return {
|
|
27
|
+
networks: networks.map((raw, i) => {
|
|
28
|
+
const n = typeof raw === "object" && raw !== null ? raw : fail(`networks[${i}] is not an object`);
|
|
29
|
+
if (typeof n["network"] !== "string" || !Number.isSafeInteger(n["chain_id"]))
|
|
30
|
+
fail(`networks[${i}] has no network or chain_id`);
|
|
31
|
+
const approvals = Array.isArray(n["approvals"]) ? n["approvals"] : fail(`networks[${i}].approvals is not a list`);
|
|
32
|
+
const targets = Array.isArray(n["call_targets"]) ? n["call_targets"] : fail(`networks[${i}].call_targets is not a list`);
|
|
33
|
+
return {
|
|
34
|
+
network: n["network"],
|
|
35
|
+
chain_id: n["chain_id"],
|
|
36
|
+
approvals: approvals.map((a) => ({
|
|
37
|
+
asset: typeof a["asset"] === "string" ? a["asset"] : fail(`networks[${i}]: an approval has no asset`),
|
|
38
|
+
token: address(a["token"], fail),
|
|
39
|
+
spenders: Array.isArray(a["spenders"]) ? a["spenders"].map((s) => address(s, fail)) : fail(`networks[${i}]: an approval has no spenders`),
|
|
40
|
+
})),
|
|
41
|
+
call_targets: targets.map((t) => ({ address: address(t["address"], fail) })),
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const lower = (v) => (typeof v === "string" ? v.toLowerCase() : v);
|
|
47
|
+
const address = (v, fail) => (typeof v === "string" && ADDRESS.test(v) ? v.toLowerCase() : fail(`${String(v)} is not an address`));
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Account, Chain, Hex, PublicClient, Transport, TransactionSerializable, WalletClient } from "viem";
|
|
2
|
+
/** One EIP-1559 transaction of an execution artifact, as this package asks a signer to send it. */
|
|
3
|
+
export interface RectaTransaction {
|
|
4
|
+
readonly chainId: number;
|
|
5
|
+
readonly to: Hex;
|
|
6
|
+
readonly data: Hex;
|
|
7
|
+
readonly value: bigint;
|
|
8
|
+
readonly gas: bigint;
|
|
9
|
+
readonly maxFeePerGas: bigint;
|
|
10
|
+
readonly maxPriorityFeePerGas: bigint;
|
|
11
|
+
}
|
|
12
|
+
/** EIP-712 typed data of a Recta fee permit, as this package rebuilds it after its checks. */
|
|
13
|
+
export interface RectaTypedData {
|
|
14
|
+
readonly domain: {
|
|
15
|
+
readonly name: "Permit2";
|
|
16
|
+
readonly chainId: number;
|
|
17
|
+
readonly verifyingContract: Hex;
|
|
18
|
+
};
|
|
19
|
+
readonly types: Record<string, readonly {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly type: string;
|
|
22
|
+
}[]>;
|
|
23
|
+
readonly primaryType: "PermitWitnessTransferFrom";
|
|
24
|
+
readonly message: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What this package needs from a wallet, and nothing more. It never receives or stores a key. The account must be an
|
|
28
|
+
* EOA: Recta's signed requests are EIP-191 signatures recovered to the sender's address (smart accounts cannot).
|
|
29
|
+
*/
|
|
30
|
+
export interface RectaSigner {
|
|
31
|
+
readonly address: Hex;
|
|
32
|
+
signMessage(message: string): Promise<Hex>;
|
|
33
|
+
signTypedData(typedData: RectaTypedData): Promise<Hex>;
|
|
34
|
+
/** Sends one transaction on `tx.chainId` and returns its hash. */
|
|
35
|
+
sendTransaction(tx: RectaTransaction): Promise<Hex>;
|
|
36
|
+
}
|
|
37
|
+
/** Read access to the source chain (the agent's own RPC): receipts. */
|
|
38
|
+
export interface RectaChainReader {
|
|
39
|
+
/** Waits until the transaction is mined and returns whether it succeeded. */
|
|
40
|
+
waitForSuccess(chainId: number, hash: Hex): Promise<boolean>;
|
|
41
|
+
}
|
|
42
|
+
/** A RectaSigner from a viem account and one wallet client per chain. */
|
|
43
|
+
export declare function viemSigner(args: {
|
|
44
|
+
readonly account: Account;
|
|
45
|
+
readonly walletClientFor: (chainId: number) => WalletClient<Transport, Chain | undefined, Account | undefined>;
|
|
46
|
+
}): RectaSigner;
|
|
47
|
+
/**
|
|
48
|
+
* What this package uses of a Coinbase CDP EVM server account (`await cdp.evm.getOrCreateAccount({ name })` in
|
|
49
|
+
* `@coinbase/cdp-sdk`): an EOA whose key stays in CDP and whose operations CDP's Policy Engine can restrict. Typed by
|
|
50
|
+
* shape only, so this package does not depend on the CDP SDK.
|
|
51
|
+
*/
|
|
52
|
+
export interface CdpEvmAccountLike {
|
|
53
|
+
readonly address: Hex;
|
|
54
|
+
signMessage(args: {
|
|
55
|
+
message: string;
|
|
56
|
+
}): Promise<Hex>;
|
|
57
|
+
signTypedData(args: {
|
|
58
|
+
domain: Record<string, unknown>;
|
|
59
|
+
types: Record<string, unknown>;
|
|
60
|
+
primaryType: string;
|
|
61
|
+
message: Record<string, unknown>;
|
|
62
|
+
}): Promise<Hex>;
|
|
63
|
+
signTransaction(transaction: TransactionSerializable): Promise<Hex>;
|
|
64
|
+
sendTransaction(args: {
|
|
65
|
+
network: CdpSendNetwork;
|
|
66
|
+
transaction: {
|
|
67
|
+
to: Hex;
|
|
68
|
+
data: Hex;
|
|
69
|
+
value: bigint;
|
|
70
|
+
gas: bigint;
|
|
71
|
+
maxFeePerGas: bigint;
|
|
72
|
+
maxPriorityFeePerGas: bigint;
|
|
73
|
+
type: "eip1559";
|
|
74
|
+
};
|
|
75
|
+
}): Promise<{
|
|
76
|
+
transactionHash: Hex;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* EVM chains on which CDP itself sends a transaction (`sendEvmTransaction`, @coinbase/cdp-sdk 1.61.0), by chain id:
|
|
81
|
+
* exactly the networks of the CDP policy Recta serves (Fable #262 B-2), so every transaction CDP sends is one that
|
|
82
|
+
* policy can allow.
|
|
83
|
+
*/
|
|
84
|
+
export declare const CDP_SEND_NETWORKS: Readonly<{
|
|
85
|
+
readonly 1: "ethereum";
|
|
86
|
+
readonly 10: "optimism";
|
|
87
|
+
readonly 137: "polygon";
|
|
88
|
+
readonly 8453: "base";
|
|
89
|
+
readonly 42161: "arbitrum";
|
|
90
|
+
readonly 43114: "avalanche";
|
|
91
|
+
}>;
|
|
92
|
+
export type CdpSendNetwork = (typeof CDP_SEND_NETWORKS)[keyof typeof CDP_SEND_NETWORKS];
|
|
93
|
+
/**
|
|
94
|
+
* A RectaSigner from a Coinbase CDP EVM server account. CDP signs the requests and the fee permit. A route transaction,
|
|
95
|
+
* after this package's checks like any other, is sent by CDP (`sendEvmTransaction`, which Recta's CDP policy at
|
|
96
|
+
* `/v1/golden-corridor/wallet-policy/cdp` restricts) on the chains CDP sends on; on any other chain it is signed by CDP
|
|
97
|
+
* (`signEvmTransaction`) and sent through `walletClientFor`, and without one it is refused.
|
|
98
|
+
*/
|
|
99
|
+
export declare function cdpSigner(args: {
|
|
100
|
+
readonly account: CdpEvmAccountLike;
|
|
101
|
+
readonly walletClientFor?: (chainId: number) => WalletClient<Transport, Chain | undefined, Account | undefined>;
|
|
102
|
+
}): RectaSigner;
|
|
103
|
+
/** A RectaChainReader from one viem public client per chain. */
|
|
104
|
+
export declare function viemChainReader(publicClientFor: (chainId: number) => PublicClient): RectaChainReader;
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { toAccount } from "viem/accounts";
|
|
2
|
+
/** A RectaSigner from a viem account and one wallet client per chain. */
|
|
3
|
+
export function viemSigner(args) {
|
|
4
|
+
const { account } = args;
|
|
5
|
+
if (account.signMessage === undefined || account.signTypedData === undefined) {
|
|
6
|
+
// A JSON-RPC account signs through the wallet client of the fee chain.
|
|
7
|
+
const client = args.walletClientFor(8453);
|
|
8
|
+
return {
|
|
9
|
+
address: account.address,
|
|
10
|
+
signMessage: (message) => client.signMessage({ account, message }),
|
|
11
|
+
signTypedData: (td) => client.signTypedData({ account, ...td }),
|
|
12
|
+
sendTransaction: (tx) => sendWith(args.walletClientFor(tx.chainId), account, tx),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
address: account.address,
|
|
17
|
+
signMessage: (message) => account.signMessage({ message }),
|
|
18
|
+
signTypedData: (td) => account.signTypedData(td),
|
|
19
|
+
sendTransaction: (tx) => sendWith(args.walletClientFor(tx.chainId), account, tx),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function sendWith(client, account, tx) {
|
|
23
|
+
const chainId = await client.getChainId();
|
|
24
|
+
if (chainId !== tx.chainId)
|
|
25
|
+
throw new Error(`walletClientFor(${tx.chainId}) returned a client on chain ${chainId}`);
|
|
26
|
+
return client.sendTransaction({
|
|
27
|
+
account,
|
|
28
|
+
chain: client.chain ?? null,
|
|
29
|
+
to: tx.to,
|
|
30
|
+
data: tx.data,
|
|
31
|
+
value: tx.value,
|
|
32
|
+
gas: tx.gas,
|
|
33
|
+
maxFeePerGas: tx.maxFeePerGas,
|
|
34
|
+
maxPriorityFeePerGas: tx.maxPriorityFeePerGas,
|
|
35
|
+
type: "eip1559",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* EVM chains on which CDP itself sends a transaction (`sendEvmTransaction`, @coinbase/cdp-sdk 1.61.0), by chain id:
|
|
40
|
+
* exactly the networks of the CDP policy Recta serves (Fable #262 B-2), so every transaction CDP sends is one that
|
|
41
|
+
* policy can allow.
|
|
42
|
+
*/
|
|
43
|
+
export const CDP_SEND_NETWORKS = Object.freeze({ 1: "ethereum", 10: "optimism", 137: "polygon", 8453: "base", 42161: "arbitrum", 43114: "avalanche" });
|
|
44
|
+
/** Integers as decimal strings: CDP's API carries typed data as JSON, which has no bigint. The EIP-712 hash is the same. */
|
|
45
|
+
function jsonSafe(value) {
|
|
46
|
+
if (typeof value === "bigint")
|
|
47
|
+
return value.toString();
|
|
48
|
+
if (Array.isArray(value))
|
|
49
|
+
return value.map(jsonSafe);
|
|
50
|
+
if (typeof value === "object" && value !== null)
|
|
51
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, jsonSafe(v)]));
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A RectaSigner from a Coinbase CDP EVM server account. CDP signs the requests and the fee permit. A route transaction,
|
|
56
|
+
* after this package's checks like any other, is sent by CDP (`sendEvmTransaction`, which Recta's CDP policy at
|
|
57
|
+
* `/v1/golden-corridor/wallet-policy/cdp` restricts) on the chains CDP sends on; on any other chain it is signed by CDP
|
|
58
|
+
* (`signEvmTransaction`) and sent through `walletClientFor`, and without one it is refused.
|
|
59
|
+
*/
|
|
60
|
+
export function cdpSigner(args) {
|
|
61
|
+
const cdp = args.account;
|
|
62
|
+
const account = toAccount({
|
|
63
|
+
address: cdp.address,
|
|
64
|
+
signMessage: async ({ message }) => {
|
|
65
|
+
if (typeof message !== "string")
|
|
66
|
+
throw new Error("cdpSigner signs text messages only");
|
|
67
|
+
return cdp.signMessage({ message });
|
|
68
|
+
},
|
|
69
|
+
signTypedData: async (typedData) => cdp.signTypedData(jsonSafe(typedData)),
|
|
70
|
+
signTransaction: async (transaction) => cdp.signTransaction(transaction),
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
address: cdp.address,
|
|
74
|
+
signMessage: (message) => account.signMessage({ message }),
|
|
75
|
+
signTypedData: (td) => account.signTypedData(td),
|
|
76
|
+
sendTransaction: async (tx) => {
|
|
77
|
+
const network = CDP_SEND_NETWORKS[tx.chainId];
|
|
78
|
+
if (network !== undefined) {
|
|
79
|
+
const { to, data, value, gas, maxFeePerGas, maxPriorityFeePerGas } = tx;
|
|
80
|
+
return (await cdp.sendTransaction({ network, transaction: { to, data, value, gas, maxFeePerGas, maxPriorityFeePerGas, type: "eip1559" } })).transactionHash;
|
|
81
|
+
}
|
|
82
|
+
if (args.walletClientFor === undefined)
|
|
83
|
+
throw new Error(`CDP does not send on chain ${tx.chainId}: give cdpSigner a walletClientFor`);
|
|
84
|
+
return sendWith(args.walletClientFor(tx.chainId), account, tx);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** A RectaChainReader from one viem public client per chain. */
|
|
89
|
+
export function viemChainReader(publicClientFor) {
|
|
90
|
+
return {
|
|
91
|
+
waitForSuccess: async (chainId, hash) => (await publicClientFor(chainId).waitForTransactionReceipt({ hash })).status === "success",
|
|
92
|
+
};
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@recta/client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": "Recta (https://recta.build)",
|
|
7
|
+
"homepage": "https://github.com/RectaBuild/recta/tree/main/packages/client#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/RectaBuild/recta.git",
|
|
11
|
+
"directory": "packages/client"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/RectaBuild/recta/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"recta",
|
|
18
|
+
"ai-agent",
|
|
19
|
+
"cross-chain",
|
|
20
|
+
"bridge",
|
|
21
|
+
"stablecoin",
|
|
22
|
+
"usdc",
|
|
23
|
+
"x402",
|
|
24
|
+
"viem"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"viem": ">=2.56.3 <3"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
}
|
|
54
|
+
}
|