@catena/sdk 0.0.0-bootstrap.0 → 0.4.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 +191 -0
- package/README.md +910 -2
- package/dist/client-BSYVYJGm.mjs +1032 -0
- package/dist/client-B_Gi0aqH.d.mts +1353 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +3 -0
- package/dist/keypair-BjMJtI3-.d.mts +35 -0
- package/dist/keypair.d.mts +2 -0
- package/dist/keypair.mjs +50 -0
- package/dist/movements.d.mts +139 -0
- package/dist/movements.mjs +334 -0
- package/dist/mpp.d.mts +327 -0
- package/dist/mpp.mjs +563 -0
- package/dist/settlement-report-DTxaAxE3.mjs +57 -0
- package/dist/viem.d.mts +135 -0
- package/dist/viem.mjs +296 -0
- package/dist/x402.d.mts +212 -0
- package/dist/x402.mjs +215 -0
- package/package.json +64 -5
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { t as InvalidKeyError } from "./keypair-BjMJtI3-.mjs";
|
|
2
|
+
import { $ as X402SignedOffer, A as ExtensibleString, B as OnchainNetworkInput, C as COUNTERPARTY_RULE_MODES, D as CounterpartiesResponse, E as CounterpartiesParams, F as Movement, G as Policy, H as POLICY_OVERRIDE_CAPABILITIES, I as MppAction, J as WalletCounterpartyRail, K as SEND_METHODS, L as MppChallenge, M as IntentAction, N as IntentResult, O as Counterparty, P as MOVEMENT_NEXT_STEPS, Q as AppInfo, R as MppResource, S as COUNTERPARTY_ACTIONS, T as CROSS_CHAIN_STATES, U as POLICY_RULE_ACTIONS, V as POLICY_CAPABILITIES, W as POLICY_RULE_TYPES, X as X402PaymentRequirements, Y as X402Authorization, Z as X402Resource, _ as AccountDepositAddressResponse, a as IntentSubmitError, b as AccountsResponse, c as ACCOUNT_AGGREGATION_SCOPES, d as ACCOUNT_TRANSACTION_MOVEMENT_STATUSES, et as X402SignedReceipt, f as ACCOUNT_TRANSACTION_STATUSES, g as AccountDepositAddressParams, h as AccountBalanceResponse, i as FetchLike, j as INTENT_ACTION_TYPES, k as CrossChainSend, l as ACCOUNT_DEPOSIT_ADDRESS_SOURCES, m as ACTOR_AGGREGATION_SCOPES, n as CatenaClient, o as TimeoutError, p as ACCOUNT_TRANSACTION_TYPES, q as SendMethod, r as CatenaClientOptions, s as createCatenaClient, t as ApiError, u as ACCOUNT_TRANSACTION_METHODS, v as AccountTransactionsParams, w as CROSS_CHAIN_NEXT_STEPS, x as Agent, y as AccountTransactionsResponse, z as OnchainAssetInput } from "./client-B_Gi0aqH.mjs";
|
|
3
|
+
export { ACCOUNT_AGGREGATION_SCOPES, ACCOUNT_DEPOSIT_ADDRESS_SOURCES, ACCOUNT_TRANSACTION_METHODS, ACCOUNT_TRANSACTION_MOVEMENT_STATUSES, ACCOUNT_TRANSACTION_STATUSES, ACCOUNT_TRANSACTION_TYPES, ACTOR_AGGREGATION_SCOPES, type AccountBalanceResponse, type AccountDepositAddressParams, type AccountDepositAddressResponse, type AccountTransactionsParams, type AccountTransactionsResponse, type AccountsResponse, type Agent, ApiError, type AppInfo, COUNTERPARTY_ACTIONS, COUNTERPARTY_RULE_MODES, CROSS_CHAIN_NEXT_STEPS, CROSS_CHAIN_STATES, type CatenaClient, CatenaClientOptions, type CounterpartiesParams, type CounterpartiesResponse, type Counterparty, type CrossChainSend, type ExtensibleString, FetchLike, INTENT_ACTION_TYPES, type IntentAction, type IntentResult, IntentSubmitError, InvalidKeyError, MOVEMENT_NEXT_STEPS, type Movement, type MppAction, type MppChallenge, type MppResource, type OnchainAssetInput, type OnchainNetworkInput, POLICY_CAPABILITIES, POLICY_OVERRIDE_CAPABILITIES, POLICY_RULE_ACTIONS, POLICY_RULE_TYPES, type Policy, SEND_METHODS, type SendMethod, TimeoutError, type WalletCounterpartyRail, type X402Authorization, type X402PaymentRequirements, type X402Resource, type X402SignedOffer, type X402SignedReceipt, createCatenaClient };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { InvalidKeyError } from "./keypair.mjs";
|
|
2
|
+
import { S as SEND_METHODS, _ as MOVEMENT_NEXT_STEPS, a as ACCOUNT_AGGREGATION_SCOPES, b as POLICY_RULE_ACTIONS, c as ACCOUNT_TRANSACTION_MOVEMENT_STATUSES, d as ACTOR_AGGREGATION_SCOPES, f as COUNTERPARTY_ACTIONS, g as INTENT_ACTION_TYPES, h as CROSS_CHAIN_STATES, i as createCatenaClient, l as ACCOUNT_TRANSACTION_STATUSES, m as CROSS_CHAIN_NEXT_STEPS, n as IntentSubmitError, o as ACCOUNT_DEPOSIT_ADDRESS_SOURCES, p as COUNTERPARTY_RULE_MODES, r as TimeoutError, s as ACCOUNT_TRANSACTION_METHODS, t as ApiError, u as ACCOUNT_TRANSACTION_TYPES, v as POLICY_CAPABILITIES, x as POLICY_RULE_TYPES, y as POLICY_OVERRIDE_CAPABILITIES } from "./client-BSYVYJGm.mjs";
|
|
3
|
+
export { ACCOUNT_AGGREGATION_SCOPES, ACCOUNT_DEPOSIT_ADDRESS_SOURCES, ACCOUNT_TRANSACTION_METHODS, ACCOUNT_TRANSACTION_MOVEMENT_STATUSES, ACCOUNT_TRANSACTION_STATUSES, ACCOUNT_TRANSACTION_TYPES, ACTOR_AGGREGATION_SCOPES, ApiError, COUNTERPARTY_ACTIONS, COUNTERPARTY_RULE_MODES, CROSS_CHAIN_NEXT_STEPS, CROSS_CHAIN_STATES, INTENT_ACTION_TYPES, IntentSubmitError, InvalidKeyError, MOVEMENT_NEXT_STEPS, POLICY_CAPABILITIES, POLICY_OVERRIDE_CAPABILITIES, POLICY_RULE_ACTIONS, POLICY_RULE_TYPES, SEND_METHODS, TimeoutError, createCatenaClient };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/keypair.d.ts
|
|
2
|
+
interface P256Keypair {
|
|
3
|
+
algorithm: "p256";
|
|
4
|
+
privateKeyHex: string;
|
|
5
|
+
publicKeyHex: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The supplied value is not a usable P-256 key.
|
|
9
|
+
*/
|
|
10
|
+
declare class InvalidKeyError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate a fresh agent credential. Feed `privateKeyHex` to
|
|
15
|
+
* `createCatenaClient`; the public half (or its thumbprint) is what gets
|
|
16
|
+
* registered with Catena when the agent is linked. The SDK never persists
|
|
17
|
+
* keys — storage is the caller's responsibility.
|
|
18
|
+
*/
|
|
19
|
+
declare function generateP256Keypair(): P256Keypair;
|
|
20
|
+
/**
|
|
21
|
+
* Derive the full keypair from the private scalar alone, so a corrupt key
|
|
22
|
+
* throws at construction instead of surfacing as bank-side invalid_signature
|
|
23
|
+
* 401s — and a mismatched pair cannot exist at all.
|
|
24
|
+
*/
|
|
25
|
+
declare function p256KeypairFromPrivateKeyHex(privateKeyHex: string): P256Keypair;
|
|
26
|
+
declare function p256PointFromCompressedHex(publicKeyHex: string): Buffer | null;
|
|
27
|
+
/**
|
|
28
|
+
* RFC 7638 JWK thumbprint of a P-256 public key — the stable id the API
|
|
29
|
+
* knows a linked credential by. Throws {@link InvalidKeyError} when the
|
|
30
|
+
* input is not a valid P-256 point.
|
|
31
|
+
*/
|
|
32
|
+
declare function computeP256PublicKeyThumbprint(publicKeyHex: string): string;
|
|
33
|
+
declare function p256PublicKeyThumbprintFromPoint(point: Buffer): string;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { p256KeypairFromPrivateKeyHex as a, generateP256Keypair as i, P256Keypair as n, p256PointFromCompressedHex as o, computeP256PublicKeyThumbprint as r, p256PublicKeyThumbprintFromPoint as s, InvalidKeyError as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as p256KeypairFromPrivateKeyHex, i as generateP256Keypair, n as P256Keypair, o as p256PointFromCompressedHex, r as computeP256PublicKeyThumbprint, s as p256PublicKeyThumbprintFromPoint, t as InvalidKeyError } from "./keypair-BjMJtI3-.mjs";
|
|
2
|
+
export { InvalidKeyError, P256Keypair, computeP256PublicKeyThumbprint, generateP256Keypair, p256KeypairFromPrivateKeyHex, p256PointFromCompressedHex, p256PublicKeyThumbprintFromPoint };
|
package/dist/keypair.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ECDH, createECDH, createHash } from "node:crypto";
|
|
2
|
+
//#region src/keypair.ts
|
|
3
|
+
var InvalidKeyError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "InvalidKeyError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function generateP256Keypair() {
|
|
10
|
+
const ecdh = createECDH("prime256v1");
|
|
11
|
+
ecdh.generateKeys();
|
|
12
|
+
return p256KeypairFromPrivateKeyHex(ecdh.getPrivateKey().toString("hex").padStart(64, "0"));
|
|
13
|
+
}
|
|
14
|
+
const PRIVATE_KEY_HEX = /^[0-9a-f]{64}$/i;
|
|
15
|
+
function p256KeypairFromPrivateKeyHex(privateKeyHex) {
|
|
16
|
+
if (!PRIVATE_KEY_HEX.test(privateKeyHex)) throw new InvalidKeyError("P-256 private key must be 64 hex characters");
|
|
17
|
+
const normalized = privateKeyHex.toLowerCase();
|
|
18
|
+
const ecdh = createECDH("prime256v1");
|
|
19
|
+
try {
|
|
20
|
+
ecdh.setPrivateKey(Buffer.from(normalized, "hex"));
|
|
21
|
+
} catch {
|
|
22
|
+
throw new InvalidKeyError("value is not a valid P-256 private key (scalar out of range)");
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
algorithm: "p256",
|
|
26
|
+
privateKeyHex: normalized,
|
|
27
|
+
publicKeyHex: ecdh.getPublicKey("hex", "compressed")
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function p256PointFromCompressedHex(publicKeyHex) {
|
|
31
|
+
try {
|
|
32
|
+
const uncompressed = ECDH.convertKey(publicKeyHex, "prime256v1", "hex", "hex", "uncompressed");
|
|
33
|
+
const point = Buffer.isBuffer(uncompressed) ? uncompressed : Buffer.from(uncompressed, "hex");
|
|
34
|
+
return point.length === 65 ? point : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function computeP256PublicKeyThumbprint(publicKeyHex) {
|
|
40
|
+
const point = p256PointFromCompressedHex(publicKeyHex);
|
|
41
|
+
if (point === null) throw new InvalidKeyError("value is not a valid P-256 public key");
|
|
42
|
+
return p256PublicKeyThumbprintFromPoint(point);
|
|
43
|
+
}
|
|
44
|
+
function p256PublicKeyThumbprintFromPoint(point) {
|
|
45
|
+
const x = point.subarray(1, 33).toString("base64url");
|
|
46
|
+
const y = point.subarray(33, 65).toString("base64url");
|
|
47
|
+
return createHash("sha256").update(`{"crv":"P-256","kty":"EC","x":"${x}","y":"${y}"}`, "utf8").digest("base64url");
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { InvalidKeyError, computeP256PublicKeyThumbprint, generateP256Keypair, p256KeypairFromPrivateKeyHex, p256PointFromCompressedHex, p256PublicKeyThumbprintFromPoint };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//#region src/movements.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Independent review of a movement approval, before anything is stamped.
|
|
4
|
+
*
|
|
5
|
+
* A movement is authorized by one signature over a batch of pre-hashed
|
|
6
|
+
* digests. A digest cannot be read, so a signing policy cannot constrain what
|
|
7
|
+
* it commits to: the only content control is the signer rebuilding every
|
|
8
|
+
* digest from the fields it is said to cover and refusing when they disagree.
|
|
9
|
+
* The console does this before it prompts for a passkey. An agent holds the
|
|
10
|
+
* same authority with a different credential, so it does the same thing here
|
|
11
|
+
* rather than stamping a body it has not checked.
|
|
12
|
+
*
|
|
13
|
+
* The check is local: no network, no server answer to trust. It proves that the
|
|
14
|
+
* request would sign exactly the digests the approval lists, that each of them
|
|
15
|
+
* is the payload shown for it, that every payload acts on the approval's own
|
|
16
|
+
* wallet and on the network it is labelled with, and that a delegation names
|
|
17
|
+
* the one contract a wallet here is allowed to hand itself to.
|
|
18
|
+
*
|
|
19
|
+
* It also refuses any payload that would move the network's own token, which
|
|
20
|
+
* is the one amount every kind states in a field rather than inside call data.
|
|
21
|
+
* On a chain whose gas token is the asset being moved, that value is a payment
|
|
22
|
+
* nothing else in the approval describes.
|
|
23
|
+
*
|
|
24
|
+
* Given the caller's own amount it also proves the approval pays it, for the
|
|
25
|
+
* approvals where that can be known: a transfer authorization states its
|
|
26
|
+
* amount in a field the digest covers, so an approval built only from those is
|
|
27
|
+
* bound to the sum they deliver.
|
|
28
|
+
*
|
|
29
|
+
* What it does not prove is what a payload's call data does. A raw transaction
|
|
30
|
+
* and a batch execution carry their recipient and amount as encoded call data,
|
|
31
|
+
* which is reproduced but not decoded, and a payment can be built entirely
|
|
32
|
+
* from those -- so an approval carrying one is not bound to any amount at all.
|
|
33
|
+
* For those kinds the guarantee is that the bytes signed are the bytes shown,
|
|
34
|
+
* not that the bytes shown are a payment to anyone in particular. Binding them
|
|
35
|
+
* needs that call data decoded, which would mean recognising every contract
|
|
36
|
+
* and call shape a payment can use and refusing any built from one this client
|
|
37
|
+
* does not know yet; that is not attempted here.
|
|
38
|
+
*
|
|
39
|
+
* Requires the optional `viem` peer dependency; importing this module without
|
|
40
|
+
* viem installed fails with a module-not-found error naming it.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* One thing the approval would sign, with every field its digest commits to.
|
|
44
|
+
*/
|
|
45
|
+
export interface MovementInstructionReview {
|
|
46
|
+
/**
|
|
47
|
+
* What kind of signable material this is.
|
|
48
|
+
*/
|
|
49
|
+
kind: string;
|
|
50
|
+
/**
|
|
51
|
+
* The network it acts on.
|
|
52
|
+
*/
|
|
53
|
+
network: string;
|
|
54
|
+
/**
|
|
55
|
+
* The wallet that must sign it.
|
|
56
|
+
*/
|
|
57
|
+
signerAddress: string;
|
|
58
|
+
/**
|
|
59
|
+
* The digest the approval carries for it.
|
|
60
|
+
*/
|
|
61
|
+
digest: string;
|
|
62
|
+
/**
|
|
63
|
+
* The fields the digest commits to.
|
|
64
|
+
*/
|
|
65
|
+
payload: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The approval as prepared, as it arrives on a `submit_stamp` next action.
|
|
69
|
+
*/
|
|
70
|
+
export interface PreparedMovement {
|
|
71
|
+
/**
|
|
72
|
+
* The wallet every instruction must name.
|
|
73
|
+
*/
|
|
74
|
+
signerAddress: string;
|
|
75
|
+
/**
|
|
76
|
+
* Every digest the approval would sign, in order.
|
|
77
|
+
*/
|
|
78
|
+
digests: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* What each of those digests commits to.
|
|
81
|
+
*/
|
|
82
|
+
instructions: readonly MovementInstructionReview[];
|
|
83
|
+
/**
|
|
84
|
+
* The provider request body that would be stamped.
|
|
85
|
+
*/
|
|
86
|
+
body: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* What the caller asked for, to check the approval against. Only the amount
|
|
90
|
+
* today: a counterparty rail is named by id rather than by address, and this
|
|
91
|
+
* client cannot resolve one locally, so the recipient stays unbound.
|
|
92
|
+
*/
|
|
93
|
+
export interface RequestedPayment {
|
|
94
|
+
/**
|
|
95
|
+
* The amount as the caller wrote it, in USDC.
|
|
96
|
+
*/
|
|
97
|
+
amount: string;
|
|
98
|
+
}
|
|
99
|
+
export type MovementReviewVerdict = {
|
|
100
|
+
ok: true;
|
|
101
|
+
} | {
|
|
102
|
+
ok: false;
|
|
103
|
+
reason: string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Recompute an instruction's digest from the fields it says it covers, or
|
|
107
|
+
* `null` when the payload does not parse as its kind. A `null` is a refusal,
|
|
108
|
+
* never a pass.
|
|
109
|
+
*/
|
|
110
|
+
export declare function rebuildInstructionDigest(instruction: Pick<MovementInstructionReview, "kind" | "payload">): string | null;
|
|
111
|
+
/**
|
|
112
|
+
* Whether a prepared movement may be stamped: every digest rebuilt from the
|
|
113
|
+
* fields it says it covers, every instruction naming the same signer and the
|
|
114
|
+
* network it actually signs on, no payload spending from another wallet, no
|
|
115
|
+
* delegation handing the wallet to an unrecognised contract, the approval
|
|
116
|
+
* listing exactly those digests, and the request body pinned to them and to
|
|
117
|
+
* that signer.
|
|
118
|
+
*
|
|
119
|
+
* Refuse on anything but `ok`. A reason describes what disagreed, and a
|
|
120
|
+
* disagreement means the body would sign something other than what it
|
|
121
|
+
* describes.
|
|
122
|
+
*
|
|
123
|
+
* No instruction may move the network's own token, which is the one amount
|
|
124
|
+
* every kind states in a field rather than inside call data.
|
|
125
|
+
*
|
|
126
|
+
* Pass `requested` to also check the approval against what the caller asked
|
|
127
|
+
* for. That binds only when every payload able to move value states its amount
|
|
128
|
+
* in a named field; an approval carrying one whose amount lives in call data
|
|
129
|
+
* is left unbound, because a sum over the rest would bind nothing.
|
|
130
|
+
*
|
|
131
|
+
* This checks that a payload is the one shown for its digest. It does not
|
|
132
|
+
* decode call data, so for kinds that carry their recipient and amount as
|
|
133
|
+
* encoded call data rather than as named fields, a passing verdict means the
|
|
134
|
+
* bytes signed are the bytes shown, not that they pay a particular party for a
|
|
135
|
+
* particular amount. The recipient is never bound: a counterparty rail is
|
|
136
|
+
* named by id, and this client cannot resolve one to an address on its own.
|
|
137
|
+
*/
|
|
138
|
+
export declare function verifyPreparedMovement(prepared: PreparedMovement, requested?: RequestedPayment): MovementReviewVerdict;
|
|
139
|
+
//#endregion
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { getAddress, hashAuthorization, hashTypedData, keccak256, serializeTransaction } from "viem/utils";
|
|
3
|
+
//#region src/movements.ts
|
|
4
|
+
const addressSchema = v.pipe(v.string(), v.transform(getAddress));
|
|
5
|
+
const digitsSchema = v.pipe(v.string(), v.regex(/^\d+$/));
|
|
6
|
+
const hexBytesSchema = v.custom((value) => typeof value === "string" && /^0x([0-9a-fA-F]{2})*$/.test(value), "expected hex bytes");
|
|
7
|
+
const bytes32Schema = v.custom((value) => typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value), "expected a 32-byte value");
|
|
8
|
+
const USDC_EIP3009_NETWORKS = ["base", "base-sepolia"];
|
|
9
|
+
const USDC_EIP3009_DOMAINS = {
|
|
10
|
+
base: {
|
|
11
|
+
name: "USD Coin",
|
|
12
|
+
version: "2",
|
|
13
|
+
chainId: 8453,
|
|
14
|
+
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
15
|
+
},
|
|
16
|
+
"base-sepolia": {
|
|
17
|
+
name: "USDC",
|
|
18
|
+
version: "2",
|
|
19
|
+
chainId: 84532,
|
|
20
|
+
verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const GAS_DELEGATE_DOMAIN = {
|
|
24
|
+
name: "TKGasDelegate",
|
|
25
|
+
version: "1.1"
|
|
26
|
+
};
|
|
27
|
+
const GAS_DELEGATE_ADDRESS = "0x955d84139e7621bc571b117d8eb5d28a4a222c6f";
|
|
28
|
+
const NETWORK_CHAIN_IDS = {
|
|
29
|
+
...Object.fromEntries(Object.entries(USDC_EIP3009_DOMAINS).map(([network, domain]) => [network, domain.chainId])),
|
|
30
|
+
arc: 5042
|
|
31
|
+
};
|
|
32
|
+
const rawTransactionSchema = v.looseObject({ transaction: v.object({
|
|
33
|
+
chainId: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
34
|
+
nonce: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
35
|
+
to: addressSchema,
|
|
36
|
+
value: digitsSchema,
|
|
37
|
+
data: hexBytesSchema,
|
|
38
|
+
gas: digitsSchema,
|
|
39
|
+
maxFeePerGasWei: digitsSchema,
|
|
40
|
+
maxPriorityFeePerGasWei: digitsSchema
|
|
41
|
+
}) });
|
|
42
|
+
const transferAuthorizationSchema = v.looseObject({
|
|
43
|
+
network: v.picklist(USDC_EIP3009_NETWORKS),
|
|
44
|
+
authorization: v.object({
|
|
45
|
+
from: addressSchema,
|
|
46
|
+
to: addressSchema,
|
|
47
|
+
value: digitsSchema,
|
|
48
|
+
validAfter: v.union([v.number(), digitsSchema]),
|
|
49
|
+
validBefore: v.union([v.number(), digitsSchema]),
|
|
50
|
+
nonce: bytes32Schema
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
const delegationSchema = v.looseObject({ authorization: v.object({
|
|
54
|
+
address: addressSchema,
|
|
55
|
+
chainId: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
56
|
+
nonce: v.pipe(v.number(), v.integer(), v.minValue(0))
|
|
57
|
+
}) });
|
|
58
|
+
const batchExecutionSchema = v.looseObject({ batch: v.object({
|
|
59
|
+
from: addressSchema,
|
|
60
|
+
chainId: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
61
|
+
nonce: digitsSchema,
|
|
62
|
+
deadline: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
63
|
+
calls: v.pipe(v.array(v.object({
|
|
64
|
+
to: addressSchema,
|
|
65
|
+
value: digitsSchema,
|
|
66
|
+
data: hexBytesSchema
|
|
67
|
+
})), v.minLength(1))
|
|
68
|
+
}) });
|
|
69
|
+
const preparedBodySchema = v.looseObject({
|
|
70
|
+
type: v.literal("ACTIVITY_TYPE_SIGN_RAW_PAYLOADS"),
|
|
71
|
+
parameters: v.looseObject({
|
|
72
|
+
signWith: addressSchema,
|
|
73
|
+
payloads: v.pipe(v.array(bytes32Schema), v.minLength(1)),
|
|
74
|
+
encoding: v.literal("PAYLOAD_ENCODING_HEXADECIMAL"),
|
|
75
|
+
hashFunction: v.literal("HASH_FUNCTION_NO_OP")
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
function usdcBaseUnits(amount) {
|
|
79
|
+
const match = /^(\d+)(?:\.(\d{1,6}))?$/.exec(amount.trim());
|
|
80
|
+
if (match === null) return null;
|
|
81
|
+
const [, whole = "0", fraction = ""] = match;
|
|
82
|
+
return BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, "0"));
|
|
83
|
+
}
|
|
84
|
+
function instructionFacts(instruction) {
|
|
85
|
+
try {
|
|
86
|
+
switch (instruction.kind) {
|
|
87
|
+
case "evm_raw_transaction": {
|
|
88
|
+
const { transaction } = v.parse(rawTransactionSchema, instruction.payload);
|
|
89
|
+
return {
|
|
90
|
+
digest: keccak256(serializeTransaction({
|
|
91
|
+
type: "eip1559",
|
|
92
|
+
chainId: transaction.chainId,
|
|
93
|
+
nonce: transaction.nonce,
|
|
94
|
+
to: transaction.to,
|
|
95
|
+
value: BigInt(transaction.value),
|
|
96
|
+
data: transaction.data,
|
|
97
|
+
gas: BigInt(transaction.gas),
|
|
98
|
+
maxFeePerGas: BigInt(transaction.maxFeePerGasWei),
|
|
99
|
+
maxPriorityFeePerGas: BigInt(transaction.maxPriorityFeePerGasWei),
|
|
100
|
+
accessList: []
|
|
101
|
+
})),
|
|
102
|
+
chainId: transaction.chainId,
|
|
103
|
+
movesNative: BigInt(transaction.value) !== 0n
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case "eip3009_transfer_authorization": {
|
|
107
|
+
const { network, authorization } = v.parse(transferAuthorizationSchema, instruction.payload);
|
|
108
|
+
return {
|
|
109
|
+
digest: hashTypedData({
|
|
110
|
+
domain: USDC_EIP3009_DOMAINS[network],
|
|
111
|
+
types: { TransferWithAuthorization: [
|
|
112
|
+
{
|
|
113
|
+
name: "from",
|
|
114
|
+
type: "address"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "to",
|
|
118
|
+
type: "address"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "value",
|
|
122
|
+
type: "uint256"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "validAfter",
|
|
126
|
+
type: "uint256"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "validBefore",
|
|
130
|
+
type: "uint256"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "nonce",
|
|
134
|
+
type: "bytes32"
|
|
135
|
+
}
|
|
136
|
+
] },
|
|
137
|
+
primaryType: "TransferWithAuthorization",
|
|
138
|
+
message: {
|
|
139
|
+
from: authorization.from,
|
|
140
|
+
to: authorization.to,
|
|
141
|
+
value: BigInt(authorization.value),
|
|
142
|
+
validAfter: BigInt(authorization.validAfter),
|
|
143
|
+
validBefore: BigInt(authorization.validBefore),
|
|
144
|
+
nonce: authorization.nonce
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
chainId: USDC_EIP3009_DOMAINS[network].chainId,
|
|
148
|
+
from: authorization.from
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "eip7702_delegation": {
|
|
152
|
+
const { authorization } = v.parse(delegationSchema, instruction.payload);
|
|
153
|
+
return {
|
|
154
|
+
digest: hashAuthorization({
|
|
155
|
+
address: authorization.address,
|
|
156
|
+
chainId: authorization.chainId,
|
|
157
|
+
nonce: authorization.nonce
|
|
158
|
+
}),
|
|
159
|
+
chainId: authorization.chainId,
|
|
160
|
+
delegate: authorization.address
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
case "eip712_execution": {
|
|
164
|
+
const { batch } = v.parse(batchExecutionSchema, instruction.payload);
|
|
165
|
+
return {
|
|
166
|
+
digest: hashTypedData({
|
|
167
|
+
domain: {
|
|
168
|
+
...GAS_DELEGATE_DOMAIN,
|
|
169
|
+
chainId: batch.chainId,
|
|
170
|
+
verifyingContract: batch.from
|
|
171
|
+
},
|
|
172
|
+
types: {
|
|
173
|
+
BatchExecution: [
|
|
174
|
+
{
|
|
175
|
+
name: "nonce",
|
|
176
|
+
type: "uint128"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "deadline",
|
|
180
|
+
type: "uint32"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "calls",
|
|
184
|
+
type: "Call[]"
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
Call: [
|
|
188
|
+
{
|
|
189
|
+
name: "to",
|
|
190
|
+
type: "address"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "value",
|
|
194
|
+
type: "uint256"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "data",
|
|
198
|
+
type: "bytes"
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
primaryType: "BatchExecution",
|
|
203
|
+
message: {
|
|
204
|
+
nonce: BigInt(batch.nonce),
|
|
205
|
+
deadline: batch.deadline,
|
|
206
|
+
calls: batch.calls.map((call) => ({
|
|
207
|
+
to: call.to,
|
|
208
|
+
value: BigInt(call.value),
|
|
209
|
+
data: call.data
|
|
210
|
+
}))
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
from: batch.from,
|
|
214
|
+
chainId: batch.chainId,
|
|
215
|
+
movesNative: batch.calls.some((call) => BigInt(call.value) !== 0n)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function rebuildInstructionDigest(instruction) {
|
|
225
|
+
return instructionFacts(instruction)?.digest ?? null;
|
|
226
|
+
}
|
|
227
|
+
function sameList(left, right) {
|
|
228
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
229
|
+
}
|
|
230
|
+
function verifyPreparedMovement(prepared, requested) {
|
|
231
|
+
if (prepared.instructions.length === 0) return {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: "The approval carries nothing to sign."
|
|
234
|
+
};
|
|
235
|
+
const signer = prepared.signerAddress.toLowerCase();
|
|
236
|
+
for (const instruction of prepared.instructions) {
|
|
237
|
+
const facts = instructionFacts(instruction);
|
|
238
|
+
if (facts === null || facts.digest.toLowerCase() !== instruction.digest.toLowerCase()) return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: "An instruction does not match what it says it signs."
|
|
241
|
+
};
|
|
242
|
+
if (instruction.signerAddress.toLowerCase() !== signer) return {
|
|
243
|
+
ok: false,
|
|
244
|
+
reason: "An instruction names a different wallet than the approval."
|
|
245
|
+
};
|
|
246
|
+
const named = NETWORK_CHAIN_IDS[instruction.network];
|
|
247
|
+
if (named === void 0) return {
|
|
248
|
+
ok: false,
|
|
249
|
+
reason: `An instruction names a network this client cannot place: ${instruction.network}. If the payment looks right, this client may be out of date.`
|
|
250
|
+
};
|
|
251
|
+
if (facts.chainId !== named) return {
|
|
252
|
+
ok: false,
|
|
253
|
+
reason: "An instruction names a different network than it signs on."
|
|
254
|
+
};
|
|
255
|
+
if (facts.delegate !== void 0 && facts.delegate.toLowerCase() !== GAS_DELEGATE_ADDRESS) return {
|
|
256
|
+
ok: false,
|
|
257
|
+
reason: "An instruction would hand the wallet over to an unrecognised contract."
|
|
258
|
+
};
|
|
259
|
+
if (facts.from !== void 0 && facts.from.toLowerCase() !== signer) return {
|
|
260
|
+
ok: false,
|
|
261
|
+
reason: "An instruction would spend from a different wallet."
|
|
262
|
+
};
|
|
263
|
+
if (facts.movesNative === true) return {
|
|
264
|
+
ok: false,
|
|
265
|
+
reason: "An instruction would move the network's own token."
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const rebuiltDigests = prepared.instructions.map((instruction) => instruction.digest.toLowerCase());
|
|
269
|
+
if (new Set(rebuiltDigests).size !== rebuiltDigests.length) return {
|
|
270
|
+
ok: false,
|
|
271
|
+
reason: "The approval lists the same digest more than once."
|
|
272
|
+
};
|
|
273
|
+
const spentNonces = new Set();
|
|
274
|
+
for (const instruction of prepared.instructions) {
|
|
275
|
+
if (instruction.kind !== "eip3009_transfer_authorization") continue;
|
|
276
|
+
const parsed = v.safeParse(transferAuthorizationSchema, instruction.payload);
|
|
277
|
+
if (!parsed.success) continue;
|
|
278
|
+
const { network, authorization } = parsed.output;
|
|
279
|
+
const spent = `${network}:${authorization.from.toLowerCase()}:${authorization.nonce.toLowerCase()}`;
|
|
280
|
+
if (spentNonces.has(spent)) return {
|
|
281
|
+
ok: false,
|
|
282
|
+
reason: "The approval spends one authorization nonce twice."
|
|
283
|
+
};
|
|
284
|
+
spentNonces.add(spent);
|
|
285
|
+
}
|
|
286
|
+
if (!sameList(prepared.digests.map((digest) => digest.toLowerCase()), rebuiltDigests)) return {
|
|
287
|
+
ok: false,
|
|
288
|
+
reason: "The approval lists different digests than its instructions."
|
|
289
|
+
};
|
|
290
|
+
let body;
|
|
291
|
+
try {
|
|
292
|
+
body = v.parse(preparedBodySchema, JSON.parse(prepared.body));
|
|
293
|
+
} catch {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
reason: "The prepared request is not in the shape expected."
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
if (!sameList(body.parameters.payloads.map((payload) => payload.toLowerCase()), rebuiltDigests)) return {
|
|
300
|
+
ok: false,
|
|
301
|
+
reason: "The prepared request would sign different digests."
|
|
302
|
+
};
|
|
303
|
+
if (body.parameters.signWith.toLowerCase() !== signer) return {
|
|
304
|
+
ok: false,
|
|
305
|
+
reason: "The prepared request names a different wallet than the approval."
|
|
306
|
+
};
|
|
307
|
+
return bindsRequestedAmount(prepared, signer, requested);
|
|
308
|
+
}
|
|
309
|
+
function bindsRequestedAmount(prepared, signer, requested) {
|
|
310
|
+
if (requested === void 0) return { ok: true };
|
|
311
|
+
const wanted = usdcBaseUnits(requested.amount);
|
|
312
|
+
if (wanted === null) return {
|
|
313
|
+
ok: false,
|
|
314
|
+
reason: "The requested amount is not an amount this client can check."
|
|
315
|
+
};
|
|
316
|
+
let delivered = 0n;
|
|
317
|
+
for (const instruction of prepared.instructions) {
|
|
318
|
+
if (instruction.kind === "evm_raw_transaction" || instruction.kind === "eip712_execution") return { ok: true };
|
|
319
|
+
if (instruction.kind !== "eip3009_transfer_authorization") continue;
|
|
320
|
+
const parsed = v.safeParse(transferAuthorizationSchema, instruction.payload);
|
|
321
|
+
if (!parsed.success) return {
|
|
322
|
+
ok: false,
|
|
323
|
+
reason: "An instruction does not match what it says it signs."
|
|
324
|
+
};
|
|
325
|
+
if (parsed.output.authorization.to.toLowerCase() !== signer) delivered += BigInt(parsed.output.authorization.value);
|
|
326
|
+
}
|
|
327
|
+
if (delivered === wanted) return { ok: true };
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
reason: "This approval does not pay the amount that was requested."
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
//#endregion
|
|
334
|
+
export { rebuildInstructionDigest, verifyPreparedMovement };
|