@tollxyz/guard 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -3
- package/dist/check.d.ts +134 -0
- package/dist/check.js +175 -0
- package/dist/erc8470.d.ts +93 -0
- package/dist/erc8470.js +193 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/policy.d.ts +45 -0
- package/dist/policy.js +110 -0
- package/dist/wallets.d.ts +39 -0
- package/dist/wallets.js +169 -0
- package/dist/x402.d.ts +14 -0
- package/dist/x402.js +75 -0
- package/package.json +69 -6
package/dist/erc8470.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export const OBS_REGISTRY = "0x730e3c356299c9203cb042f6c0a9274aeabf9af3";
|
|
2
|
+
/** Toll's detector wallet (the engine that records observations) */
|
|
3
|
+
export const OBS_DETECTOR = "0x02593AbFf31f4E8eC1D5c2F297a347C51ef06E5c";
|
|
4
|
+
export const OBS_CHAIN_ID = 4663;
|
|
5
|
+
export const RH_RPC = "https://rpc.mainnet.chain.robinhood.com";
|
|
6
|
+
/** type(uint256).max */
|
|
7
|
+
export const NOT_READ = (1n << 256n) - 1n;
|
|
8
|
+
/** selectors, from the compiled LaunchObservations.json methodIdentifiers */
|
|
9
|
+
const SEL = {
|
|
10
|
+
getObservation: "f5fec8b1",
|
|
11
|
+
latestByToken: "8c9b1620",
|
|
12
|
+
latestFinding: "cf5fd50d",
|
|
13
|
+
};
|
|
14
|
+
/** every kind the ERC defines plus Toll's own, by name: keccak256 of the namespaced string, and its unit */
|
|
15
|
+
export const FINDING_KINDS = {
|
|
16
|
+
"first-block-share": { id: "0xf1503018582646f41619eefcdeaae1c1b9ad5e71e1a74f59a7c177cd5d8a1b0c", unit: "bps" },
|
|
17
|
+
"early-buyer-share": { id: "0xde1880e1c55491741e8806f8091354331ccfae6d260de5624fa7b5131f8ab19a", unit: "bps" },
|
|
18
|
+
"creator-prior-launches": { id: "0xb559eb50996235dcc907f3c528e2b0bd21669b438572d50973405b9394544c38", unit: "count" },
|
|
19
|
+
"funding-linked-share": { id: "0xd0f99f8b1f2648a01d4d94c4ac89e81b3f3036b775a02e9d840391fece5b51d0", unit: "bps" },
|
|
20
|
+
"product-label": { id: "0x2af827bba451d4ec3e3a25936a27478e62bf16a8046dfec8f9369f5b9f139345", unit: "label" },
|
|
21
|
+
"text-flags": { id: "0x69c611869dc6d3ce968978b3e0a2a3b7d975d9ab462081222ae451a444230bc5", unit: "mask" },
|
|
22
|
+
"xyz.t0ll.dev-sold-share": { id: "0x0b9baa9f583b056e93eb0a650daaafff3b7aa27fbe9f4243b91f5f4337fbc10a", unit: "bps" },
|
|
23
|
+
"signal.deployerSupplyShare": { id: "0x36da7198a2ed7b9d71cd98db4049dce4bc9fe964562e0d5f3085f915405fa318", unit: "bps" },
|
|
24
|
+
"signal.insiderAllocationShare": { id: "0x03c04aaf168c10242870103dfdc5e913904f5c02b2be34091db637984b72eb72", unit: "bps" },
|
|
25
|
+
"signal.sniperConcentration": { id: "0x3f22e0f440671a38c44563d087b0027456ed5018d1cb21c5e36837aaa21eaf2c", unit: "bps" },
|
|
26
|
+
"signal.lpLockedShare": { id: "0x3c8cfdf5cbd8671de60b2c3e64bd5d69438136cd74cdc340287ba5fd7a836755", unit: "bps" },
|
|
27
|
+
"signal.lpLockRemaining": { id: "0x17c1b81f88b7beaaf5cd4b6fed97887360c13fe178a9f826064c0522c3d69ab2", unit: "seconds" },
|
|
28
|
+
"signal.liquidityRemoved": { id: "0xd0d754e325d16e378c48160ce2a439bc8587980fb9b2f69aa6a4037973ad2f71", unit: "bps" },
|
|
29
|
+
"signal.deployerSellRatio": { id: "0x5dbad0bcd30c444d847c6ae8571340fddcb77cad4ef280497f4196032ba201a4", unit: "bps" },
|
|
30
|
+
"signal.proceedsWithdrawnShare": { id: "0xcf325865161b29692c7e7d30708d3a6b2a7f788ff1859f76720127464e2a9dad", unit: "bps" },
|
|
31
|
+
"signal.privilegedPowers": { id: "0xb6b77a84953f300885ac1de83ad6cb7a3683d87ffbad4dd9963eb7bfc207a6f5", unit: "mask" },
|
|
32
|
+
"signal.priorUpheldClaims": { id: "0x1b8f9350bd49cd3504abfddf158e8726187ddd9b8060a017918c300020e9254d", unit: "count" },
|
|
33
|
+
"signal.supplyInflation": { id: "0xf695c94870a4cb86b0539718a4b7e89ed00257f15b2b7b33829785224bb25cd8", unit: "bps" },
|
|
34
|
+
"signal.washTradeRatio": { id: "0x79cbc6923e1ea8a086a7713aada26f70a0665a99e796b1fa083c2e3046d9abc0", unit: "bps" },
|
|
35
|
+
};
|
|
36
|
+
const BY_ID = new Map(Object.entries(FINDING_KINDS).map(([name, k]) => [k.id, { name, unit: k.unit }]));
|
|
37
|
+
const LEANS = ["none", "clear", "careful", "against"];
|
|
38
|
+
const KEY_KINDS = ["none", "erc8376", "token", "erc8462-venue"];
|
|
39
|
+
/* ------------------------------------------------------------------ ABI, by hand */
|
|
40
|
+
const ADDR = /^0x[0-9a-fA-F]{40}$/;
|
|
41
|
+
const B32 = /^0x[0-9a-fA-F]{64}$/;
|
|
42
|
+
const word = (n) => {
|
|
43
|
+
if (n < 0n || n > NOT_READ)
|
|
44
|
+
throw new RangeError("uint256 out of range");
|
|
45
|
+
return n.toString(16).padStart(64, "0");
|
|
46
|
+
};
|
|
47
|
+
const addrWord = (a) => {
|
|
48
|
+
if (!ADDR.test(a))
|
|
49
|
+
throw new TypeError(`not an address: ${a}`);
|
|
50
|
+
return a.slice(2).toLowerCase().padStart(64, "0");
|
|
51
|
+
};
|
|
52
|
+
export const encodeLatestByToken = (chainId, token, detector) => `0x${SEL.latestByToken}${word(chainId)}${addrWord(token)}${addrWord(detector)}`;
|
|
53
|
+
export const encodeGetObservation = (id) => `0x${SEL.getObservation}${word(id)}`;
|
|
54
|
+
export const encodeLatestFinding = (chainId, token, detector, kind, param) => {
|
|
55
|
+
if (!B32.test(kind))
|
|
56
|
+
throw new TypeError(`kind must be bytes32 hex: ${kind}`);
|
|
57
|
+
if (param < 0n || param >= 1n << 64n)
|
|
58
|
+
throw new RangeError("param must fit uint64");
|
|
59
|
+
return `0x${SEL.latestFinding}${word(chainId)}${addrWord(token)}${addrWord(detector)}${kind.slice(2).toLowerCase()}${word(param)}`;
|
|
60
|
+
};
|
|
61
|
+
/** a reader over 32-byte words of ABI-encoded return data */
|
|
62
|
+
class Reader {
|
|
63
|
+
hex;
|
|
64
|
+
constructor(data) {
|
|
65
|
+
if (!/^0x([0-9a-fA-F]{2})*$/.test(data))
|
|
66
|
+
throw new TypeError("return data is not hex");
|
|
67
|
+
this.hex = data.slice(2);
|
|
68
|
+
}
|
|
69
|
+
/** the 32-byte word at a byte offset */
|
|
70
|
+
w(byteOff) {
|
|
71
|
+
const s = this.hex.slice(byteOff * 2, byteOff * 2 + 64);
|
|
72
|
+
if (s.length !== 64)
|
|
73
|
+
throw new RangeError(`return data too short at byte ${byteOff}`);
|
|
74
|
+
return s;
|
|
75
|
+
}
|
|
76
|
+
uint(byteOff) {
|
|
77
|
+
return BigInt("0x" + this.w(byteOff));
|
|
78
|
+
}
|
|
79
|
+
num(byteOff, max = 2 ** 32) {
|
|
80
|
+
const n = this.uint(byteOff);
|
|
81
|
+
if (n > BigInt(max))
|
|
82
|
+
throw new RangeError(`value too large at byte ${byteOff}`);
|
|
83
|
+
return Number(n);
|
|
84
|
+
}
|
|
85
|
+
addr(byteOff) {
|
|
86
|
+
return "0x" + this.w(byteOff).slice(24);
|
|
87
|
+
}
|
|
88
|
+
b32(byteOff) {
|
|
89
|
+
return "0x" + this.w(byteOff);
|
|
90
|
+
}
|
|
91
|
+
bool(byteOff) {
|
|
92
|
+
return this.uint(byteOff) !== 0n;
|
|
93
|
+
}
|
|
94
|
+
string(byteOff) {
|
|
95
|
+
const len = this.num(byteOff, 1 << 20);
|
|
96
|
+
const s = this.hex.slice((byteOff + 32) * 2, (byteOff + 32 + len) * 2);
|
|
97
|
+
if (s.length !== len * 2)
|
|
98
|
+
throw new RangeError("string runs past the return data");
|
|
99
|
+
const bytes = new Uint8Array(len);
|
|
100
|
+
for (let i = 0; i < len; i++)
|
|
101
|
+
bytes[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
102
|
+
return new TextDecoder().decode(bytes);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const leanOf = (n) => LEANS[n] ?? "none";
|
|
106
|
+
function finding(kind, value, param, lean) {
|
|
107
|
+
const k = BY_ID.get(kind.toLowerCase());
|
|
108
|
+
const notRead = value === NOT_READ;
|
|
109
|
+
return { kind, name: k?.name ?? null, unit: k?.unit ?? null, value: notRead ? null : value, notRead, param, lean: leanOf(lean) };
|
|
110
|
+
}
|
|
111
|
+
/** decodes getObservation(uint256)'s return: (Observation, Finding[]) */
|
|
112
|
+
export function decodeGetObservation(data, id) {
|
|
113
|
+
const r = new Reader(data);
|
|
114
|
+
const o = r.num(0, 1 << 24);
|
|
115
|
+
const f = r.num(32, 1 << 24);
|
|
116
|
+
const at = (i) => o + i * 32;
|
|
117
|
+
const keyKind = r.num(at(3), 255);
|
|
118
|
+
const obs = {
|
|
119
|
+
id,
|
|
120
|
+
launchKey: r.b32(at(0)),
|
|
121
|
+
chainId: r.uint(at(1)),
|
|
122
|
+
token: r.addr(at(2)),
|
|
123
|
+
keyKind: KEY_KINDS[keyKind] ?? "none",
|
|
124
|
+
observedBlock: r.uint(at(4)),
|
|
125
|
+
hashChecked: r.bool(at(5)),
|
|
126
|
+
detector: r.addr(at(6)),
|
|
127
|
+
recordedAt: r.uint(at(7)),
|
|
128
|
+
identityRegistry: r.addr(at(8)),
|
|
129
|
+
tally: { against: r.num(at(9), 65535), careful: r.num(at(10), 65535), clear: r.num(at(11), 65535) },
|
|
130
|
+
agentId: r.uint(at(12)),
|
|
131
|
+
supersedes: r.uint(at(13)),
|
|
132
|
+
reportHash: r.b32(at(14)),
|
|
133
|
+
evidenceRoot: r.b32(at(15)),
|
|
134
|
+
declarationId: r.b32(at(16)),
|
|
135
|
+
reportURI: r.string(o + r.num(at(17), 1 << 24)),
|
|
136
|
+
findings: [],
|
|
137
|
+
};
|
|
138
|
+
const n = r.num(f, 64);
|
|
139
|
+
for (let i = 0; i < n; i++) {
|
|
140
|
+
const b = f + 32 + i * 128;
|
|
141
|
+
obs.findings.push(finding(r.b32(b), r.uint(b + 32), r.uint(b + 64), r.num(b + 96, 255)));
|
|
142
|
+
}
|
|
143
|
+
return obs;
|
|
144
|
+
}
|
|
145
|
+
/* ------------------------------------------------------------------ RPC */
|
|
146
|
+
async function ethCall(to, data, o) {
|
|
147
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
148
|
+
const ctl = new AbortController();
|
|
149
|
+
const timer = setTimeout(() => ctl.abort(), o.timeoutMs ?? 10_000);
|
|
150
|
+
try {
|
|
151
|
+
const res = await f(o.rpcUrl ?? RH_RPC, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: { "content-type": "application/json" },
|
|
154
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to, data }, "latest"] }),
|
|
155
|
+
signal: ctl.signal,
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok)
|
|
158
|
+
throw new Error(`RPC answered HTTP ${res.status}`);
|
|
159
|
+
const j = (await res.json());
|
|
160
|
+
if (j.error)
|
|
161
|
+
throw new Error(`eth_call failed: ${j.error.message ?? "error"}`);
|
|
162
|
+
if (typeof j.result !== "string")
|
|
163
|
+
throw new Error("eth_call returned no result");
|
|
164
|
+
return j.result;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const kindId = (kind) => FINDING_KINDS[kind]?.id ?? kind;
|
|
171
|
+
/**
|
|
172
|
+
* Toll's latest ERC-8470 observation of a token: latestByToken(chainId, token, detector), then getObservation(id).
|
|
173
|
+
* null when the detector has no observation of the token (id 0). Throws on an RPC failure (a failed read is not
|
|
174
|
+
* "no observation").
|
|
175
|
+
*/
|
|
176
|
+
export async function read8470(token, opts = {}) {
|
|
177
|
+
const registry = opts.registry ?? OBS_REGISTRY;
|
|
178
|
+
const id = BigInt(await ethCall(registry, encodeLatestByToken(BigInt(opts.chainId ?? OBS_CHAIN_ID), token, opts.detector ?? OBS_DETECTOR), opts));
|
|
179
|
+
if (id === 0n)
|
|
180
|
+
return null;
|
|
181
|
+
return decodeGetObservation(await ethCall(registry, encodeGetObservation(id), opts), id);
|
|
182
|
+
}
|
|
183
|
+
/** decodes latestFinding's fixed-size return (uint256, uint64, uint64, uint256, uint8) */
|
|
184
|
+
export function decodeLatestFinding(data) {
|
|
185
|
+
const r = new Reader(data);
|
|
186
|
+
const value = r.uint(96);
|
|
187
|
+
return { observationId: r.uint(0), observedBlock: r.uint(32), recordedAt: r.uint(64), value: value === NOT_READ ? null : value, notRead: value === NOT_READ, lean: leanOf(r.num(128, 255)) };
|
|
188
|
+
}
|
|
189
|
+
/** One finding of Toll's latest observation of a token: latestFinding(chainId, token, detector, kind, param). */
|
|
190
|
+
export async function latestFinding8470(token, kind, param = 0, opts = {}) {
|
|
191
|
+
const data = encodeLatestFinding(BigInt(opts.chainId ?? OBS_CHAIN_ID), token, opts.detector ?? OBS_DETECTOR, kindId(kind), BigInt(param));
|
|
192
|
+
return decodeLatestFinding(await ethCall(opts.registry ?? OBS_REGISTRY, data, opts));
|
|
193
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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 } from "./policy.js";
|
|
4
|
+
export type { GuardOptions, GuardReport, GuardKind, WarnPolicy, BlockPolicy } from "./policy.js";
|
|
5
|
+
export { guard, guardViem, guardEthers, isViemClient, primaryTypeOf } from "./wallets.js";
|
|
6
|
+
export type { ViemGuardOptions } from "./wallets.js";
|
|
7
|
+
export { guardX402Fetch, readPaymentRequired } from "./x402.js";
|
|
8
|
+
export type { X402GuardOptions } from "./x402.js";
|
|
9
|
+
export { read8470, latestFinding8470, decodeGetObservation, decodeLatestFinding, encodeGetObservation, encodeLatestByToken, encodeLatestFinding, FINDING_KINDS, NOT_READ, OBS_REGISTRY, OBS_DETECTOR, OBS_CHAIN_ID, RH_RPC, } from "./erc8470.js";
|
|
10
|
+
export type { Observation8470, Finding8470, LatestFinding8470, Read8470Options, Lean, KeyKind, Unit } from "./erc8470.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +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 } from "./policy.js";
|
|
3
|
+
export { guard, guardViem, guardEthers, isViemClient, primaryTypeOf } from "./wallets.js";
|
|
4
|
+
export { guardX402Fetch, readPaymentRequired } from "./x402.js";
|
|
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
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TollUnavailableError, type CheckInput, type CheckOptions, type RiskAssessment } from "./check.js";
|
|
2
|
+
export type WarnPolicy = "allow" | "block" | ((assessment: RiskAssessment) => boolean | Promise<boolean>);
|
|
3
|
+
export type BlockPolicy = "throw" | ((assessment: RiskAssessment) => boolean | Promise<boolean>);
|
|
4
|
+
export type GuardKind = "transaction" | "typedData" | "x402";
|
|
5
|
+
export interface GuardReport {
|
|
6
|
+
kind: GuardKind;
|
|
7
|
+
/** what was sent to Toll (null when nothing was sent) */
|
|
8
|
+
input: CheckInput | null;
|
|
9
|
+
/** Toll's answer; null when Toll was unavailable or the request was not checked */
|
|
10
|
+
assessment: RiskAssessment | null;
|
|
11
|
+
/** what the guard did */
|
|
12
|
+
decision: "allowed" | "blocked";
|
|
13
|
+
/** why: Toll's action, "unavailable" (Toll could not answer) or "unsupported" (not a request Toll reads) */
|
|
14
|
+
basis: "ALLOW" | "WARN" | "BLOCK" | "unavailable" | "unsupported";
|
|
15
|
+
/** the paid route (no daily limit) when this caller's free checks are used */
|
|
16
|
+
paidUrl: string | null;
|
|
17
|
+
error?: TollUnavailableError;
|
|
18
|
+
}
|
|
19
|
+
export interface GuardOptions extends CheckOptions {
|
|
20
|
+
/** WARN: "allow" (default, reported and logged), "block", or a function; true lets it through */
|
|
21
|
+
onWarn?: WarnPolicy;
|
|
22
|
+
/** BLOCK: "throw" (default, TollBlockedError), or a function (e.g. ask a human); true lets it through */
|
|
23
|
+
onBlock?: BlockPolicy;
|
|
24
|
+
/**
|
|
25
|
+
* 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).
|
|
27
|
+
*/
|
|
28
|
+
onUnavailable?: "allow" | "block";
|
|
29
|
+
/** a request Toll does not read (another chain, a contract deploy): "allow" (default) or "block" */
|
|
30
|
+
onUnsupported?: "allow" | "block";
|
|
31
|
+
/** every decision, with Toll's answer; errors thrown here are caught and logged */
|
|
32
|
+
onReport?: (report: GuardReport) => void | Promise<void>;
|
|
33
|
+
/** where WARN / unavailable lines go (default console.warn); false = silent */
|
|
34
|
+
logger?: ((message: string) => void) | false;
|
|
35
|
+
/** the seller's ERC-8004 agent id for x402 payments (or a function of the URL) */
|
|
36
|
+
agentId?: number | string | ((url: string) => number | string | undefined | null);
|
|
37
|
+
}
|
|
38
|
+
/** why Toll would not read this request, or null when it does */
|
|
39
|
+
export declare function unsupportedWhy(input: CheckInput): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Runs Toll's check on one request and applies the policy. Resolves with the report when the request may go ahead;
|
|
42
|
+
* throws TollBlockedError (BLOCK, or WARN under a blocking policy) or TollUnavailableError (fail-closed).
|
|
43
|
+
* `build` may throw (e.g. calldata that could not be encoded): that counts as unavailable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function enforce(kind: GuardKind, build: () => CheckInput | Promise<CheckInput>, o?: GuardOptions): Promise<GuardReport>;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { TX_CHAIN_ID, TollBlockedError, TollUnavailableError, checkTx, chainIdOf, paidUrlOf, } from "./check.js";
|
|
2
|
+
const say = (o, msg) => {
|
|
3
|
+
if (o.logger === false)
|
|
4
|
+
return;
|
|
5
|
+
try {
|
|
6
|
+
(o.logger ?? console.warn)(`[toll-guard] ${msg}`);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
/* a logger must never stop a signature */
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
async function report(o, r) {
|
|
13
|
+
if (!o.onReport)
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
await o.onReport(r);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
say(o, `onReport threw: ${e?.message ?? e}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** why Toll would not read this request, or null when it does */
|
|
23
|
+
export function unsupportedWhy(input) {
|
|
24
|
+
const tx = input.to !== undefined || input.data !== undefined;
|
|
25
|
+
if (tx) {
|
|
26
|
+
if (input.to === undefined || input.to === null)
|
|
27
|
+
return "a contract deployment (no `to`) is not read";
|
|
28
|
+
const cid = input.chainId === undefined ? null : chainIdOf(input.chainId);
|
|
29
|
+
if (input.chainId !== undefined && cid === null)
|
|
30
|
+
return "the chain id could not be read";
|
|
31
|
+
if (cid !== null && cid !== TX_CHAIN_ID)
|
|
32
|
+
return `chain ${cid} is not read (Robinhood Chain ${TX_CHAIN_ID} only for now)`;
|
|
33
|
+
}
|
|
34
|
+
if (input.typedData && !input.x402) {
|
|
35
|
+
const c = input.typedData.domain?.chainId;
|
|
36
|
+
const cid = c === undefined || c === null ? null : chainIdOf(c);
|
|
37
|
+
if (cid !== null && cid !== TX_CHAIN_ID)
|
|
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)
|
|
41
|
+
return "nothing to check";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Runs Toll's check on one request and applies the policy. Resolves with the report when the request may go ahead;
|
|
46
|
+
* throws TollBlockedError (BLOCK, or WARN under a blocking policy) or TollUnavailableError (fail-closed).
|
|
47
|
+
* `build` may throw (e.g. calldata that could not be encoded): that counts as unavailable.
|
|
48
|
+
*/
|
|
49
|
+
export async function enforce(kind, build, o = {}) {
|
|
50
|
+
let input;
|
|
51
|
+
try {
|
|
52
|
+
input = await build();
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
const err = e instanceof TollUnavailableError ? e : new TollUnavailableError(`The request could not be read for Toll's check: ${e?.message ?? e}`, { reason: "encode", cause: e });
|
|
56
|
+
return unavailable(kind, null, err, o);
|
|
57
|
+
}
|
|
58
|
+
const why = unsupportedWhy(input);
|
|
59
|
+
if (why) {
|
|
60
|
+
const blocked = o.onUnsupported === "block";
|
|
61
|
+
const r = { kind, input, assessment: null, decision: blocked ? "blocked" : "allowed", basis: "unsupported", paidUrl: null };
|
|
62
|
+
await report(o, r);
|
|
63
|
+
if (blocked)
|
|
64
|
+
throw new TollUnavailableError(`Not checked by Toll: ${why}. onUnsupported is "block".`, { reason: "unsupported" });
|
|
65
|
+
return r;
|
|
66
|
+
}
|
|
67
|
+
let a;
|
|
68
|
+
try {
|
|
69
|
+
a = await checkTx(input, o);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
const err = e instanceof TollUnavailableError ? e : new TollUnavailableError(`Toll's check failed: ${e?.message ?? e}`, { reason: "network", cause: e });
|
|
73
|
+
return unavailable(kind, input, err, o);
|
|
74
|
+
}
|
|
75
|
+
const paidUrl = paidUrlOf(a);
|
|
76
|
+
const done = async (decision, basis) => {
|
|
77
|
+
const r = { kind, input, assessment: a, decision, basis, paidUrl };
|
|
78
|
+
await report(o, r);
|
|
79
|
+
return r;
|
|
80
|
+
};
|
|
81
|
+
if (a.action === "ALLOW")
|
|
82
|
+
return done("allowed", "ALLOW");
|
|
83
|
+
if (a.action === "BLOCK") {
|
|
84
|
+
const p = o.onBlock ?? "throw";
|
|
85
|
+
if (typeof p === "function" && (await p(a)) === true) {
|
|
86
|
+
say(o, `BLOCK let through by onBlock: ${a.warnings.map((w) => w.code).join(", ")}`);
|
|
87
|
+
return done("allowed", "BLOCK");
|
|
88
|
+
}
|
|
89
|
+
await done("blocked", "BLOCK");
|
|
90
|
+
throw new TollBlockedError(a, "BLOCK");
|
|
91
|
+
}
|
|
92
|
+
// WARN, and anything that is not one of the three actions: never treated as an ALLOW
|
|
93
|
+
const p = o.onWarn ?? "allow";
|
|
94
|
+
const ok = p === "allow" ? true : p === "block" ? false : (await p(a)) === true;
|
|
95
|
+
if (ok) {
|
|
96
|
+
say(o, `WARN (${kind}), going ahead: ${a.warnings.map((w) => `${w.code}: ${w.message}`).join(" | ")}${paidUrl ? ` Paid, no daily limit: ${paidUrl}` : ""}`);
|
|
97
|
+
return done("allowed", "WARN");
|
|
98
|
+
}
|
|
99
|
+
await done("blocked", "WARN");
|
|
100
|
+
throw new TollBlockedError(a, "WARN");
|
|
101
|
+
}
|
|
102
|
+
async function unavailable(kind, input, err, o) {
|
|
103
|
+
const blocked = o.onUnavailable === "block";
|
|
104
|
+
const r = { kind, input, assessment: null, decision: blocked ? "blocked" : "allowed", basis: "unavailable", paidUrl: err.paidUrl, error: err };
|
|
105
|
+
await report(o, r);
|
|
106
|
+
if (blocked)
|
|
107
|
+
throw err;
|
|
108
|
+
say(o, `Toll unavailable, NOT checked, going ahead (onUnavailable "allow"): ${err.message}`);
|
|
109
|
+
return r;
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wallet wrappers. Duck-typed: viem and ethers are optional peer dependencies, never imported at load time.
|
|
3
|
+
* The wrapper is a Proxy: every other method and field of the client passes through unchanged.
|
|
4
|
+
*/
|
|
5
|
+
import { type Hex } from "./check.js";
|
|
6
|
+
import { type GuardOptions } from "./policy.js";
|
|
7
|
+
export interface ViemGuardOptions extends GuardOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Encodes writeContract calldata. Default: viem's own encodeFunctionData, loaded on first use (viem is installed
|
|
10
|
+
* wherever a viem WalletClient is).
|
|
11
|
+
*/
|
|
12
|
+
encodeFunctionData?: (args: {
|
|
13
|
+
abi: unknown;
|
|
14
|
+
functionName: string;
|
|
15
|
+
args?: readonly unknown[];
|
|
16
|
+
}) => Hex | string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a viem WalletClient: sendTransaction, writeContract, signTransaction and signTypedData ask Toll first.
|
|
20
|
+
* Wrap last: `client.extend(...)` on the wrapped client returns an unguarded client.
|
|
21
|
+
*/
|
|
22
|
+
export declare function guardViem<T extends object>(client: T, opts?: ViemGuardOptions): T;
|
|
23
|
+
/** the EIP-712 primary type: the one type no other type refers to (as ethers' TypedDataEncoder.getPrimaryType) */
|
|
24
|
+
export declare function primaryTypeOf(types: Record<string, Array<{
|
|
25
|
+
name: string;
|
|
26
|
+
type: string;
|
|
27
|
+
}>>): string;
|
|
28
|
+
/**
|
|
29
|
+
* Wraps an ethers v6 Signer: sendTransaction, signTransaction and signTypedData ask Toll first.
|
|
30
|
+
* `signer.connect(provider)` returns an unguarded signer: wrap the connected one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function guardEthers<T extends object>(signer: T, opts?: GuardOptions): T;
|
|
33
|
+
/** true for a viem WalletClient (has `extend` and `request`), false for an ethers Signer */
|
|
34
|
+
export declare const isViemClient: (x: unknown) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Puts Toll's pre-sign check in front of a wallet. A viem WalletClient or an ethers v6 Signer; the wrapped wallet has
|
|
37
|
+
* the same type and methods.
|
|
38
|
+
*/
|
|
39
|
+
export declare function guard<T extends object>(wallet: T, opts?: ViemGuardOptions): T;
|
package/dist/wallets.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wallet wrappers. Duck-typed: viem and ethers are optional peer dependencies, never imported at load time.
|
|
3
|
+
* The wrapper is a Proxy: every other method and field of the client passes through unchanged.
|
|
4
|
+
*/
|
|
5
|
+
import { isAddress } from "./check.js";
|
|
6
|
+
import { enforce } from "./policy.js";
|
|
7
|
+
/** wraps an object's methods; everything else passes through, bound to the original */
|
|
8
|
+
function proxy(target, wrapped) {
|
|
9
|
+
return new Proxy(target, {
|
|
10
|
+
get(t, p) {
|
|
11
|
+
if (typeof p === "string" && Object.prototype.hasOwnProperty.call(wrapped, p))
|
|
12
|
+
return wrapped[p];
|
|
13
|
+
const v = Reflect.get(t, p, t);
|
|
14
|
+
return typeof v === "function" ? v.bind(t) : v;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const addrOf = (account) => {
|
|
19
|
+
if (isAddress(account))
|
|
20
|
+
return account;
|
|
21
|
+
const a = account?.address;
|
|
22
|
+
return isAddress(a) ? a : undefined;
|
|
23
|
+
};
|
|
24
|
+
/* ------------------------------------------------------------------ viem */
|
|
25
|
+
let viemEncode = null;
|
|
26
|
+
async function loadViemEncode() {
|
|
27
|
+
if (viemEncode)
|
|
28
|
+
return viemEncode;
|
|
29
|
+
const name = "viem";
|
|
30
|
+
const m = (await import(name));
|
|
31
|
+
if (typeof m.encodeFunctionData !== "function")
|
|
32
|
+
throw new Error("viem's encodeFunctionData was not found");
|
|
33
|
+
viemEncode = m.encodeFunctionData;
|
|
34
|
+
return viemEncode;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wraps a viem WalletClient: sendTransaction, writeContract, signTransaction and signTypedData ask Toll first.
|
|
38
|
+
* Wrap last: `client.extend(...)` on the wrapped client returns an unguarded client.
|
|
39
|
+
*/
|
|
40
|
+
export function guardViem(client, opts = {}) {
|
|
41
|
+
const c = client;
|
|
42
|
+
const chainOf = (args) => (args?.chain === null ? undefined : (args?.chain?.id ?? c.chain?.id));
|
|
43
|
+
const fromOf = (args) => addrOf(args?.account ?? c.account);
|
|
44
|
+
const txInput = (args, data) => ({
|
|
45
|
+
chainId: chainOf(args),
|
|
46
|
+
from: fromOf(args),
|
|
47
|
+
to: args?.to ?? undefined,
|
|
48
|
+
data: data ?? "0x",
|
|
49
|
+
value: args?.value ?? 0n,
|
|
50
|
+
});
|
|
51
|
+
const wrapped = {};
|
|
52
|
+
if (typeof c.sendTransaction === "function") {
|
|
53
|
+
wrapped.sendTransaction = async (args, ...rest) => {
|
|
54
|
+
// a deployment (no `to`) goes to the unsupported policy
|
|
55
|
+
await enforce("transaction", () => txInput(args, args?.data), opts);
|
|
56
|
+
return c.sendTransaction(args, ...rest);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (typeof c.signTransaction === "function") {
|
|
60
|
+
wrapped.signTransaction = async (args, ...rest) => {
|
|
61
|
+
await enforce("transaction", () => txInput(args, args?.data), opts);
|
|
62
|
+
return c.signTransaction(args, ...rest);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (typeof c.writeContract === "function") {
|
|
66
|
+
wrapped.writeContract = async (args, ...rest) => {
|
|
67
|
+
await enforce("transaction", async () => {
|
|
68
|
+
const enc = opts.encodeFunctionData ?? (await loadViemEncode());
|
|
69
|
+
let data = String(enc({ abi: args.abi, functionName: args.functionName, args: args.args }));
|
|
70
|
+
if (typeof args.dataSuffix === "string")
|
|
71
|
+
data += args.dataSuffix.replace(/^0x/, "");
|
|
72
|
+
return txInput({ ...args, to: args.address }, data);
|
|
73
|
+
}, opts);
|
|
74
|
+
return c.writeContract(args, ...rest);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (typeof c.signTypedData === "function") {
|
|
78
|
+
wrapped.signTypedData = async (args, ...rest) => {
|
|
79
|
+
const td = { domain: args?.domain ?? {}, types: args?.types ?? {}, primaryType: args?.primaryType, message: args?.message ?? {} };
|
|
80
|
+
await enforce("typedData", () => ({ typedData: td }), opts);
|
|
81
|
+
return c.signTypedData(args, ...rest);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return proxy(client, wrapped);
|
|
85
|
+
}
|
|
86
|
+
/* ------------------------------------------------------------------ ethers v6 */
|
|
87
|
+
/** the EIP-712 primary type: the one type no other type refers to (as ethers' TypedDataEncoder.getPrimaryType) */
|
|
88
|
+
export function primaryTypeOf(types) {
|
|
89
|
+
const names = Object.keys(types).filter((n) => n !== "EIP712Domain");
|
|
90
|
+
const used = new Set();
|
|
91
|
+
for (const n of names)
|
|
92
|
+
for (const f of types[n] ?? [])
|
|
93
|
+
used.add(String(f.type).replace(/(\[\d*\])+$/, ""));
|
|
94
|
+
const roots = names.filter((n) => !used.has(n));
|
|
95
|
+
if (roots.length !== 1)
|
|
96
|
+
throw new Error(roots.length ? `ambiguous primary type: ${roots.join(", ")}` : "no primary type");
|
|
97
|
+
return roots[0];
|
|
98
|
+
}
|
|
99
|
+
async function ethersTx(signer, tx) {
|
|
100
|
+
let to = tx?.to;
|
|
101
|
+
if (to && typeof to === "object" && typeof to.getAddress === "function")
|
|
102
|
+
to = await to.getAddress();
|
|
103
|
+
if (typeof to === "string" && !isAddress(to)) {
|
|
104
|
+
// an ENS name: resolved the way the signer would
|
|
105
|
+
const r = typeof signer.resolveName === "function" ? await signer.resolveName(to) : null;
|
|
106
|
+
if (!isAddress(r))
|
|
107
|
+
throw new Error(`could not resolve ${to}`);
|
|
108
|
+
to = r;
|
|
109
|
+
}
|
|
110
|
+
let chainId = tx?.chainId;
|
|
111
|
+
if (chainId === undefined || chainId === null) {
|
|
112
|
+
const net = typeof signer.provider?.getNetwork === "function" ? await signer.provider.getNetwork() : null;
|
|
113
|
+
chainId = net?.chainId ?? undefined;
|
|
114
|
+
}
|
|
115
|
+
let from = tx?.from;
|
|
116
|
+
if (from && typeof from === "object" && typeof from.getAddress === "function")
|
|
117
|
+
from = await from.getAddress();
|
|
118
|
+
if (!isAddress(from) && typeof signer.getAddress === "function")
|
|
119
|
+
from = await signer.getAddress();
|
|
120
|
+
return {
|
|
121
|
+
chainId: (chainId ?? undefined),
|
|
122
|
+
from: isAddress(from) ? from : undefined,
|
|
123
|
+
to: (to ?? undefined),
|
|
124
|
+
data: typeof tx?.data === "string" ? tx.data : "0x",
|
|
125
|
+
value: tx?.value ?? 0n,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Wraps an ethers v6 Signer: sendTransaction, signTransaction and signTypedData ask Toll first.
|
|
130
|
+
* `signer.connect(provider)` returns an unguarded signer: wrap the connected one.
|
|
131
|
+
*/
|
|
132
|
+
export function guardEthers(signer, opts = {}) {
|
|
133
|
+
const s = signer;
|
|
134
|
+
const wrapped = {};
|
|
135
|
+
if (typeof s.sendTransaction === "function") {
|
|
136
|
+
wrapped.sendTransaction = async (tx, ...rest) => {
|
|
137
|
+
await enforce("transaction", () => ethersTx(s, tx), opts);
|
|
138
|
+
return s.sendTransaction(tx, ...rest);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (typeof s.signTransaction === "function") {
|
|
142
|
+
wrapped.signTransaction = async (tx, ...rest) => {
|
|
143
|
+
await enforce("transaction", () => ethersTx(s, tx), opts);
|
|
144
|
+
return s.signTransaction(tx, ...rest);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (typeof s.signTypedData === "function") {
|
|
148
|
+
wrapped.signTypedData = async (domain, types, value, ...rest) => {
|
|
149
|
+
await enforce("typedData", () => ({ typedData: { domain: domain ?? {}, types: types ?? {}, primaryType: primaryTypeOf(types ?? {}), message: value ?? {} } }), opts);
|
|
150
|
+
return s.signTypedData(domain, types, value, ...rest);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return proxy(signer, wrapped);
|
|
154
|
+
}
|
|
155
|
+
/* ------------------------------------------------------------------ either */
|
|
156
|
+
/** true for a viem WalletClient (has `extend` and `request`), false for an ethers Signer */
|
|
157
|
+
export const isViemClient = (x) => !!x && typeof x.extend === "function" && typeof x.request === "function";
|
|
158
|
+
/**
|
|
159
|
+
* Puts Toll's pre-sign check in front of a wallet. A viem WalletClient or an ethers v6 Signer; the wrapped wallet has
|
|
160
|
+
* the same type and methods.
|
|
161
|
+
*/
|
|
162
|
+
export function guard(wallet, opts = {}) {
|
|
163
|
+
if (isViemClient(wallet))
|
|
164
|
+
return guardViem(wallet, opts);
|
|
165
|
+
if (typeof wallet.getAddress === "function" && (typeof wallet.sendTransaction === "function" || typeof wallet.signTypedData === "function")) {
|
|
166
|
+
return guardEthers(wallet, opts);
|
|
167
|
+
}
|
|
168
|
+
throw new TypeError("guard(): pass a viem WalletClient or an ethers v6 Signer (or use guardViem / guardEthers).");
|
|
169
|
+
}
|
package/dist/x402.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type GuardOptions, type GuardReport } from "./policy.js";
|
|
2
|
+
type AnyFetch = (input: any, init?: any) => Promise<Response>;
|
|
3
|
+
/** the 402's PaymentRequired: v2 from the PAYMENT-REQUIRED header, else v1 from the JSON body; null when neither */
|
|
4
|
+
export declare function readPaymentRequired(res: Response): Promise<Record<string, unknown> | null>;
|
|
5
|
+
export interface X402GuardOptions extends GuardOptions {
|
|
6
|
+
/** called with each 402's report (also passed to onReport) */
|
|
7
|
+
on402?: (report: GuardReport, url: string) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a fetch so every 402 is checked by Toll before an x402 client pays it. A 402 to a request that already
|
|
11
|
+
* carries a payment header passes through (it was checked before it was signed). Non-402 answers pass untouched.
|
|
12
|
+
*/
|
|
13
|
+
export declare function guardX402Fetch<F extends AnyFetch>(fetchLike: F, opts?: X402GuardOptions): F;
|
|
14
|
+
export {};
|
package/dist/x402.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { enforce } from "./policy.js";
|
|
2
|
+
function decodeBase64Json(value) {
|
|
3
|
+
const bin = atob(value.trim());
|
|
4
|
+
return JSON.parse(new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0))));
|
|
5
|
+
}
|
|
6
|
+
/** the 402's PaymentRequired: v2 from the PAYMENT-REQUIRED header, else v1 from the JSON body; null when neither */
|
|
7
|
+
export async function readPaymentRequired(res) {
|
|
8
|
+
const h = res.headers.get("payment-required");
|
|
9
|
+
if (h) {
|
|
10
|
+
try {
|
|
11
|
+
const pr = decodeBase64Json(h);
|
|
12
|
+
if (pr && Array.isArray(pr.accepts) && pr.accepts.length)
|
|
13
|
+
return pr;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
/* fall back to the body */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const body = JSON.parse(await res.clone().text());
|
|
21
|
+
if (body && Array.isArray(body.accepts) && body.accepts.length)
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* not JSON */
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const urlOf = (input) => {
|
|
30
|
+
if (typeof input === "string")
|
|
31
|
+
return input;
|
|
32
|
+
if (input instanceof URL)
|
|
33
|
+
return input.href;
|
|
34
|
+
const u = input?.url;
|
|
35
|
+
return typeof u === "string" ? u : String(input);
|
|
36
|
+
};
|
|
37
|
+
const PAY_HEADERS = ["payment-signature", "x-payment"];
|
|
38
|
+
const hasPayment = (input, init) => {
|
|
39
|
+
const read = (h) => {
|
|
40
|
+
if (!h)
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
const hs = new Headers(h);
|
|
44
|
+
return PAY_HEADERS.some((n) => hs.has(n));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return read(init?.headers) || (typeof Request !== "undefined" && input instanceof Request && read(input.headers));
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Wraps a fetch so every 402 is checked by Toll before an x402 client pays it. A 402 to a request that already
|
|
54
|
+
* carries a payment header passes through (it was checked before it was signed). Non-402 answers pass untouched.
|
|
55
|
+
*/
|
|
56
|
+
export function guardX402Fetch(fetchLike, opts = {}) {
|
|
57
|
+
const toll = { ...opts, fetch: opts.fetch ?? globalThis.fetch?.bind(globalThis) };
|
|
58
|
+
const wrapped = async (input, init) => {
|
|
59
|
+
const res = await fetchLike(input, init);
|
|
60
|
+
if (res.status !== 402 || hasPayment(input, init))
|
|
61
|
+
return res;
|
|
62
|
+
const url = urlOf(input);
|
|
63
|
+
const pr = await readPaymentRequired(res);
|
|
64
|
+
const r = await enforce("x402", () => {
|
|
65
|
+
if (!pr)
|
|
66
|
+
return {}; // nothing readable: the unsupported policy decides
|
|
67
|
+
const x402 = pr.resource || pr.url ? pr : { ...pr, url };
|
|
68
|
+
const id = typeof opts.agentId === "function" ? opts.agentId(url) : opts.agentId;
|
|
69
|
+
return { x402, ...(id !== undefined && id !== null ? { agentId: id } : {}) };
|
|
70
|
+
}, toll);
|
|
71
|
+
opts.on402?.(r, url);
|
|
72
|
+
return res;
|
|
73
|
+
};
|
|
74
|
+
return wrapped;
|
|
75
|
+
}
|