@ethlimo/ens-hooks-release-testing 0.1.11
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/CHANGELOG.md +82 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/contracts/AllParameterPermutationsHookTarget.sol +155 -0
- package/contracts/DataResolver.sol +17 -0
- package/contracts/FunctionCallParserTest.sol +203 -0
- package/contracts/IDataResolver.sol +9 -0
- package/contracts/ZeroParameterHookTarget.sol +19 -0
- package/package.json +69 -0
- package/sbom.spdx.json +1 -0
- package/src/dataurl/constants.ts +19 -0
- package/src/dataurl/encoding.ts +587 -0
- package/src/dataurl/index.ts +164 -0
- package/src/dataurl/trust.ts +39 -0
- package/src/index.ts +49 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Contract, Provider, AbiCoder } from "ethers";
|
|
2
|
+
import {
|
|
3
|
+
DecodedEIP8121Hook,
|
|
4
|
+
extractFunctionName,
|
|
5
|
+
parseParameterTypes,
|
|
6
|
+
parseFunctionCallValues,
|
|
7
|
+
isEIP8121Hook,
|
|
8
|
+
decodeHook
|
|
9
|
+
} from "./encoding.js";
|
|
10
|
+
import { TrustedTargets, verifyTrustedTarget } from "./trust.js";
|
|
11
|
+
|
|
12
|
+
export type ProviderMap = Map<number, Provider>;
|
|
13
|
+
|
|
14
|
+
export interface HookExecutionResult {
|
|
15
|
+
_tag: "HookExecutionResult";
|
|
16
|
+
data: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HookExecutionError {
|
|
20
|
+
_tag: "HookExecutionError";
|
|
21
|
+
error: true;
|
|
22
|
+
message: string;
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ExecutionResult = HookExecutionResult | HookExecutionError;
|
|
27
|
+
|
|
28
|
+
export interface HookValidationResult {
|
|
29
|
+
isValid: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates an EIP-8121 hook.
|
|
35
|
+
* Checks: 0-2 fixed-size primitive parameters, (bytes) return type.
|
|
36
|
+
*/
|
|
37
|
+
export function validateHook(hook: DecodedEIP8121Hook): HookValidationResult {
|
|
38
|
+
try {
|
|
39
|
+
// Validate function signature and parameters
|
|
40
|
+
const params = parseParameterTypes(hook.functionSignature);
|
|
41
|
+
|
|
42
|
+
// Validate parameter count (0-2 allowed)
|
|
43
|
+
if (params.length > 2) {
|
|
44
|
+
return {
|
|
45
|
+
isValid: false,
|
|
46
|
+
error: `Too many parameters: expected 0-2, got ${params.length}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate return type
|
|
51
|
+
if (hook.returnType !== '(bytes)') {
|
|
52
|
+
return {
|
|
53
|
+
isValid: false,
|
|
54
|
+
error: `Invalid return type: expected (bytes), got ${hook.returnType}`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { isValid: true };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
isValid: false,
|
|
62
|
+
error: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decodes result from hook execution.
|
|
69
|
+
* Note: ethers already ABI-decodes the return value, so we just validate and return.
|
|
70
|
+
*/
|
|
71
|
+
export function decodeResult(resultBytes: string, returnType: string): string {
|
|
72
|
+
if (returnType !== '(bytes)') {
|
|
73
|
+
throw new Error(`Invalid return type: expected (bytes), got ${returnType}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ethers already decoded the bytes return value
|
|
77
|
+
return resultBytes;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ExecuteHookOptions {
|
|
81
|
+
providerMap: ProviderMap;
|
|
82
|
+
trustedTargets?: TrustedTargets;
|
|
83
|
+
throwOnUntrusted?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function executeHook(
|
|
87
|
+
hook: DecodedEIP8121Hook,
|
|
88
|
+
options: ExecuteHookOptions
|
|
89
|
+
): Promise<ExecutionResult> {
|
|
90
|
+
try {
|
|
91
|
+
const validation = validateHook(hook);
|
|
92
|
+
if (!validation.isValid) {
|
|
93
|
+
return {
|
|
94
|
+
_tag: "HookExecutionError",
|
|
95
|
+
error: true,
|
|
96
|
+
message: `Hook validation failed: ${validation.error}`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options.trustedTargets) {
|
|
101
|
+
const isTrusted = verifyTrustedTarget(hook.target, options.trustedTargets);
|
|
102
|
+
if (!isTrusted) {
|
|
103
|
+
const errorMsg = `Untrusted target: ${hook.target.address} on chain ${hook.target.chainId}`;
|
|
104
|
+
if (options.throwOnUntrusted) {
|
|
105
|
+
throw new Error(errorMsg);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
_tag: "HookExecutionError",
|
|
109
|
+
error: true,
|
|
110
|
+
message: errorMsg
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const provider = options.providerMap.get(hook.target.chainId);
|
|
116
|
+
if (!provider) {
|
|
117
|
+
return {
|
|
118
|
+
_tag: "HookExecutionError",
|
|
119
|
+
error: true,
|
|
120
|
+
message: `No provider available for chain ID ${hook.target.chainId}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse parameter values from functionCall (with generic error messages for security)
|
|
125
|
+
const paramValues = parseFunctionCallValues(hook.functionCall, hook.functionSignature, false);
|
|
126
|
+
|
|
127
|
+
const functionName = extractFunctionName(hook.functionSignature);
|
|
128
|
+
|
|
129
|
+
const abi = [`function ${hook.functionSignature} view returns (bytes)`];
|
|
130
|
+
|
|
131
|
+
const contract = new Contract(
|
|
132
|
+
hook.target.address,
|
|
133
|
+
abi,
|
|
134
|
+
provider
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const resultBytes = await contract[functionName](...paramValues);
|
|
138
|
+
const result = decodeResult(resultBytes, hook.returnType);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
_tag: "HookExecutionResult",
|
|
142
|
+
data: result
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
_tag: "HookExecutionError",
|
|
147
|
+
error: true,
|
|
148
|
+
message: `Hook execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
149
|
+
cause: error
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function detectAndDecodeHook(data: string): Promise<DecodedEIP8121Hook | null> {
|
|
155
|
+
if (!isEIP8121Hook(data)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { tryDecodeEIP8121HookFromContenthash } = await import("./encoding.js");
|
|
160
|
+
const stripped = tryDecodeEIP8121HookFromContenthash(data);
|
|
161
|
+
const hookData = stripped ?? data;
|
|
162
|
+
|
|
163
|
+
return await decodeHook(hookData);
|
|
164
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EIP8121Target } from "./encoding.js";
|
|
2
|
+
|
|
3
|
+
export interface TrustedTarget {
|
|
4
|
+
chainId: number;
|
|
5
|
+
address: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type TrustedTargets =
|
|
10
|
+
| TrustedTarget[]
|
|
11
|
+
| Set<string>
|
|
12
|
+
| ((target: EIP8121Target) => boolean);
|
|
13
|
+
|
|
14
|
+
export function verifyTrustedTarget(
|
|
15
|
+
target: EIP8121Target,
|
|
16
|
+
trustedTargets: TrustedTargets
|
|
17
|
+
): boolean {
|
|
18
|
+
if (typeof trustedTargets === 'function') {
|
|
19
|
+
return trustedTargets(target);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (trustedTargets instanceof Set) {
|
|
23
|
+
const key = createTargetKey(target.chainId, target.address);
|
|
24
|
+
return trustedTargets.has(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return trustedTargets.some(trusted =>
|
|
28
|
+
trusted.chainId === target.chainId &&
|
|
29
|
+
trusted.address.toLowerCase() === target.address.toLowerCase()
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createTargetKey(chainId: number, address: string): string {
|
|
34
|
+
return `${chainId}:${address.toLowerCase()}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createTrustedTargets(targets: TrustedTarget[]): Set<string> {
|
|
38
|
+
return new Set(targets.map(t => createTargetKey(t.chainId, t.address)));
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export {
|
|
2
|
+
executeHook,
|
|
3
|
+
validateHook,
|
|
4
|
+
decodeResult,
|
|
5
|
+
detectAndDecodeHook,
|
|
6
|
+
type ProviderMap,
|
|
7
|
+
type HookExecutionResult,
|
|
8
|
+
type HookExecutionError,
|
|
9
|
+
type ExecutionResult,
|
|
10
|
+
type HookValidationResult,
|
|
11
|
+
type ExecuteHookOptions,
|
|
12
|
+
} from "./dataurl/index.js";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
encodeHook,
|
|
16
|
+
decodeHook,
|
|
17
|
+
computeSelector,
|
|
18
|
+
extractFunctionName,
|
|
19
|
+
parseParameterTypes,
|
|
20
|
+
parseFunctionCallValues,
|
|
21
|
+
encodeERC7930Target,
|
|
22
|
+
decodeERC7930Target,
|
|
23
|
+
isEIP8121Hook,
|
|
24
|
+
isAllowedParameterType,
|
|
25
|
+
validateFunctionCallMatchesSignature,
|
|
26
|
+
MAX_STRING_LENGTH,
|
|
27
|
+
type DecodedEIP8121Hook,
|
|
28
|
+
type EIP8121Target,
|
|
29
|
+
type HookParameter
|
|
30
|
+
} from "./dataurl/encoding.js";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
HOOK_SELECTOR
|
|
34
|
+
} from "./dataurl/constants.js";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
verifyTrustedTarget,
|
|
38
|
+
createTrustedTargets,
|
|
39
|
+
createTargetKey,
|
|
40
|
+
type TrustedTarget,
|
|
41
|
+
type TrustedTargets
|
|
42
|
+
} from "./dataurl/trust.js";
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
encodeDataUri,
|
|
46
|
+
tryDecodeDataUri,
|
|
47
|
+
encodeEIP8121HookForContenthash,
|
|
48
|
+
tryDecodeEIP8121HookFromContenthash
|
|
49
|
+
} from "./dataurl/encoding.js";
|