@tollxyz/guard 0.1.2 → 0.3.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/README.md +69 -21
- package/dist/check.d.ts +26 -2
- package/dist/check.js +23 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/policy.d.ts +45 -10
- package/dist/policy.js +116 -17
- package/dist/wallets.d.ts +20 -11
- package/dist/wallets.js +242 -57
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
# @tollxyz/guard
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
> **What changed in 0.3.0: every signing method is checked.**
|
|
4
|
+
> - Messages: viem and ethers `signMessage`, and `request()` for `personal_sign` and `eth_sign`.
|
|
5
|
+
> - Typed data: `request()` for `eth_signTypedData_v4` too, any EIP-712 (orders, permits, sign-ins).
|
|
6
|
+
> - Batches: viem `sendCalls` (EIP-5792) and `request()` for `wallet_sendCalls`, each call checked.
|
|
7
|
+
> - Smart accounts: `guardSmartAccount` wraps a viem bundler client's `sendUserOperation` and `prepareUserOperation`;
|
|
8
|
+
> `guard()` picks it for a bundler client.
|
|
9
|
+
> - EIP-7702: `onAuthorization` now defaults to `"check"`: Toll reads the authorization and decides (a known, verified
|
|
10
|
+
> delegate is WARN medium and goes ahead under `block-high`; an unknown one is BLOCK). `"block"` and `"allow"` stay.
|
|
11
|
+
> An authorization Toll cannot read is blocked.
|
|
12
|
+
> - A signature for another chain is blocked (before 0.3.0 it went through as unsupported); `onUnsupported: "allow"`
|
|
13
|
+
> lets it through.
|
|
14
|
+
> - `eth_sign` always fails closed: it can sign any 32 bytes, a transaction included, so when Toll cannot answer it is
|
|
15
|
+
> blocked whatever `onUnavailable` says.
|
|
16
|
+
|
|
17
|
+
> **Defaults changed in 0.2.0.** A WARN with any `high` or `critical` warning is now blocked (`onWarn: "block-high"`,
|
|
18
|
+
> the new default); a WARN with only `medium`/`low` warnings still goes ahead, logged. To keep the 0.1.x behaviour,
|
|
19
|
+
> pass `onWarn: "allow"`. New `strict: true` preset: WARN, unavailable and unsupported all block. Any option you pass
|
|
20
|
+
> explicitly still wins over the preset (`{ strict: true, onUnavailable: "allow" }` stays fail-open).
|
|
21
|
+
|
|
22
|
+
Toll's live pre-sign check in front of an AI agent's wallet. Before the agent signs a transaction, a message, an
|
|
23
|
+
EIP-712 signature, a batch, a user operation, an EIP-7702 authorization or an x402 payment on Robinhood Chain, the guard asks [Toll](https://t0ll.xyz/agents/x402#presign) and
|
|
5
24
|
gets ALLOW / WARN / BLOCK with the reasons. Zero runtime dependencies, ESM only, Node >= 20.19, with types.
|
|
6
25
|
|
|
7
26
|
```sh
|
|
@@ -9,13 +28,13 @@ npm install @tollxyz/guard
|
|
|
9
28
|
```
|
|
10
29
|
|
|
11
30
|
```ts
|
|
12
|
-
const wallet = guard(walletClient); // viem WalletClient or ethers v6 Signer; same methods, checked first
|
|
31
|
+
const wallet = guard(walletClient); // viem WalletClient, viem bundler client or ethers v6 Signer; same methods, checked first
|
|
13
32
|
```
|
|
14
33
|
|
|
15
34
|
```ts
|
|
16
35
|
import { guard, TollBlockedError } from "@tollxyz/guard";
|
|
17
36
|
|
|
18
|
-
const wallet = guard(walletClient, { onWarn: "
|
|
37
|
+
const wallet = guard(walletClient, { onWarn: "block-high", onUnavailable: "allow" });
|
|
19
38
|
try {
|
|
20
39
|
await wallet.writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [spender, maxUint256] });
|
|
21
40
|
} catch (e) {
|
|
@@ -57,23 +76,43 @@ The answer is an [ERC-8265](https://github.com/ethereum/ERCs/pull/1753) RiskAsse
|
|
|
57
76
|
`{ action, warnings: [{ code, severity, message, "x-toll-evidence" }], scanners, "x-toll": { decoded, link, checked_at, note } }`.
|
|
58
77
|
Critical = BLOCK, high or medium = WARN, low or none = ALLOW.
|
|
59
78
|
|
|
60
|
-
Robinhood Chain (4663) only for now. A
|
|
61
|
-
goes through (`onUnsupported: "block"` to stop it instead).
|
|
79
|
+
Robinhood Chain (4663) only for now. A transaction on another chain, or a contract deployment, is not sent to Toll
|
|
80
|
+
and goes through (`onUnsupported: "block"` to stop it instead). A signature for another chain is blocked
|
|
81
|
+
(`onUnsupported: "allow"` lets it through).
|
|
62
82
|
|
|
63
83
|
## API
|
|
64
84
|
|
|
65
|
-
### `guard(wallet, opts?)`, `guardViem(client, opts?)`, `guardEthers(signer, opts?)`
|
|
85
|
+
### `guard(wallet, opts?)`, `guardViem(client, opts?)`, `guardSmartAccount(bundler, opts?)`, `guardEthers(signer, opts?)`
|
|
66
86
|
|
|
67
87
|
Returns the same wallet behind a Proxy. Checked first:
|
|
68
88
|
|
|
69
89
|
- viem WalletClient: `sendTransaction`, `writeContract` (calldata encoded with your viem's `encodeFunctionData`),
|
|
70
|
-
`signTransaction`, `signTypedData`
|
|
71
|
-
|
|
90
|
+
`signTransaction`, `signTypedData`, `signMessage`, `sendCalls`, `signAuthorization`, and `request()` for
|
|
91
|
+
`eth_sendTransaction`, `eth_signTransaction`, `personal_sign`, `eth_sign`, `eth_signTypedData_v4` and
|
|
92
|
+
`wallet_sendCalls` (every other method passes through)
|
|
93
|
+
- viem bundler client (account abstraction): `sendUserOperation`, `prepareUserOperation`. `calls` are checked one by
|
|
94
|
+
one as transactions from the smart account's address; a ready `callData` is sent as a user operation with the
|
|
95
|
+
account's EntryPoint
|
|
96
|
+
- ethers v6 Signer: `sendTransaction`, `signTransaction`, `signTypedData`, `signMessage`, `authorize`
|
|
97
|
+
|
|
98
|
+
A batch is checked call by call, in order; the first call blocked stops the whole batch.
|
|
99
|
+
|
|
100
|
+
An EIP-7702 authorization sets the wallet's code to another contract, which can then move everything it holds: a
|
|
101
|
+
transaction with an `authorizationList` on any method above, viem's `signAuthorization`, ethers' `authorize`, a user
|
|
102
|
+
operation's `authorization`. By default (`onAuthorization: "check"`) Toll reads it and decides; if Toll cannot read it
|
|
103
|
+
(unreachable, another chain) it is blocked (TollBlockedError, code `eip7702-authorization`). `"block"` refuses every
|
|
104
|
+
authorization without asking; `"allow"` lets it through unchecked. The account's own sign methods
|
|
105
|
+
(`client.account.sign*`) are not wrapped.
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
107
|
+
```ts
|
|
108
|
+
const wallet = guard(walletClient, { origin: "https://app.example" }); // origin: the site asking, for sign-ins
|
|
109
|
+
await wallet.signMessage({ message: "Sign in to app.example" }); // checked as personal_sign
|
|
110
|
+
await wallet.request({ method: "personal_sign", params: [hexMessage, address] });
|
|
111
|
+
await wallet.request({ method: "eth_sign", params: [address, hash] }); // blocked when Toll blocks it, or cannot answer
|
|
112
|
+
|
|
113
|
+
const smart = guard(bundlerClient); // detected by sendUserOperation
|
|
114
|
+
await smart.sendUserOperation({ calls: [{ to: token, abi: erc20Abi, functionName: "transfer", args: [to, amount] }] });
|
|
115
|
+
```
|
|
77
116
|
|
|
78
117
|
Everything else passes through unchanged. Wrap last: `client.extend(...)` or `signer.connect(...)` on the wrapped
|
|
79
118
|
wallet returns an unguarded one. viem and ethers are optional peer dependencies, never imported at load time.
|
|
@@ -97,8 +136,12 @@ const pay = wrapFetchWithPayment(guardX402Fetch(fetch, { agentId: 123 }), accoun
|
|
|
97
136
|
|
|
98
137
|
### `checkTx(input, opts?)`
|
|
99
138
|
|
|
100
|
-
The raw call. `input`: `{ chainId, from, to, data, value }` for a transaction,
|
|
101
|
-
|
|
139
|
+
The raw call. `input`: `{ chainId, from, to, data, value, authorizationList? }` for a transaction,
|
|
140
|
+
`{ chainId, from, authorizationList }` for an authorization alone, `{ typedData }` for an EIP-712 request,
|
|
141
|
+
`{ message: { kind: "personal_sign" | "eth_sign", data, from? } }` for a message, `{ rpc: { method, params } }` for a
|
|
142
|
+
wallet request (`personal_sign`, `eth_sign`, `eth_signTypedData_v4`, `eth_sendTransaction`), `{ userOp, entryPoint? }`
|
|
143
|
+
for an ERC-4337 user operation, `{ x402, agentId?, payment? }` for an x402 payment, or several together; `origin` is
|
|
144
|
+
the site asking. Returns the RiskAssessment. When a
|
|
102
145
|
coin is still being read, Toll answers WARN `coin-not-read` with `Retry-After`: `checkTx` asks once more if the wait
|
|
103
146
|
is at most `maxRetryWaitMs` (default 10 s); a coin still not read stays WARN, never ALLOW. Throws
|
|
104
147
|
`TollUnavailableError` when Toll cannot answer.
|
|
@@ -107,11 +150,13 @@ is at most `maxRetryWaitMs` (default 10 s); a coin still not read stays WARN, ne
|
|
|
107
150
|
|
|
108
151
|
| Option | Default | |
|
|
109
152
|
| --- | --- | --- |
|
|
110
|
-
| `
|
|
153
|
+
| `strict` | `false` | preset: the defaults of `onWarn`, `onUnavailable` and `onUnsupported` become `"block"`; each one passed explicitly still wins |
|
|
154
|
+
| `onWarn` | `"block-high"` | `"block-high"` (blocked when any warning is `high` or `critical`, else allowed and logged), `"allow"` (every WARN goes ahead, logged; the 0.1.x default), `"block"`, or `(assessment) => boolean` (true lets it through) |
|
|
111
155
|
| `onBlock` | `"throw"` | `"throw"` (TollBlockedError), or `(assessment) => boolean`, e.g. ask a human; true lets it through |
|
|
112
|
-
| `onUnavailable` | `"allow"` | Toll unreachable, timed out, 5xx, 429, or a request it could not read: `"allow"` (fail-open, logged) or `"block"` (fail-closed, TollUnavailableError) |
|
|
113
|
-
| `onUnsupported` | `"allow"` | another chain, a deployment: `"allow"` or `"block"` |
|
|
114
|
-
| `onAuthorization` | `"
|
|
156
|
+
| `onUnavailable` | `"allow"` (`strict`: `"block"`) | Toll unreachable, timed out, 5xx, 429, or a request it could not read: `"allow"` (fail-open, logged) or `"block"` (fail-closed, TollUnavailableError) |
|
|
157
|
+
| `onUnsupported` | `"allow"` (`strict`: `"block"`) | a transaction on another chain, a deployment: `"allow"` or `"block"`. A signature for another chain is blocked unless this is `"allow"` explicitly |
|
|
158
|
+
| `onAuthorization` | `"check"` | an EIP-7702 authorization: `"check"` (Toll decides; blocked when Toll cannot read it), `"block"` (TollBlockedError, Toll not asked) or `"allow"` (unchecked, logged) |
|
|
159
|
+
| `origin` | none | the site asking for a signature, sent with messages and typed data |
|
|
115
160
|
| `onReport` | none | `(report) => void`: every decision, with `assessment`, `decision`, `basis`, `paidUrl` |
|
|
116
161
|
| `logger` | `console.warn` | where WARN and unavailable lines go; `false` = silent |
|
|
117
162
|
| `timeoutMs` | `15000` | one request to Toll |
|
|
@@ -126,13 +171,16 @@ Errors: `TollBlockedError` (`.assessment`, `.codes`, message lists each code and
|
|
|
126
171
|
The default is fail-open: if Toll cannot answer, the signature goes ahead, with a `[toll-guard] ... NOT checked`
|
|
127
172
|
line and an `onReport` call with `basis: "unavailable"`. The agent must never get stuck because of Toll, and the
|
|
128
173
|
free route is rate limited per IP (a busy agent will see 429s). If an unchecked signature is worse for you than a
|
|
129
|
-
stalled agent, set `onUnavailable: "block"
|
|
174
|
+
stalled agent, set `onUnavailable: "block"` (or `strict: true`, which also blocks every WARN and unsupported request).
|
|
175
|
+
|
|
176
|
+
Two requests fail closed whatever `onUnavailable` says: `eth_sign` (it signs any 32 bytes, a transaction hash
|
|
177
|
+
included, so an unchecked one could be anything) and an EIP-7702 authorization under `onAuthorization: "check"`.
|
|
130
178
|
|
|
131
179
|
## Free limit and the paid route
|
|
132
180
|
|
|
133
181
|
The check is free, rate limited per IP. The coin a swap buys is read through the same free coins as Toll's coin
|
|
134
182
|
check (10 new coins a day per caller, coins already read stay free); past that the coin is not read and the answer
|
|
135
|
-
is WARN `coin-over-quota` naming the paid route, `https://t0ll.xyz/api/x402/tx` (0.
|
|
183
|
+
is WARN `coin-over-quota` naming the paid route, `https://t0ll.xyz/api/x402/tx` (0.005 USDG over x402, no daily
|
|
136
184
|
limit). The guard never pays: it puts that URL in the log line, in `report.paidUrl` and in `paidUrlOf(assessment)`.
|
|
137
185
|
|
|
138
186
|
## Privacy
|
package/dist/check.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export declare const DEFAULT_BASE_URL = "https://t0ll.xyz";
|
|
6
6
|
/** Robinhood Chain: the only chain the pre-sign check reads for now */
|
|
7
7
|
export declare const TX_CHAIN_ID = 4663;
|
|
8
|
-
/** the paid route without the daily limit (0.
|
|
8
|
+
/** the paid route without the daily limit (0.005 USDG over x402); the guard never pays it */
|
|
9
9
|
export declare const PAID_TX_PATH = "/api/x402/tx";
|
|
10
10
|
export declare const PRESIGN_DOCS = "https://t0ll.xyz/agents/x402#presign";
|
|
11
11
|
export declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
@@ -61,8 +61,22 @@ export interface TypedDataLike {
|
|
|
61
61
|
primaryType: string;
|
|
62
62
|
message: Record<string, unknown>;
|
|
63
63
|
}
|
|
64
|
+
/** A personal_sign / eth_sign message: data is hex (the bytes signed) or text. */
|
|
65
|
+
export interface SignMessageLike {
|
|
66
|
+
kind: "personal_sign" | "eth_sign";
|
|
67
|
+
data: string;
|
|
68
|
+
origin?: string;
|
|
69
|
+
from?: string;
|
|
70
|
+
}
|
|
71
|
+
/** A wallet RPC request Toll reads as the body it stands for: personal_sign, eth_sign, eth_signTypedData_v4, eth_sendTransaction. */
|
|
72
|
+
export interface RpcLike {
|
|
73
|
+
method: string;
|
|
74
|
+
params?: readonly unknown[];
|
|
75
|
+
}
|
|
64
76
|
/**
|
|
65
|
-
* What to check. A transaction ({chainId, from, to, data, value}
|
|
77
|
+
* What to check. A transaction ({chainId, from, to, data, value}, with an EIP-7702 authorizationList or not), an
|
|
78
|
+
* authorization list alone ({chainId, from, authorizationList}), an EIP-712 request ({typedData}), a message
|
|
79
|
+
* ({message}), a wallet request ({rpc}), an ERC-4337 user operation ({userOp, entryPoint?}), an x402 payment
|
|
66
80
|
* ({x402, agentId?, payment?}), or several together.
|
|
67
81
|
*/
|
|
68
82
|
export interface CheckInput {
|
|
@@ -71,7 +85,17 @@ export interface CheckInput {
|
|
|
71
85
|
to?: string;
|
|
72
86
|
data?: string;
|
|
73
87
|
value?: Numberish;
|
|
88
|
+
/** EIP-7702 authorizations: with to/data a type-4 transaction, alone a signAuthorization */
|
|
89
|
+
authorizationList?: readonly unknown[];
|
|
74
90
|
typedData?: TypedDataLike;
|
|
91
|
+
message?: SignMessageLike;
|
|
92
|
+
rpc?: RpcLike;
|
|
93
|
+
/** an ERC-4337 user operation (v0.6, v0.7 packed or the RPC form) */
|
|
94
|
+
userOp?: Record<string, unknown>;
|
|
95
|
+
/** the user operation's EntryPoint */
|
|
96
|
+
entryPoint?: string;
|
|
97
|
+
/** the site asking for the signature (a URL or host name) */
|
|
98
|
+
origin?: string;
|
|
75
99
|
/** the 402's PaymentRequired, or one of its accepts entries */
|
|
76
100
|
x402?: Record<string, unknown>;
|
|
77
101
|
/** the seller's ERC-8004 agent id, when known */
|
package/dist/check.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export const DEFAULT_BASE_URL = "https://t0ll.xyz";
|
|
6
6
|
/** Robinhood Chain: the only chain the pre-sign check reads for now */
|
|
7
7
|
export const TX_CHAIN_ID = 4663;
|
|
8
|
-
/** the paid route without the daily limit (0.
|
|
8
|
+
/** the paid route without the daily limit (0.005 USDG over x402); the guard never pays it */
|
|
9
9
|
export const PAID_TX_PATH = "/api/x402/tx";
|
|
10
10
|
export const PRESIGN_DOCS = "https://t0ll.xyz/agents/x402#presign";
|
|
11
11
|
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
@@ -61,6 +61,7 @@ const trimBase = (b) => b.replace(/\/+$/, "");
|
|
|
61
61
|
/** the request body as the route reads it (lib/txcheck.ts parseTxCheckBody): payment rides inside x402 */
|
|
62
62
|
export function bodyOf(input) {
|
|
63
63
|
const b = {};
|
|
64
|
+
const auths = input.authorizationList !== undefined && input.authorizationList !== null && input.authorizationList.length > 0;
|
|
64
65
|
if (input.to !== undefined || input.data !== undefined) {
|
|
65
66
|
if (input.chainId !== undefined)
|
|
66
67
|
b.chainId = input.chainId;
|
|
@@ -69,9 +70,30 @@ export function bodyOf(input) {
|
|
|
69
70
|
b.to = input.to;
|
|
70
71
|
b.data = input.data ?? "0x";
|
|
71
72
|
b.value = input.value ?? 0;
|
|
73
|
+
if (auths)
|
|
74
|
+
b.authorizationList = input.authorizationList;
|
|
75
|
+
}
|
|
76
|
+
else if (auths) {
|
|
77
|
+
// a signAuthorization alone: the list and its signer
|
|
78
|
+
if (input.chainId !== undefined)
|
|
79
|
+
b.chainId = input.chainId;
|
|
80
|
+
if (input.from !== undefined)
|
|
81
|
+
b.from = input.from;
|
|
82
|
+
b.authorizationList = input.authorizationList;
|
|
72
83
|
}
|
|
73
84
|
if (input.typedData)
|
|
74
85
|
b.typedData = input.typedData;
|
|
86
|
+
if (input.message)
|
|
87
|
+
b.message = input.message;
|
|
88
|
+
if (input.rpc)
|
|
89
|
+
b.rpc = input.rpc;
|
|
90
|
+
if (input.userOp) {
|
|
91
|
+
b.userOp = input.userOp;
|
|
92
|
+
if (input.entryPoint !== undefined)
|
|
93
|
+
b.entryPoint = input.entryPoint;
|
|
94
|
+
}
|
|
95
|
+
if (input.origin !== undefined)
|
|
96
|
+
b.origin = input.origin;
|
|
75
97
|
if (input.x402)
|
|
76
98
|
b.x402 = input.payment ? { ...input.x402, payment: input.payment } : input.x402;
|
|
77
99
|
if (input.agentId !== undefined && input.agentId !== null)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { checkTx, chainIdOf, paidUrlOf, isAssessment, TollBlockedError, TollUnavailableError, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRY_WAIT_MS, PAID_TX_PATH, PRESIGN_DOCS, TX_CHAIN_ID, } from "./check.js";
|
|
2
|
-
export type { Action, Severity, RiskWarning, ScannerVerdict, TollExtra, RiskAssessment, CheckInput, CheckOptions, TypedDataLike, FetchLike, Hex, Numberish, UnavailableReason, } from "./check.js";
|
|
3
|
-
export { enforce, unsupportedWhy, refuse, gateAuthorization } from "./policy.js";
|
|
1
|
+
export { checkTx, bodyOf, chainIdOf, paidUrlOf, isAssessment, TollBlockedError, TollUnavailableError, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRY_WAIT_MS, PAID_TX_PATH, PRESIGN_DOCS, TX_CHAIN_ID, } from "./check.js";
|
|
2
|
+
export type { Action, Severity, RiskWarning, ScannerVerdict, TollExtra, RiskAssessment, SignMessageLike, RpcLike, CheckInput, CheckOptions, TypedDataLike, FetchLike, Hex, Numberish, UnavailableReason, } from "./check.js";
|
|
3
|
+
export { enforce, enforceCalls, unsupportedWhy, refuse, gateAuthorization, resolvePolicy, hasHighWarning, isSignature, isEthSign } from "./policy.js";
|
|
4
4
|
export type { GuardOptions, GuardReport, GuardKind, WarnPolicy, BlockPolicy } from "./policy.js";
|
|
5
|
-
export { guard, guardViem, guardEthers, isViemClient, primaryTypeOf } from "./wallets.js";
|
|
5
|
+
export { guard, guardViem, guardEthers, guardSmartAccount, isViemClient, isBundlerClient, primaryTypeOf } from "./wallets.js";
|
|
6
6
|
export type { ViemGuardOptions } from "./wallets.js";
|
|
7
7
|
export { guardX402Fetch, readPaymentRequired, readPaymentForms, paymentFormsDiffer } from "./x402.js";
|
|
8
8
|
export type { X402GuardOptions } from "./x402.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { checkTx, chainIdOf, paidUrlOf, isAssessment, TollBlockedError, TollUnavailableError, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRY_WAIT_MS, PAID_TX_PATH, PRESIGN_DOCS, TX_CHAIN_ID, } from "./check.js";
|
|
2
|
-
export { enforce, unsupportedWhy, refuse, gateAuthorization } from "./policy.js";
|
|
3
|
-
export { guard, guardViem, guardEthers, isViemClient, primaryTypeOf } from "./wallets.js";
|
|
1
|
+
export { checkTx, bodyOf, chainIdOf, paidUrlOf, isAssessment, TollBlockedError, TollUnavailableError, DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRY_WAIT_MS, PAID_TX_PATH, PRESIGN_DOCS, TX_CHAIN_ID, } from "./check.js";
|
|
2
|
+
export { enforce, enforceCalls, unsupportedWhy, refuse, gateAuthorization, resolvePolicy, hasHighWarning, isSignature, isEthSign } from "./policy.js";
|
|
3
|
+
export { guard, guardViem, guardEthers, guardSmartAccount, isViemClient, isBundlerClient, primaryTypeOf } from "./wallets.js";
|
|
4
4
|
export { guardX402Fetch, readPaymentRequired, readPaymentForms, paymentFormsDiffer } from "./x402.js";
|
|
5
5
|
export { read8470, latestFinding8470, decodeGetObservation, decodeLatestFinding, encodeGetObservation, encodeLatestByToken, encodeLatestFinding, FINDING_KINDS, NOT_READ, OBS_REGISTRY, OBS_DETECTOR, OBS_CHAIN_ID, RH_RPC, } from "./erc8470.js";
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TollUnavailableError, type CheckInput, type CheckOptions, type RiskAssessment } from "./check.js";
|
|
2
|
-
export type WarnPolicy = "allow" | "block" | ((assessment: RiskAssessment) => boolean | Promise<boolean>);
|
|
2
|
+
export type WarnPolicy = "allow" | "block" | "block-high" | ((assessment: RiskAssessment) => boolean | Promise<boolean>);
|
|
3
3
|
export type BlockPolicy = "throw" | ((assessment: RiskAssessment) => boolean | Promise<boolean>);
|
|
4
|
-
export type GuardKind = "transaction" | "typedData" | "x402";
|
|
4
|
+
export type GuardKind = "transaction" | "typedData" | "x402" | "message" | "userOp" | "calls";
|
|
5
5
|
export interface GuardReport {
|
|
6
6
|
kind: GuardKind;
|
|
7
7
|
/** what was sent to Toll (null when nothing was sent) */
|
|
@@ -17,17 +17,25 @@ export interface GuardReport {
|
|
|
17
17
|
error?: TollUnavailableError;
|
|
18
18
|
}
|
|
19
19
|
export interface GuardOptions extends CheckOptions {
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* WARN: "block-high" (default since 0.2.0: blocked when any warning is high or critical, else allowed and logged),
|
|
22
|
+
* "allow" (every WARN goes ahead, logged; the 0.1.x default), "block", or a function (true lets it through)
|
|
23
|
+
*/
|
|
21
24
|
onWarn?: WarnPolicy;
|
|
22
25
|
/** BLOCK: "throw" (default, TollBlockedError), or a function (e.g. ask a human); true lets it through */
|
|
23
26
|
onBlock?: BlockPolicy;
|
|
24
27
|
/**
|
|
25
28
|
* Toll unreachable, timed out, 5xx, 429, or a request it could not read: "allow" (default, fail-open, logged) or
|
|
26
|
-
* "block" (fail-closed, throws TollUnavailableError).
|
|
29
|
+
* "block" (fail-closed, throws TollUnavailableError). `strict` makes "block" the default.
|
|
27
30
|
*/
|
|
28
31
|
onUnavailable?: "allow" | "block";
|
|
29
|
-
/** a request Toll does not read (another chain, a contract deploy): "allow" (default) or "block" */
|
|
32
|
+
/** a request Toll does not read (another chain, a contract deploy): "allow" (default) or "block"; `strict`: "block" */
|
|
30
33
|
onUnsupported?: "allow" | "block";
|
|
34
|
+
/**
|
|
35
|
+
* The strict preset: the defaults become onWarn "block", onUnavailable "block" and onUnsupported "block".
|
|
36
|
+
* Any of those three passed explicitly (not undefined) still wins over the preset.
|
|
37
|
+
*/
|
|
38
|
+
strict?: boolean;
|
|
31
39
|
/** every decision, with Toll's answer; errors thrown here are caught and logged */
|
|
32
40
|
onReport?: (report: GuardReport) => void | Promise<void>;
|
|
33
41
|
/** where WARN / unavailable lines go (default console.warn); false = silent */
|
|
@@ -36,11 +44,28 @@ export interface GuardOptions extends CheckOptions {
|
|
|
36
44
|
agentId?: number | string | ((url: string) => number | string | undefined | null);
|
|
37
45
|
/**
|
|
38
46
|
* An EIP-7702 authorization (a transaction's authorizationList, signAuthorization / authorize): it sets the
|
|
39
|
-
* wallet's code to another contract, which can then move everything the wallet holds.
|
|
40
|
-
* "
|
|
47
|
+
* wallet's code to another contract, which can then move everything the wallet holds.
|
|
48
|
+
* "check" (default since 0.3.0): Toll reads it and decides (a known, verified delegate comes back WARN medium and
|
|
49
|
+
* passes under onWarn "block-high"; an unknown one comes back BLOCK). It fails closed: when Toll cannot read it
|
|
50
|
+
* (unreachable, another chain, a request it refuses) it is blocked, whatever onUnavailable / onUnsupported say.
|
|
51
|
+
* "block": TollBlockedError, nothing is signed, Toll is not asked. "allow": let it through unchecked (logged, reported).
|
|
41
52
|
*/
|
|
42
|
-
onAuthorization?: "block" | "allow";
|
|
53
|
+
onAuthorization?: "check" | "block" | "allow";
|
|
54
|
+
/** the site asking for a signature (a URL or host name), sent with messages so Toll can match a sign-in's domain */
|
|
55
|
+
origin?: string;
|
|
43
56
|
}
|
|
57
|
+
/** the effective policy: explicit options win, then the strict preset, then the defaults */
|
|
58
|
+
export declare function resolvePolicy(o?: GuardOptions): {
|
|
59
|
+
onWarn: WarnPolicy;
|
|
60
|
+
onUnavailable: "allow" | "block";
|
|
61
|
+
onUnsupported: "allow" | "block";
|
|
62
|
+
};
|
|
63
|
+
/** true when any warning is high or critical */
|
|
64
|
+
export declare const hasHighWarning: (a: RiskAssessment) => boolean;
|
|
65
|
+
/** true for a signature request (typed data, a message, a signing RPC) rather than a transaction or an x402 payment */
|
|
66
|
+
export declare const isSignature: (input: CheckInput | null | undefined) => boolean;
|
|
67
|
+
/** true for eth_sign: it signs any 32 bytes (a transaction hash included), so it fails closed when Toll cannot answer */
|
|
68
|
+
export declare const isEthSign: (input: CheckInput | null | undefined) => boolean;
|
|
44
69
|
/** why Toll would not read this request, or null when it does */
|
|
45
70
|
export declare function unsupportedWhy(input: CheckInput): string | null;
|
|
46
71
|
/**
|
|
@@ -54,5 +79,15 @@ export declare function enforce(kind: GuardKind, build: () => CheckInput | Promi
|
|
|
54
79
|
* disagree): reported as a BLOCK with one critical warning, then TollBlockedError. Nothing is sent to Toll.
|
|
55
80
|
*/
|
|
56
81
|
export declare function refuse(kind: GuardKind, input: CheckInput | null, code: string, message: string, o?: GuardOptions): Promise<never>;
|
|
57
|
-
/**
|
|
58
|
-
|
|
82
|
+
/**
|
|
83
|
+
* The onAuthorization policy for an EIP-7702 authorization. Resolves true when Toll checked the whole request (the
|
|
84
|
+
* "check" default: the caller asks nothing more), false when "allow" let it through unchecked (the caller still checks
|
|
85
|
+
* the rest of the request); throws when it is blocked. `input` is the request with its authorizationList, or a function
|
|
86
|
+
* that builds it; under "check", null is blocked (nothing to send).
|
|
87
|
+
*/
|
|
88
|
+
export declare function gateAuthorization(kind: GuardKind, where: string, input: CheckInput | null | (() => CheckInput | Promise<CheckInput>), o?: GuardOptions): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* A batch (EIP-5792 sendCalls, wallet_sendCalls, a smart account's calls): each call is checked as its own transaction
|
|
91
|
+
* from the batch's sender, in order; the first one blocked stops the batch. Reports carry kind "calls".
|
|
92
|
+
*/
|
|
93
|
+
export declare function enforceCalls(build: () => CheckInput[] | Promise<CheckInput[]>, o?: GuardOptions): Promise<GuardReport[]>;
|
package/dist/policy.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { TX_CHAIN_ID, TollBlockedError, TollUnavailableError, checkTx, chainIdOf, paidUrlOf, } from "./check.js";
|
|
2
|
+
/** the effective policy: explicit options win, then the strict preset, then the defaults */
|
|
3
|
+
export function resolvePolicy(o = {}) {
|
|
4
|
+
const strict = o.strict === true;
|
|
5
|
+
return {
|
|
6
|
+
onWarn: o.onWarn ?? (strict ? "block" : "block-high"),
|
|
7
|
+
onUnavailable: o.onUnavailable ?? (strict ? "block" : "allow"),
|
|
8
|
+
onUnsupported: o.onUnsupported ?? (strict ? "block" : "allow"),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const HIGH = new Set(["high", "critical"]);
|
|
12
|
+
/** true when any warning is high or critical */
|
|
13
|
+
export const hasHighWarning = (a) => (a.warnings ?? []).some((w) => HIGH.has(String(w?.severity).toLowerCase()));
|
|
2
14
|
const say = (o, msg) => {
|
|
3
15
|
if (o.logger === false)
|
|
4
16
|
return;
|
|
@@ -19,9 +31,49 @@ async function report(o, r) {
|
|
|
19
31
|
say(o, `onReport threw: ${e?.message ?? e}`);
|
|
20
32
|
}
|
|
21
33
|
}
|
|
34
|
+
const SIGN_RPC = new Set(["personal_sign", "eth_sign", "eth_signTypedData_v4"]);
|
|
35
|
+
/** the typed data a request signs: its typedData, or an eth_signTypedData_v4 request's params[1] (JSON or object) */
|
|
36
|
+
function typedOf(input) {
|
|
37
|
+
if (input.typedData)
|
|
38
|
+
return input.typedData;
|
|
39
|
+
if (input.rpc?.method !== "eth_signTypedData_v4")
|
|
40
|
+
return null;
|
|
41
|
+
let td = input.rpc.params?.[1];
|
|
42
|
+
if (typeof td === "string") {
|
|
43
|
+
try {
|
|
44
|
+
td = JSON.parse(td);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return td && typeof td === "object" ? td : null;
|
|
51
|
+
}
|
|
52
|
+
/** true for a signature request (typed data, a message, a signing RPC) rather than a transaction or an x402 payment */
|
|
53
|
+
export const isSignature = (input) => !!input && !input.x402 && (!!input.typedData || !!input.message || SIGN_RPC.has(String(input.rpc?.method)));
|
|
54
|
+
/** true for eth_sign: it signs any 32 bytes (a transaction hash included), so it fails closed when Toll cannot answer */
|
|
55
|
+
export const isEthSign = (input) => input?.message?.kind === "eth_sign" || input?.rpc?.method === "eth_sign";
|
|
56
|
+
/** the other chain a typed-data signature names (its domain chainId), or null (Robinhood Chain, none, or an x402 payment) */
|
|
57
|
+
function otherChainSignature(input) {
|
|
58
|
+
if (input.x402)
|
|
59
|
+
return null;
|
|
60
|
+
const c = typedOf(input)?.domain?.chainId;
|
|
61
|
+
const cid = c === undefined || c === null ? null : chainIdOf(c);
|
|
62
|
+
return cid !== null && cid !== TX_CHAIN_ID ? cid : null;
|
|
63
|
+
}
|
|
64
|
+
/** Toll's 400 for a request on another chain ("... chainId N is not read ...") */
|
|
65
|
+
const OTHER_CHAIN_400 = /chainId \d+ is not read/;
|
|
22
66
|
/** why Toll would not read this request, or null when it does */
|
|
23
67
|
export function unsupportedWhy(input) {
|
|
24
68
|
const tx = input.to !== undefined || input.data !== undefined;
|
|
69
|
+
const auths = !!input.authorizationList && input.authorizationList.length > 0;
|
|
70
|
+
if (!tx && auths) {
|
|
71
|
+
const cid = input.chainId === undefined ? null : chainIdOf(input.chainId);
|
|
72
|
+
if (input.chainId !== undefined && cid === null)
|
|
73
|
+
return "the chain id could not be read";
|
|
74
|
+
if (cid !== null && cid !== TX_CHAIN_ID)
|
|
75
|
+
return `chain ${cid} is not read (Robinhood Chain ${TX_CHAIN_ID} only for now)`;
|
|
76
|
+
}
|
|
25
77
|
if (tx) {
|
|
26
78
|
if (input.to === undefined || input.to === null)
|
|
27
79
|
return "a contract deployment (no `to`) is not read";
|
|
@@ -31,13 +83,10 @@ export function unsupportedWhy(input) {
|
|
|
31
83
|
if (cid !== null && cid !== TX_CHAIN_ID)
|
|
32
84
|
return `chain ${cid} is not read (Robinhood Chain ${TX_CHAIN_ID} only for now)`;
|
|
33
85
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return `a signature for chain ${cid} is not read (Robinhood Chain ${TX_CHAIN_ID} only for now)`;
|
|
39
|
-
}
|
|
40
|
-
if (!tx && !input.typedData && !input.x402)
|
|
86
|
+
const other = otherChainSignature(input);
|
|
87
|
+
if (other !== null)
|
|
88
|
+
return `a signature for chain ${other} is not read (Robinhood Chain ${TX_CHAIN_ID} only for now)`;
|
|
89
|
+
if (!tx && !auths && !input.typedData && !input.x402 && !input.message && !input.rpc && !input.userOp)
|
|
41
90
|
return "nothing to check";
|
|
42
91
|
return null;
|
|
43
92
|
}
|
|
@@ -56,12 +105,16 @@ export async function enforce(kind, build, o = {}) {
|
|
|
56
105
|
return unavailable(kind, null, err, o);
|
|
57
106
|
}
|
|
58
107
|
const why = unsupportedWhy(input);
|
|
108
|
+
// a signature for another chain is not "unsupported -> allow": an off-chain signature Toll cannot read is refused
|
|
109
|
+
if (why && isSignature(input) && otherChainSignature(input) !== null && o.onUnsupported !== "allow") {
|
|
110
|
+
return refuse(kind, input, "signature-other-chain", `Not signed: ${why}. A signature Toll cannot read could hand over anything; onUnsupported: "allow" lets it through unchecked.`, o);
|
|
111
|
+
}
|
|
59
112
|
if (why) {
|
|
60
|
-
const blocked = o.onUnsupported === "block";
|
|
113
|
+
const blocked = resolvePolicy(o).onUnsupported === "block";
|
|
61
114
|
const r = { kind, input, assessment: null, decision: blocked ? "blocked" : "allowed", basis: "unsupported", paidUrl: null };
|
|
62
115
|
await report(o, r);
|
|
63
116
|
if (blocked)
|
|
64
|
-
throw new TollUnavailableError(`Not checked by Toll: ${why}. onUnsupported is "block".`, { reason: "unsupported" });
|
|
117
|
+
throw new TollUnavailableError(`Not checked by Toll: ${why}. onUnsupported is "block"${o.strict === true && o.onUnsupported === undefined ? " (strict)" : ""}.`, { reason: "unsupported" });
|
|
65
118
|
return r;
|
|
66
119
|
}
|
|
67
120
|
let a;
|
|
@@ -70,6 +123,10 @@ export async function enforce(kind, build, o = {}) {
|
|
|
70
123
|
}
|
|
71
124
|
catch (e) {
|
|
72
125
|
const err = e instanceof TollUnavailableError ? e : new TollUnavailableError(`Toll's check failed: ${e?.message ?? e}`, { reason: "network", cause: e });
|
|
126
|
+
// Toll's 400 for a signature on another chain: blocked, never "unsupported -> allow"
|
|
127
|
+
if (err.reason === "bad-request" && isSignature(input) && OTHER_CHAIN_400.test(err.message) && o.onUnsupported !== "allow") {
|
|
128
|
+
return refuse(kind, input, "signature-other-chain", `Not signed: Toll does not read a signature for another chain (${err.message}). onUnsupported: "allow" lets it through unchecked.`, o);
|
|
129
|
+
}
|
|
73
130
|
return unavailable(kind, input, err, o);
|
|
74
131
|
}
|
|
75
132
|
const paidUrl = paidUrlOf(a);
|
|
@@ -90,8 +147,8 @@ export async function enforce(kind, build, o = {}) {
|
|
|
90
147
|
throw new TollBlockedError(a, "BLOCK");
|
|
91
148
|
}
|
|
92
149
|
// WARN, and anything that is not one of the three actions: never treated as an ALLOW
|
|
93
|
-
const p = o.onWarn
|
|
94
|
-
const ok = p === "allow" ? true : p === "block" ? false : (await p(a)) === true;
|
|
150
|
+
const p = resolvePolicy(o).onWarn;
|
|
151
|
+
const ok = p === "allow" ? true : p === "block" ? false : p === "block-high" ? !hasHighWarning(a) : (await p(a)) === true;
|
|
95
152
|
if (ok) {
|
|
96
153
|
say(o, `WARN (${kind}), going ahead: ${a.warnings.map((w) => `${w.code}: ${w.message}`).join(" | ")}${paidUrl ? ` Paid, no daily limit: ${paidUrl}` : ""}`);
|
|
97
154
|
return done("allowed", "WARN");
|
|
@@ -100,7 +157,8 @@ export async function enforce(kind, build, o = {}) {
|
|
|
100
157
|
throw new TollBlockedError(a, "WARN");
|
|
101
158
|
}
|
|
102
159
|
async function unavailable(kind, input, err, o) {
|
|
103
|
-
|
|
160
|
+
// eth_sign signs any 32 bytes (a transaction hash included): it always fails closed
|
|
161
|
+
const blocked = resolvePolicy(o).onUnavailable === "block" || isEthSign(input);
|
|
104
162
|
const r = { kind, input, assessment: null, decision: blocked ? "blocked" : "allowed", basis: "unavailable", paidUrl: err.paidUrl, error: err };
|
|
105
163
|
await report(o, r);
|
|
106
164
|
if (blocked)
|
|
@@ -117,12 +175,53 @@ export async function refuse(kind, input, code, message, o = {}) {
|
|
|
117
175
|
await report(o, { kind, input, assessment: a, decision: "blocked", basis: "BLOCK", paidUrl: null });
|
|
118
176
|
throw new TollBlockedError(a, "BLOCK");
|
|
119
177
|
}
|
|
120
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* The onAuthorization policy for an EIP-7702 authorization. Resolves true when Toll checked the whole request (the
|
|
180
|
+
* "check" default: the caller asks nothing more), false when "allow" let it through unchecked (the caller still checks
|
|
181
|
+
* the rest of the request); throws when it is blocked. `input` is the request with its authorizationList, or a function
|
|
182
|
+
* that builds it; under "check", null is blocked (nothing to send).
|
|
183
|
+
*/
|
|
121
184
|
export async function gateAuthorization(kind, where, input, o = {}) {
|
|
122
|
-
|
|
185
|
+
const p = o.onAuthorization ?? "check";
|
|
186
|
+
const shown = typeof input === "function" ? null : input;
|
|
187
|
+
if (p === "allow") {
|
|
123
188
|
say(o, `EIP-7702 authorization (${where}) NOT checked, going ahead (onAuthorization "allow")`);
|
|
124
|
-
await report(o, { kind, input, assessment: null, decision: "allowed", basis: "unsupported", paidUrl: null });
|
|
125
|
-
return;
|
|
189
|
+
await report(o, { kind, input: shown, assessment: null, decision: "allowed", basis: "unsupported", paidUrl: null });
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const what = `${where} carries an EIP-7702 authorization, which sets this wallet's code to another contract that can then move everything it holds.`;
|
|
193
|
+
if (p === "block" || input === null) {
|
|
194
|
+
return refuse(kind, shown, "eip7702-authorization", `${what} ${p === "block" ? 'onAuthorization is "block"' : "It could not be read for Toll's check"}; onAuthorization: "allow" lets it through unchecked.`, o);
|
|
195
|
+
}
|
|
196
|
+
const build = typeof input === "function" ? input : () => input;
|
|
197
|
+
try {
|
|
198
|
+
// fail closed: an authorization Toll could not read is not signed
|
|
199
|
+
await enforce(kind, build, { ...o, onUnavailable: "block", onUnsupported: "block" });
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
if (!(e instanceof TollUnavailableError))
|
|
203
|
+
throw e;
|
|
204
|
+
return refuse(kind, null, "eip7702-authorization", `${what} Toll could not read it (${e.message}), so it is not signed; onAuthorization: "allow" lets it through unchecked.`, o);
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A batch (EIP-5792 sendCalls, wallet_sendCalls, a smart account's calls): each call is checked as its own transaction
|
|
210
|
+
* from the batch's sender, in order; the first one blocked stops the batch. Reports carry kind "calls".
|
|
211
|
+
*/
|
|
212
|
+
export async function enforceCalls(build, o = {}) {
|
|
213
|
+
let calls;
|
|
214
|
+
try {
|
|
215
|
+
calls = await build();
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
const err = e instanceof TollUnavailableError ? e : new TollUnavailableError(`The batch could not be read for Toll's check: ${e?.message ?? e}`, { reason: "encode", cause: e });
|
|
219
|
+
return [await unavailable("calls", null, err, o)];
|
|
126
220
|
}
|
|
127
|
-
|
|
221
|
+
if (!calls.length)
|
|
222
|
+
return [await enforce("calls", () => ({}), o)];
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const c of calls)
|
|
225
|
+
out.push(await enforce("calls", () => c, o));
|
|
226
|
+
return out;
|
|
128
227
|
}
|
package/dist/wallets.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { type Hex } from "./check.js";
|
|
|
6
6
|
import { type GuardOptions } from "./policy.js";
|
|
7
7
|
export interface ViemGuardOptions extends GuardOptions {
|
|
8
8
|
/**
|
|
9
|
-
* Encodes writeContract calldata. Default: viem's own encodeFunctionData, loaded on first
|
|
10
|
-
* wherever a viem
|
|
9
|
+
* Encodes writeContract (and contract-call batch) calldata. Default: viem's own encodeFunctionData, loaded on first
|
|
10
|
+
* use (viem is installed wherever a viem client is).
|
|
11
11
|
*/
|
|
12
12
|
encodeFunctionData?: (args: {
|
|
13
13
|
abi: unknown;
|
|
@@ -16,27 +16,36 @@ export interface ViemGuardOptions extends GuardOptions {
|
|
|
16
16
|
}) => Hex | string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Wraps a viem WalletClient: sendTransaction, writeContract, signTransaction
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Wraps a viem WalletClient. Asks Toll first: sendTransaction, writeContract, signTransaction, signTypedData,
|
|
20
|
+
* signMessage, sendCalls (EIP-5792, each call), signAuthorization, and request() for eth_sendTransaction,
|
|
21
|
+
* eth_signTransaction, personal_sign, eth_sign, eth_signTypedData_v4 and wallet_sendCalls. An EIP-7702 authorization
|
|
22
|
+
* goes to onAuthorization (default "check"). Wrap last: `client.extend(...)` on the wrapped client returns an unguarded
|
|
23
|
+
* client, and the account's own sign methods (client.account.sign*) are not wrapped.
|
|
23
24
|
*/
|
|
24
25
|
export declare function guardViem<T extends object>(client: T, opts?: ViemGuardOptions): T;
|
|
26
|
+
/**
|
|
27
|
+
* Wraps a viem bundler client (account abstraction): sendUserOperation and prepareUserOperation ask Toll first.
|
|
28
|
+
* `calls` are checked one by one as transactions from the smart account's address; a ready `callData` is sent as a
|
|
29
|
+
* user operation with the account's EntryPoint. An EIP-7702 `authorization` goes to onAuthorization.
|
|
30
|
+
*/
|
|
31
|
+
export declare function guardSmartAccount<T extends object>(client: T, opts?: ViemGuardOptions): T;
|
|
25
32
|
/** the EIP-712 primary type: the one type no other type refers to (as ethers' TypedDataEncoder.getPrimaryType) */
|
|
26
33
|
export declare function primaryTypeOf(types: Record<string, Array<{
|
|
27
34
|
name: string;
|
|
28
35
|
type: string;
|
|
29
36
|
}>>): string;
|
|
30
37
|
/**
|
|
31
|
-
* Wraps an ethers v6 Signer: sendTransaction, signTransaction and
|
|
32
|
-
* authorization (a transaction's authorizationList, or authorize()) goes to onAuthorization
|
|
33
|
-
* `signer.connect(provider)` returns an unguarded signer: wrap the connected one.
|
|
38
|
+
* Wraps an ethers v6 Signer: sendTransaction, signTransaction, signTypedData, signMessage and authorize() ask Toll
|
|
39
|
+
* first. An EIP-7702 authorization (a transaction's authorizationList, or authorize()) goes to onAuthorization
|
|
40
|
+
* (default "check"). `signer.connect(provider)` returns an unguarded signer: wrap the connected one.
|
|
34
41
|
*/
|
|
35
42
|
export declare function guardEthers<T extends object>(signer: T, opts?: GuardOptions): T;
|
|
36
43
|
/** true for a viem WalletClient (has `extend` and `request`), false for an ethers Signer */
|
|
37
44
|
export declare const isViemClient: (x: unknown) => boolean;
|
|
45
|
+
/** true for a viem bundler client (account abstraction): it has sendUserOperation */
|
|
46
|
+
export declare const isBundlerClient: (x: unknown) => boolean;
|
|
38
47
|
/**
|
|
39
|
-
* Puts Toll's pre-sign check in front of a wallet
|
|
40
|
-
* the same type and methods.
|
|
48
|
+
* Puts Toll's pre-sign check in front of a wallet: a viem bundler client (smart account), a viem WalletClient or an
|
|
49
|
+
* ethers v6 Signer; the wrapped wallet has the same type and methods.
|
|
41
50
|
*/
|
|
42
51
|
export declare function guard<T extends object>(wallet: T, opts?: ViemGuardOptions): T;
|
package/dist/wallets.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The wallet wrappers. Duck-typed: viem and ethers are optional peer dependencies, never imported at load time.
|
|
3
3
|
* The wrapper is a Proxy: every other method and field of the client passes through unchanged.
|
|
4
4
|
*/
|
|
5
|
-
import { isAddress } from "./check.js";
|
|
6
|
-
import { enforce, gateAuthorization } from "./policy.js";
|
|
5
|
+
import { chainIdOf, isAddress } from "./check.js";
|
|
6
|
+
import { enforce, enforceCalls, gateAuthorization } from "./policy.js";
|
|
7
7
|
/** wraps an object's methods; everything else passes through, bound to the original */
|
|
8
8
|
function proxy(target, wrapped) {
|
|
9
9
|
return new Proxy(target, {
|
|
@@ -26,6 +26,32 @@ const addrOf = (account) => {
|
|
|
26
26
|
const a = account?.address;
|
|
27
27
|
return isAddress(a) ? a : undefined;
|
|
28
28
|
};
|
|
29
|
+
/** a chain id as a number when it can be read, else as given (the check then calls it unreadable) */
|
|
30
|
+
const chainOut = (v) => (v === undefined || v === null ? undefined : (chainIdOf(v) ?? v));
|
|
31
|
+
const HEXDIGITS = "0123456789abcdef";
|
|
32
|
+
const bytesHex = (b) => {
|
|
33
|
+
let s = "0x";
|
|
34
|
+
for (const x of b)
|
|
35
|
+
s += HEXDIGITS[x >> 4] + HEXDIGITS[x & 15];
|
|
36
|
+
return s;
|
|
37
|
+
};
|
|
38
|
+
/** a text message as the bytes a wallet signs (UTF-8), hex */
|
|
39
|
+
const utf8Hex = (s) => bytesHex(new TextEncoder().encode(s));
|
|
40
|
+
/** the request without its authorizationList (what is checked when onAuthorization "allow" let the list through) */
|
|
41
|
+
const withoutAuths = (i) => {
|
|
42
|
+
const { authorizationList: _drop, ...rest } = i;
|
|
43
|
+
return rest;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* One transaction: when it carries an authorizationList, onAuthorization decides first ("check": one Toll check of the
|
|
47
|
+
* whole transaction with its list, fail-closed; "block": refused; "allow": the list is let through and the rest is
|
|
48
|
+
* checked as usual).
|
|
49
|
+
*/
|
|
50
|
+
async function gateTx(where, build, auths, o) {
|
|
51
|
+
if (auths && (await gateAuthorization("transaction", where, build, o)))
|
|
52
|
+
return;
|
|
53
|
+
await enforce("transaction", async () => withoutAuths(await build()), o);
|
|
54
|
+
}
|
|
29
55
|
/* ------------------------------------------------------------------ viem */
|
|
30
56
|
let viemEncode = null;
|
|
31
57
|
async function loadViemEncode() {
|
|
@@ -38,11 +64,38 @@ async function loadViemEncode() {
|
|
|
38
64
|
viemEncode = m.encodeFunctionData;
|
|
39
65
|
return viemEncode;
|
|
40
66
|
}
|
|
67
|
+
/** a batch's calls as transactions: {to, data?, value?} or a contract call {to | address, abi, functionName, args} */
|
|
68
|
+
async function callsOf(calls, chainId, from, o) {
|
|
69
|
+
if (!Array.isArray(calls))
|
|
70
|
+
throw new Error("calls must be an array");
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const raw of calls) {
|
|
73
|
+
const k = (raw ?? {});
|
|
74
|
+
let data = typeof k.data === "string" ? k.data : "0x";
|
|
75
|
+
if (k.abi !== undefined && typeof k.functionName === "string") {
|
|
76
|
+
const enc = o.encodeFunctionData ?? (await loadViemEncode());
|
|
77
|
+
data = String(enc({ abi: k.abi, functionName: k.functionName, args: k.args }));
|
|
78
|
+
if (typeof k.dataSuffix === "string")
|
|
79
|
+
data += k.dataSuffix.replace(/^0x/, "");
|
|
80
|
+
}
|
|
81
|
+
out.push({ chainId: chainOut(chainId), from, to: k.to ?? k.address ?? undefined, data, value: k.value ?? 0n });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** a viem / EIP-7702 authorization request as Toll reads it: one entry, the signer, the chain */
|
|
86
|
+
function authInput(a, chainId, from) {
|
|
87
|
+
const address = a?.contractAddress ?? a?.address;
|
|
88
|
+
const entry = { address, chainId: chainOut(a?.chainId ?? chainId) };
|
|
89
|
+
if (a?.nonce !== undefined && a?.nonce !== null)
|
|
90
|
+
entry.nonce = a.nonce;
|
|
91
|
+
return { ...(chainId !== undefined && chainId !== null ? { chainId: chainOut(chainId) } : {}), from, authorizationList: [entry] };
|
|
92
|
+
}
|
|
41
93
|
/**
|
|
42
|
-
* Wraps a viem WalletClient: sendTransaction, writeContract, signTransaction
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
94
|
+
* Wraps a viem WalletClient. Asks Toll first: sendTransaction, writeContract, signTransaction, signTypedData,
|
|
95
|
+
* signMessage, sendCalls (EIP-5792, each call), signAuthorization, and request() for eth_sendTransaction,
|
|
96
|
+
* eth_signTransaction, personal_sign, eth_sign, eth_signTypedData_v4 and wallet_sendCalls. An EIP-7702 authorization
|
|
97
|
+
* goes to onAuthorization (default "check"). Wrap last: `client.extend(...)` on the wrapped client returns an unguarded
|
|
98
|
+
* client, and the account's own sign methods (client.account.sign*) are not wrapped.
|
|
46
99
|
*/
|
|
47
100
|
export function guardViem(client, opts = {}) {
|
|
48
101
|
const c = client;
|
|
@@ -54,50 +107,164 @@ export function guardViem(client, opts = {}) {
|
|
|
54
107
|
to: args?.to ?? undefined,
|
|
55
108
|
data: data ?? "0x",
|
|
56
109
|
value: args?.value ?? 0n,
|
|
110
|
+
...(hasAuthorizations(args) ? { authorizationList: args.authorizationList } : {}),
|
|
57
111
|
});
|
|
112
|
+
const origin = opts.origin !== undefined ? { origin: opts.origin } : {};
|
|
58
113
|
const wrapped = {};
|
|
59
|
-
|
|
60
|
-
|
|
114
|
+
for (const m of ["sendTransaction", "signTransaction"]) {
|
|
115
|
+
if (typeof c[m] !== "function")
|
|
116
|
+
continue;
|
|
117
|
+
wrapped[m] = async (args, ...rest) => {
|
|
61
118
|
// a deployment (no `to`) goes to the unsupported policy
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await enforce("transaction", () => txInput(args, args?.data), opts);
|
|
65
|
-
return c.sendTransaction(args, ...rest);
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (typeof c.signTransaction === "function") {
|
|
69
|
-
wrapped.signTransaction = async (args, ...rest) => {
|
|
70
|
-
if (hasAuthorizations(args))
|
|
71
|
-
await gateAuthorization("transaction", "signTransaction", txInput(args, args?.data), opts);
|
|
72
|
-
await enforce("transaction", () => txInput(args, args?.data), opts);
|
|
73
|
-
return c.signTransaction(args, ...rest);
|
|
119
|
+
await gateTx(m, () => txInput(args, args?.data), hasAuthorizations(args), opts);
|
|
120
|
+
return c[m](args, ...rest);
|
|
74
121
|
};
|
|
75
122
|
}
|
|
76
123
|
if (typeof c.writeContract === "function") {
|
|
77
124
|
wrapped.writeContract = async (args, ...rest) => {
|
|
78
|
-
|
|
79
|
-
await gateAuthorization("transaction", "writeContract", null, opts);
|
|
80
|
-
await enforce("transaction", async () => {
|
|
125
|
+
await gateTx("writeContract", async () => {
|
|
81
126
|
const enc = opts.encodeFunctionData ?? (await loadViemEncode());
|
|
82
127
|
let data = String(enc({ abi: args.abi, functionName: args.functionName, args: args.args }));
|
|
83
128
|
if (typeof args.dataSuffix === "string")
|
|
84
129
|
data += args.dataSuffix.replace(/^0x/, "");
|
|
85
130
|
return txInput({ ...args, to: args.address }, data);
|
|
86
|
-
}, opts);
|
|
131
|
+
}, hasAuthorizations(args), opts);
|
|
87
132
|
return c.writeContract(args, ...rest);
|
|
88
133
|
};
|
|
89
134
|
}
|
|
90
135
|
if (typeof c.signTypedData === "function") {
|
|
91
136
|
wrapped.signTypedData = async (args, ...rest) => {
|
|
92
137
|
const td = { domain: args?.domain ?? {}, types: args?.types ?? {}, primaryType: args?.primaryType, message: args?.message ?? {} };
|
|
93
|
-
await enforce("typedData", () => ({ typedData: td }), opts);
|
|
138
|
+
await enforce("typedData", () => ({ typedData: td, ...origin }), opts);
|
|
94
139
|
return c.signTypedData(args, ...rest);
|
|
95
140
|
};
|
|
96
141
|
}
|
|
142
|
+
if (typeof c.signMessage === "function") {
|
|
143
|
+
wrapped.signMessage = async (args, ...rest) => {
|
|
144
|
+
await enforce("message", () => {
|
|
145
|
+
const m = args?.message;
|
|
146
|
+
const raw = m && typeof m === "object" ? m.raw : undefined;
|
|
147
|
+
const data = typeof m === "string" ? utf8Hex(m) : typeof raw === "string" ? raw : raw instanceof Uint8Array ? bytesHex(raw) : null;
|
|
148
|
+
if (data === null)
|
|
149
|
+
throw new Error("signMessage: message must be a string or {raw}");
|
|
150
|
+
const from = fromOf(args);
|
|
151
|
+
return { message: { kind: "personal_sign", data, ...(from ? { from } : {}) }, ...origin };
|
|
152
|
+
}, opts);
|
|
153
|
+
return c.signMessage(args, ...rest);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (typeof c.sendCalls === "function") {
|
|
157
|
+
wrapped.sendCalls = async (args, ...rest) => {
|
|
158
|
+
await enforceCalls(() => callsOf(args?.calls, chainOf(args), fromOf(args), opts), opts);
|
|
159
|
+
return c.sendCalls(args, ...rest);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
97
162
|
if (typeof c.signAuthorization === "function") {
|
|
98
|
-
wrapped.signAuthorization = async (...
|
|
99
|
-
await gateAuthorization("transaction", "signAuthorization",
|
|
100
|
-
return c.signAuthorization(...
|
|
163
|
+
wrapped.signAuthorization = async (args, ...rest) => {
|
|
164
|
+
await gateAuthorization("transaction", "signAuthorization", () => authInput(args, c.chain?.id, fromOf(args)), opts);
|
|
165
|
+
return c.signAuthorization(args, ...rest);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (typeof c.request === "function") {
|
|
169
|
+
wrapped.request = async (args, ...rest) => {
|
|
170
|
+
await checkRequest(args, c, fromOf({}), opts);
|
|
171
|
+
return c.request(args, ...rest);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return proxy(client, wrapped);
|
|
175
|
+
}
|
|
176
|
+
/** the wallet RPC methods that sign: each asks Toll first; every other method passes through unchecked */
|
|
177
|
+
async function checkRequest(args, c, from, o) {
|
|
178
|
+
const method = args?.method;
|
|
179
|
+
const params = Array.isArray(args?.params) ? args.params : [];
|
|
180
|
+
const origin = o.origin !== undefined ? { origin: o.origin } : {};
|
|
181
|
+
switch (method) {
|
|
182
|
+
case "personal_sign":
|
|
183
|
+
case "eth_sign":
|
|
184
|
+
await enforce("message", () => ({ rpc: { method, params }, ...origin }), o);
|
|
185
|
+
return;
|
|
186
|
+
case "eth_signTypedData_v4":
|
|
187
|
+
await enforce("typedData", () => ({ rpc: { method, params }, ...origin }), o);
|
|
188
|
+
return;
|
|
189
|
+
case "eth_sendTransaction":
|
|
190
|
+
case "eth_signTransaction": {
|
|
191
|
+
const tx = (params[0] ?? {});
|
|
192
|
+
const build = () => ({
|
|
193
|
+
chainId: chainOut(tx.chainId ?? c.chain?.id),
|
|
194
|
+
from: isAddress(tx.from) ? tx.from : from,
|
|
195
|
+
to: tx.to ?? undefined,
|
|
196
|
+
data: typeof tx.data === "string" ? tx.data : typeof tx.input === "string" ? tx.input : "0x",
|
|
197
|
+
value: tx.value ?? 0,
|
|
198
|
+
...(hasAuthorizations(tx) ? { authorizationList: tx.authorizationList } : {}),
|
|
199
|
+
});
|
|
200
|
+
await gateTx(method, build, hasAuthorizations(tx), o);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
case "wallet_sendCalls": {
|
|
204
|
+
const p = (params[0] ?? {});
|
|
205
|
+
await enforceCalls(() => callsOf(p.calls, p.chainId ?? c.chain?.id, isAddress(p.from) ? p.from : from, o), o);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/* ------------------------------------------------------------------ ERC-4337 smart accounts (viem account abstraction) */
|
|
213
|
+
/** the user-operation fields passed on to Toll as given (bigints become decimal strings) */
|
|
214
|
+
const USEROP_FIELDS = [
|
|
215
|
+
"factory",
|
|
216
|
+
"factoryData",
|
|
217
|
+
"initCode",
|
|
218
|
+
"paymaster",
|
|
219
|
+
"paymasterData",
|
|
220
|
+
"paymasterAndData",
|
|
221
|
+
"paymasterVerificationGasLimit",
|
|
222
|
+
"paymasterPostOpGasLimit",
|
|
223
|
+
"callGasLimit",
|
|
224
|
+
"verificationGasLimit",
|
|
225
|
+
"preVerificationGas",
|
|
226
|
+
"maxFeePerGas",
|
|
227
|
+
"maxPriorityFeePerGas",
|
|
228
|
+
"signature",
|
|
229
|
+
];
|
|
230
|
+
/**
|
|
231
|
+
* Wraps a viem bundler client (account abstraction): sendUserOperation and prepareUserOperation ask Toll first.
|
|
232
|
+
* `calls` are checked one by one as transactions from the smart account's address; a ready `callData` is sent as a
|
|
233
|
+
* user operation with the account's EntryPoint. An EIP-7702 `authorization` goes to onAuthorization.
|
|
234
|
+
*/
|
|
235
|
+
export function guardSmartAccount(client, opts = {}) {
|
|
236
|
+
const c = client;
|
|
237
|
+
const check = async (where, args) => {
|
|
238
|
+
const acct = args?.account ?? c.account;
|
|
239
|
+
const sender = addrOf(acct);
|
|
240
|
+
const chainId = args?.chain?.id ?? c.chain?.id;
|
|
241
|
+
if (args?.authorization)
|
|
242
|
+
await gateAuthorization("userOp", where, () => authInput(args.authorization, chainId, sender), opts);
|
|
243
|
+
if (Array.isArray(args?.calls)) {
|
|
244
|
+
await enforceCalls(() => callsOf(args.calls, chainId, sender, opts), opts);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
await enforce("userOp", async () => {
|
|
248
|
+
if (typeof args?.callData !== "string")
|
|
249
|
+
return {};
|
|
250
|
+
let nonce = args.nonce;
|
|
251
|
+
if ((nonce === undefined || nonce === null) && typeof acct?.getNonce === "function")
|
|
252
|
+
nonce = await acct.getNonce();
|
|
253
|
+
const op = { sender: isAddress(args.sender) ? args.sender : sender, nonce: nonce ?? 0n, callData: args.callData };
|
|
254
|
+
for (const k of USEROP_FIELDS)
|
|
255
|
+
if (args[k] !== undefined && args[k] !== null)
|
|
256
|
+
op[k] = args[k];
|
|
257
|
+
const ep = acct?.entryPoint?.address ?? args.entryPointAddress;
|
|
258
|
+
return { userOp: op, ...(isAddress(ep) ? { entryPoint: ep } : {}) };
|
|
259
|
+
}, opts);
|
|
260
|
+
};
|
|
261
|
+
const wrapped = {};
|
|
262
|
+
for (const m of ["sendUserOperation", "prepareUserOperation"]) {
|
|
263
|
+
if (typeof c[m] !== "function")
|
|
264
|
+
continue;
|
|
265
|
+
wrapped[m] = async (args, ...rest) => {
|
|
266
|
+
await check(m, args);
|
|
267
|
+
return c[m](args, ...rest);
|
|
101
268
|
};
|
|
102
269
|
}
|
|
103
270
|
return proxy(client, wrapped);
|
|
@@ -115,6 +282,10 @@ export function primaryTypeOf(types) {
|
|
|
115
282
|
throw new Error(roots.length ? `ambiguous primary type: ${roots.join(", ")}` : "no primary type");
|
|
116
283
|
return roots[0];
|
|
117
284
|
}
|
|
285
|
+
async function ethersChainId(signer) {
|
|
286
|
+
const net = typeof signer.provider?.getNetwork === "function" ? await signer.provider.getNetwork() : null;
|
|
287
|
+
return net?.chainId ?? undefined;
|
|
288
|
+
}
|
|
118
289
|
async function ethersTx(signer, tx) {
|
|
119
290
|
let to = tx?.to;
|
|
120
291
|
if (to && typeof to === "object" && typeof to.getAddress === "function")
|
|
@@ -127,10 +298,8 @@ async function ethersTx(signer, tx) {
|
|
|
127
298
|
to = r;
|
|
128
299
|
}
|
|
129
300
|
let chainId = tx?.chainId;
|
|
130
|
-
if (chainId === undefined || chainId === null)
|
|
131
|
-
|
|
132
|
-
chainId = net?.chainId ?? undefined;
|
|
133
|
-
}
|
|
301
|
+
if (chainId === undefined || chainId === null)
|
|
302
|
+
chainId = await ethersChainId(signer);
|
|
134
303
|
let from = tx?.from;
|
|
135
304
|
if (from && typeof from === "object" && typeof from.getAddress === "function")
|
|
136
305
|
from = await from.getAddress();
|
|
@@ -142,58 +311,74 @@ async function ethersTx(signer, tx) {
|
|
|
142
311
|
to: (to ?? undefined),
|
|
143
312
|
data: typeof tx?.data === "string" ? tx.data : "0x",
|
|
144
313
|
value: tx?.value ?? 0n,
|
|
314
|
+
...(hasAuthorizations(tx) ? { authorizationList: tx.authorizationList } : {}),
|
|
145
315
|
};
|
|
146
316
|
}
|
|
147
317
|
/**
|
|
148
|
-
* Wraps an ethers v6 Signer: sendTransaction, signTransaction and
|
|
149
|
-
* authorization (a transaction's authorizationList, or authorize()) goes to onAuthorization
|
|
150
|
-
* `signer.connect(provider)` returns an unguarded signer: wrap the connected one.
|
|
318
|
+
* Wraps an ethers v6 Signer: sendTransaction, signTransaction, signTypedData, signMessage and authorize() ask Toll
|
|
319
|
+
* first. An EIP-7702 authorization (a transaction's authorizationList, or authorize()) goes to onAuthorization
|
|
320
|
+
* (default "check"). `signer.connect(provider)` returns an unguarded signer: wrap the connected one.
|
|
151
321
|
*/
|
|
152
322
|
export function guardEthers(signer, opts = {}) {
|
|
153
323
|
const s = signer;
|
|
324
|
+
const origin = opts.origin !== undefined ? { origin: opts.origin } : {};
|
|
154
325
|
const wrapped = {};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await
|
|
160
|
-
return s
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (typeof s.signTransaction === "function") {
|
|
164
|
-
wrapped.signTransaction = async (tx, ...rest) => {
|
|
165
|
-
if (hasAuthorizations(tx))
|
|
166
|
-
await gateAuthorization("transaction", "signTransaction", null, opts);
|
|
167
|
-
await enforce("transaction", () => ethersTx(s, tx), opts);
|
|
168
|
-
return s.signTransaction(tx, ...rest);
|
|
326
|
+
for (const m of ["sendTransaction", "signTransaction"]) {
|
|
327
|
+
if (typeof s[m] !== "function")
|
|
328
|
+
continue;
|
|
329
|
+
wrapped[m] = async (tx, ...rest) => {
|
|
330
|
+
await gateTx(m, () => ethersTx(s, tx), hasAuthorizations(tx), opts);
|
|
331
|
+
return s[m](tx, ...rest);
|
|
169
332
|
};
|
|
170
333
|
}
|
|
171
334
|
if (typeof s.signTypedData === "function") {
|
|
172
335
|
wrapped.signTypedData = async (domain, types, value, ...rest) => {
|
|
173
|
-
await enforce("typedData", () => ({ typedData: { domain: domain ?? {}, types: types ?? {}, primaryType: primaryTypeOf(types ?? {}), message: value ?? {} } }), opts);
|
|
336
|
+
await enforce("typedData", () => ({ typedData: { domain: domain ?? {}, types: types ?? {}, primaryType: primaryTypeOf(types ?? {}), message: value ?? {} }, ...origin }), opts);
|
|
174
337
|
return s.signTypedData(domain, types, value, ...rest);
|
|
175
338
|
};
|
|
176
339
|
}
|
|
340
|
+
if (typeof s.signMessage === "function") {
|
|
341
|
+
wrapped.signMessage = async (message, ...rest) => {
|
|
342
|
+
await enforce("message", async () => {
|
|
343
|
+
// ethers signs a string as its UTF-8 bytes, even one that looks like hex
|
|
344
|
+
const data = typeof message === "string" ? utf8Hex(message) : message instanceof Uint8Array ? bytesHex(message) : null;
|
|
345
|
+
if (data === null)
|
|
346
|
+
throw new Error("signMessage: message must be a string or bytes");
|
|
347
|
+
const from = typeof s.getAddress === "function" ? await s.getAddress() : undefined;
|
|
348
|
+
return { message: { kind: "personal_sign", data, ...(isAddress(from) ? { from } : {}) }, ...origin };
|
|
349
|
+
}, opts);
|
|
350
|
+
return s.signMessage(message, ...rest);
|
|
351
|
+
};
|
|
352
|
+
}
|
|
177
353
|
if (typeof s.authorize === "function") {
|
|
178
|
-
wrapped.authorize = async (...
|
|
179
|
-
await gateAuthorization("transaction", "authorize",
|
|
180
|
-
|
|
354
|
+
wrapped.authorize = async (auth, ...rest) => {
|
|
355
|
+
await gateAuthorization("transaction", "authorize", async () => {
|
|
356
|
+
const chainId = chainOut(await ethersChainId(s));
|
|
357
|
+
const from = typeof s.getAddress === "function" ? await s.getAddress() : undefined;
|
|
358
|
+
return authInput(auth ?? {}, chainId, isAddress(from) ? from : undefined);
|
|
359
|
+
}, opts);
|
|
360
|
+
return s.authorize(auth, ...rest);
|
|
181
361
|
};
|
|
182
362
|
}
|
|
183
363
|
return proxy(signer, wrapped);
|
|
184
364
|
}
|
|
185
|
-
/* ------------------------------------------------------------------
|
|
365
|
+
/* ------------------------------------------------------------------ any */
|
|
186
366
|
/** true for a viem WalletClient (has `extend` and `request`), false for an ethers Signer */
|
|
187
367
|
export const isViemClient = (x) => !!x && typeof x.extend === "function" && typeof x.request === "function";
|
|
368
|
+
/** true for a viem bundler client (account abstraction): it has sendUserOperation */
|
|
369
|
+
export const isBundlerClient = (x) => !!x && typeof x.sendUserOperation === "function";
|
|
188
370
|
/**
|
|
189
|
-
* Puts Toll's pre-sign check in front of a wallet
|
|
190
|
-
* the same type and methods.
|
|
371
|
+
* Puts Toll's pre-sign check in front of a wallet: a viem bundler client (smart account), a viem WalletClient or an
|
|
372
|
+
* ethers v6 Signer; the wrapped wallet has the same type and methods.
|
|
191
373
|
*/
|
|
192
374
|
export function guard(wallet, opts = {}) {
|
|
375
|
+
// a bundler client is also a viem client: it is detected first
|
|
376
|
+
if (isBundlerClient(wallet))
|
|
377
|
+
return guardSmartAccount(wallet, opts);
|
|
193
378
|
if (isViemClient(wallet))
|
|
194
379
|
return guardViem(wallet, opts);
|
|
195
380
|
if (typeof wallet.getAddress === "function" && (typeof wallet.sendTransaction === "function" || typeof wallet.signTypedData === "function")) {
|
|
196
381
|
return guardEthers(wallet, opts);
|
|
197
382
|
}
|
|
198
|
-
throw new TypeError("guard(): pass a viem WalletClient or an ethers v6 Signer (or use guardViem / guardEthers).");
|
|
383
|
+
throw new TypeError("guard(): pass a viem WalletClient, a viem bundler client or an ethers v6 Signer (or use guardViem / guardSmartAccount / guardEthers).");
|
|
199
384
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollxyz/guard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Toll's live pre-sign check in front of an AI agent's wallet: viem, ethers and x402 fetch wrappers that ask Toll (ALLOW / WARN / BLOCK, ERC-8265) before anything is signed on Robinhood Chain. Zero runtime dependencies.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Toll's live pre-sign check in front of an AI agent's wallet: viem, viem smart-account, ethers and x402 fetch wrappers that ask Toll (ALLOW / WARN / BLOCK, ERC-8265) before anything is signed on Robinhood Chain: transactions, messages, typed data, batches, user operations, EIP-7702 authorizations. Zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Toll",
|
|
7
7
|
"type": "module",
|