@metamask-previews/phishing-controller 18.0.0-preview-cfb927be0 → 18.0.0-preview-e8a256c39
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 +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/signature-address-extraction.d.ts +86 -0
- package/dist/signature-address-extraction.d.ts.map +1 -0
- package/dist/signature-address-extraction.js +267 -0
- package/dist/signature-address-extraction.js.map +1 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add `extractSignatureAddresses` utility, plus `ExtractedSignatureAddresses` and `ExtractSignatureAddressesOptions` types, to collect the `address`-typed values from an EIP-712 typed-data message for real-time address scanning ([#10170](https://github.com/MetaMask/core/pull/10170))
|
|
13
|
+
- Walks the `types` schema from `primaryType`, matching fields by declared type (`address`/`address[]`, including nested structs and arrays) rather than by field name, so custom and unknown message shapes are covered without per-protocol handling.
|
|
14
|
+
- Normalizes non-canonical `address` encodings (variable-length hex and decimal strings) into canonical lower-case 20-byte hex by taking the leading 20 bytes of the signer-compatible big-endian encoding, and de-duplicates case-insensitively.
|
|
15
|
+
- Excludes the zero address, a caller-provided `exclude` list (e.g. the signer), and caller-provided top-level `excludeFields`.
|
|
16
|
+
- Bounds work with a distinct-address cap (default 10, caller-overridable via `maxAddresses`, hard ceiling 50), a traversal depth limit, and a node budget, reporting `overflow` when the message could not be fully walked. Exports `DEFAULT_MAX_SIGNATURE_ADDRESSES` and `MAX_SIGNATURE_ADDRESSES_CEILING`.
|
|
17
|
+
- Returns the field name each address was found under so callers can attribute alerts.
|
|
18
|
+
|
|
10
19
|
## [18.0.0]
|
|
11
20
|
|
|
12
21
|
### Changed
|
package/dist/index.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export { TokenScanResultType } from './types.js';
|
|
|
8
8
|
export { PhishingDetectorResultType, RecommendedAction, AddressScanResultType, ApprovalResultType, ApprovalFeatureType, } from './types.js';
|
|
9
9
|
export type { CacheEntry } from './CacheManager.js';
|
|
10
10
|
export { PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS, getPhishingDetectionScanUrlParam, isAddressScanSupportedChainId, isPhishingDetectionPathBasedHostname, } from './utils.js';
|
|
11
|
+
export { extractSignatureAddresses, DEFAULT_MAX_SIGNATURE_ADDRESSES, MAX_SIGNATURE_ADDRESSES_CEILING, } from './signature-address-extraction.js';
|
|
12
|
+
export type { ExtractedSignatureAddresses, ExtractSignatureAddressesOptions, } from './signature-address-extraction.js';
|
|
11
13
|
export type { PhishingControllerMaybeUpdateStateAction, PhishingControllerTestOriginAction, PhishingControllerIsBlockedRequestAction, PhishingControllerBypassAction, PhishingControllerScanUrlAction, PhishingControllerBulkScanUrlsAction, PhishingControllerBulkScanTokensAction, PhishingControllerScanAddressAction, PhishingControllerGetApprovalsAction, PhishingControllerCheckAddressPoisoningAction, } from './PhishingController-method-action-types.js';
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,0BAA0B,EAC1B,oBAAoB,EACpB,cAAc,EACd,uBAAuB,EACvB,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,QAAQ,EACR,OAAO,EACP,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EACL,0CAA0C,EAC1C,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EACV,0BAA0B,EAC1B,oBAAoB,EACpB,cAAc,EACd,uBAAuB,EACvB,6BAA6B,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,aAAa,EACb,QAAQ,EACR,OAAO,EACP,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EACL,0CAA0C,EAC1C,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,2BAA2B,EAC3B,gCAAgC,GACjC,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EACV,wCAAwC,EACxC,kCAAkC,EAClC,wCAAwC,EACxC,8BAA8B,EAC9B,+BAA+B,EAC/B,oCAAoC,EACpC,sCAAsC,EACtC,mCAAmC,EACnC,oCAAoC,EACpC,6CAA6C,GAC9C,MAAM,6CAA6C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export { PhishingDetector } from './PhishingDetector.js';
|
|
|
4
4
|
export { TokenScanResultType } from './types.js';
|
|
5
5
|
export { PhishingDetectorResultType, RecommendedAction, AddressScanResultType, ApprovalResultType, ApprovalFeatureType, } from './types.js';
|
|
6
6
|
export { PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS, getPhishingDetectionScanUrlParam, isAddressScanSupportedChainId, isPhishingDetectionPathBasedHostname, } from './utils.js';
|
|
7
|
+
export { extractSignatureAddresses, DEFAULT_MAX_SIGNATURE_ADDRESSES, MAX_SIGNATURE_ADDRESSES_CEILING, } from './signature-address-extraction.js';
|
|
7
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAQ9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAgBzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,0CAA0C,EAC1C,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,YAAY,CAAC","sourcesContent":["export * from './PhishingController.js';\nexport { findSimilarAddresses } from './address-poisoning.js';\nexport type {\n LegacyPhishingDetectorList,\n PhishingDetectorList,\n FuzzyTolerance,\n PhishingDetectorOptions,\n PhishingDetectorConfiguration,\n} from './PhishingDetector.js';\nexport { PhishingDetector } from './PhishingDetector.js';\nexport type {\n PhishingDetectionScanResult,\n AddressScanResult,\n BulkTokenScanResponse,\n SimilarAddressMatch,\n SimilarityOptions,\n ApprovalsResponse,\n Approval,\n Allowance,\n ApprovalAsset,\n Exposure,\n Spender,\n ApprovalFeature,\n} from './types.js';\nexport type { TokenScanCacheData } from './types.js';\nexport { TokenScanResultType } from './types.js';\nexport {\n PhishingDetectorResultType,\n RecommendedAction,\n AddressScanResultType,\n ApprovalResultType,\n ApprovalFeatureType,\n} from './types.js';\nexport type { CacheEntry } from './CacheManager.js';\nexport {\n PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS,\n getPhishingDetectionScanUrlParam,\n isAddressScanSupportedChainId,\n isPhishingDetectionPathBasedHostname,\n} from './utils.js';\n\nexport type {\n PhishingControllerMaybeUpdateStateAction,\n PhishingControllerTestOriginAction,\n PhishingControllerIsBlockedRequestAction,\n PhishingControllerBypassAction,\n PhishingControllerScanUrlAction,\n PhishingControllerBulkScanUrlsAction,\n PhishingControllerBulkScanTokensAction,\n PhishingControllerScanAddressAction,\n PhishingControllerGetApprovalsAction,\n PhishingControllerCheckAddressPoisoningAction,\n} from './PhishingController-method-action-types.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAQ9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAgBzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,0CAA0C,EAC1C,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,+BAA+B,EAC/B,+BAA+B,GAChC,MAAM,mCAAmC,CAAC","sourcesContent":["export * from './PhishingController.js';\nexport { findSimilarAddresses } from './address-poisoning.js';\nexport type {\n LegacyPhishingDetectorList,\n PhishingDetectorList,\n FuzzyTolerance,\n PhishingDetectorOptions,\n PhishingDetectorConfiguration,\n} from './PhishingDetector.js';\nexport { PhishingDetector } from './PhishingDetector.js';\nexport type {\n PhishingDetectionScanResult,\n AddressScanResult,\n BulkTokenScanResponse,\n SimilarAddressMatch,\n SimilarityOptions,\n ApprovalsResponse,\n Approval,\n Allowance,\n ApprovalAsset,\n Exposure,\n Spender,\n ApprovalFeature,\n} from './types.js';\nexport type { TokenScanCacheData } from './types.js';\nexport { TokenScanResultType } from './types.js';\nexport {\n PhishingDetectorResultType,\n RecommendedAction,\n AddressScanResultType,\n ApprovalResultType,\n ApprovalFeatureType,\n} from './types.js';\nexport type { CacheEntry } from './CacheManager.js';\nexport {\n PHISHING_DETECTION_PATH_BASED_ROOT_DOMAINS,\n getPhishingDetectionScanUrlParam,\n isAddressScanSupportedChainId,\n isPhishingDetectionPathBasedHostname,\n} from './utils.js';\nexport {\n extractSignatureAddresses,\n DEFAULT_MAX_SIGNATURE_ADDRESSES,\n MAX_SIGNATURE_ADDRESSES_CEILING,\n} from './signature-address-extraction.js';\nexport type {\n ExtractedSignatureAddresses,\n ExtractSignatureAddressesOptions,\n} from './signature-address-extraction.js';\n\nexport type {\n PhishingControllerMaybeUpdateStateAction,\n PhishingControllerTestOriginAction,\n PhishingControllerIsBlockedRequestAction,\n PhishingControllerBypassAction,\n PhishingControllerScanUrlAction,\n PhishingControllerBulkScanUrlsAction,\n PhishingControllerBulkScanTokensAction,\n PhishingControllerScanAddressAction,\n PhishingControllerGetApprovalsAction,\n PhishingControllerCheckAddressPoisoningAction,\n} from './PhishingController-method-action-types.js';\n"]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export declare const DEFAULT_MAX_SIGNATURE_ADDRESSES = 10;
|
|
2
|
+
export declare const MAX_SIGNATURE_ADDRESSES_CEILING = 50;
|
|
3
|
+
/**
|
|
4
|
+
* The result of walking an EIP-712 typed-data message for `address`-typed
|
|
5
|
+
* values.
|
|
6
|
+
*/
|
|
7
|
+
export type ExtractedSignatureAddresses = {
|
|
8
|
+
/**
|
|
9
|
+
* Distinct canonical addresses to scan, capped at the effective `maxAddresses`.
|
|
10
|
+
*/
|
|
11
|
+
addresses: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Canonical address -> the field name it was first found under, so a caller
|
|
14
|
+
* can name the specific field in an alert.
|
|
15
|
+
*/
|
|
16
|
+
fields: Record<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* True when the message could not be fully walked: more distinct addresses
|
|
19
|
+
* than the cap, traversal stopped by the depth or work budget, or an
|
|
20
|
+
* address-bearing type could not be walked (array type with a non-array
|
|
21
|
+
* value, or struct type with a non-object value). Some addresses may be
|
|
22
|
+
* unscanned, so the caller should surface a caution.
|
|
23
|
+
*/
|
|
24
|
+
overflow: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Effective address cap after applying the default and ceiling.
|
|
27
|
+
*/
|
|
28
|
+
maxAddresses: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Options for {@link extractSignatureAddresses}.
|
|
32
|
+
*/
|
|
33
|
+
export type ExtractSignatureAddressesOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* Addresses to skip (e.g. the signer). The zero address is always excluded.
|
|
36
|
+
*/
|
|
37
|
+
exclude?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Top-level field names to skip, used to avoid a duplicate scan/alert for a
|
|
40
|
+
* field already handled elsewhere (e.g. permit `spender`). Names must match
|
|
41
|
+
* the declared EIP-712 field exactly. Only applied to the primary type
|
|
42
|
+
* (depth 0), not nested structs.
|
|
43
|
+
*/
|
|
44
|
+
excludeFields?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Distinct-address cap for this call. Defaults to
|
|
47
|
+
* {@link DEFAULT_MAX_SIGNATURE_ADDRESSES}. Clamped to
|
|
48
|
+
* {@link MAX_SIGNATURE_ADDRESSES_CEILING}. Invalid values use the default.
|
|
49
|
+
*/
|
|
50
|
+
maxAddresses?: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Collect every `address`-typed value in an EIP-712 message.
|
|
54
|
+
*
|
|
55
|
+
* Walks the `types` schema from `primaryType` and returns the value of each
|
|
56
|
+
* field declared as `address` or `address[]`, recursing into nested structs and
|
|
57
|
+
* arrays. Matching on the declared type rather than the field name means custom
|
|
58
|
+
* and unknown message shapes are covered without per-protocol handling.
|
|
59
|
+
*
|
|
60
|
+
* Type dispatch matches the signer: a custom struct in `types` is walked first
|
|
61
|
+
* (even if its name looks like `address` or `address[]`), then `address`, then
|
|
62
|
+
* types whose name ends in `]` as arrays.
|
|
63
|
+
*
|
|
64
|
+
* `domain` is not traversed; its `verifyingContract` is expected to be scanned
|
|
65
|
+
* separately by the caller.
|
|
66
|
+
*
|
|
67
|
+
* @param typedData - Parsed EIP-712 payload (`types`, `primaryType`, `message`).
|
|
68
|
+
* @param options - Optional configuration.
|
|
69
|
+
* @param options.exclude - Addresses to skip (e.g. the signer). The zero
|
|
70
|
+
* address is always excluded.
|
|
71
|
+
* @param options.excludeFields - Top-level field names to skip. Names must
|
|
72
|
+
* match the declared EIP-712 field exactly. Only applied to the primary type
|
|
73
|
+
* (depth 0), not nested structs.
|
|
74
|
+
* @param options.maxAddresses - Distinct-address cap. Defaults to
|
|
75
|
+
* {@link DEFAULT_MAX_SIGNATURE_ADDRESSES} and is clamped to
|
|
76
|
+
* {@link MAX_SIGNATURE_ADDRESSES_CEILING}.
|
|
77
|
+
* @returns Up to `maxAddresses` distinct canonical addresses, the field each
|
|
78
|
+
* was found under, whether the message could not be fully walked, and the
|
|
79
|
+
* effective cap.
|
|
80
|
+
*/
|
|
81
|
+
export declare function extractSignatureAddresses(typedData: {
|
|
82
|
+
types?: unknown;
|
|
83
|
+
primaryType?: unknown;
|
|
84
|
+
message?: unknown;
|
|
85
|
+
} | null | undefined, options?: ExtractSignatureAddressesOptions): ExtractedSignatureAddresses;
|
|
86
|
+
//# sourceMappingURL=signature-address-extraction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature-address-extraction.d.ts","sourceRoot":"","sources":["../src/signature-address-extraction.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAYlD;;;GAGG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gCAAgC,GAAG;IAC7C;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AA+DF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EACL;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7D,IAAI,GACJ,SAAS,EACb,OAAO,GAAE,gCAAqC,GAC7C,2BAA2B,CAoN7B"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
2
|
+
// Same as `@metamask/utils` `isStrictHexString` (`/^0x[0-9a-f]+$/iu`): the
|
|
3
|
+
// signer accepts a `0X` prefix, so we must too.
|
|
4
|
+
const HEX_STRING_REGEX = /^0x[0-9a-f]+$/iu;
|
|
5
|
+
const DECIMAL_STRING_REGEX = /^[0-9]+$/u;
|
|
6
|
+
export const DEFAULT_MAX_SIGNATURE_ADDRESSES = 10;
|
|
7
|
+
export const MAX_SIGNATURE_ADDRESSES_CEILING = 50;
|
|
8
|
+
// Limit recursion depth when walking nested types.
|
|
9
|
+
const MAX_TRAVERSAL_DEPTH = 12;
|
|
10
|
+
// Limit total nodes walked so a large or highly-repetitive payload cannot stall
|
|
11
|
+
// traversal, independent of how many distinct addresses are found.
|
|
12
|
+
const MAX_TRAVERSAL_NODES = 5000;
|
|
13
|
+
function resolveMaxAddresses(value) {
|
|
14
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
15
|
+
return DEFAULT_MAX_SIGNATURE_ADDRESSES;
|
|
16
|
+
}
|
|
17
|
+
const floored = Math.floor(value);
|
|
18
|
+
if (floored < 1) {
|
|
19
|
+
return DEFAULT_MAX_SIGNATURE_ADDRESSES;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(floored, MAX_SIGNATURE_ADDRESSES_CEILING);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Encode a non-negative integer as big-endian hex (even length) and take the
|
|
25
|
+
* leading 20 bytes.
|
|
26
|
+
*
|
|
27
|
+
* @param numeric - A non-negative integer.
|
|
28
|
+
* @returns Canonical lower-case 20-byte address.
|
|
29
|
+
*/
|
|
30
|
+
function leadingTwentyBytesFromInteger(numeric) {
|
|
31
|
+
let digits = numeric.toString(16);
|
|
32
|
+
if (digits.length % 2 === 1) {
|
|
33
|
+
digits = `0${digits}`;
|
|
34
|
+
}
|
|
35
|
+
return `0x${digits.slice(0, 40).padStart(40, '0')}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Reduce an `address`-typed value to canonical 20-byte hex.
|
|
39
|
+
*
|
|
40
|
+
* The signer accepts more than canonical hex for an `address` field (hex of any
|
|
41
|
+
* length, or a decimal string) and takes the high / leading 20 bytes of the
|
|
42
|
+
* big-endian encoding (`reallyStrangeAddressToBytes(value).subarray(0, 20)` /
|
|
43
|
+
* `hexToBytes(value).subarray(0, 20)` in `@metamask/eth-sig-util`), so matching
|
|
44
|
+
* only `0x` + 40 hex would miss an address encoded in another form.
|
|
45
|
+
*
|
|
46
|
+
* @param value - The raw field value from the message.
|
|
47
|
+
* @returns Canonical lower-case address, or undefined if not address-like.
|
|
48
|
+
*/
|
|
49
|
+
function normalizeAddress(value) {
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (HEX_STRING_REGEX.test(trimmed)) {
|
|
53
|
+
let digits = trimmed.slice(2);
|
|
54
|
+
if (digits.length % 2 === 1) {
|
|
55
|
+
digits = `0${digits}`;
|
|
56
|
+
}
|
|
57
|
+
return `0x${digits.slice(0, 40).padStart(40, '0').toLowerCase()}`;
|
|
58
|
+
}
|
|
59
|
+
if (DECIMAL_STRING_REGEX.test(trimmed)) {
|
|
60
|
+
return leadingTwentyBytesFromInteger(BigInt(trimmed));
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {
|
|
65
|
+
return leadingTwentyBytesFromInteger(BigInt(value));
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Collect every `address`-typed value in an EIP-712 message.
|
|
71
|
+
*
|
|
72
|
+
* Walks the `types` schema from `primaryType` and returns the value of each
|
|
73
|
+
* field declared as `address` or `address[]`, recursing into nested structs and
|
|
74
|
+
* arrays. Matching on the declared type rather than the field name means custom
|
|
75
|
+
* and unknown message shapes are covered without per-protocol handling.
|
|
76
|
+
*
|
|
77
|
+
* Type dispatch matches the signer: a custom struct in `types` is walked first
|
|
78
|
+
* (even if its name looks like `address` or `address[]`), then `address`, then
|
|
79
|
+
* types whose name ends in `]` as arrays.
|
|
80
|
+
*
|
|
81
|
+
* `domain` is not traversed; its `verifyingContract` is expected to be scanned
|
|
82
|
+
* separately by the caller.
|
|
83
|
+
*
|
|
84
|
+
* @param typedData - Parsed EIP-712 payload (`types`, `primaryType`, `message`).
|
|
85
|
+
* @param options - Optional configuration.
|
|
86
|
+
* @param options.exclude - Addresses to skip (e.g. the signer). The zero
|
|
87
|
+
* address is always excluded.
|
|
88
|
+
* @param options.excludeFields - Top-level field names to skip. Names must
|
|
89
|
+
* match the declared EIP-712 field exactly. Only applied to the primary type
|
|
90
|
+
* (depth 0), not nested structs.
|
|
91
|
+
* @param options.maxAddresses - Distinct-address cap. Defaults to
|
|
92
|
+
* {@link DEFAULT_MAX_SIGNATURE_ADDRESSES} and is clamped to
|
|
93
|
+
* {@link MAX_SIGNATURE_ADDRESSES_CEILING}.
|
|
94
|
+
* @returns Up to `maxAddresses` distinct canonical addresses, the field each
|
|
95
|
+
* was found under, whether the message could not be fully walked, and the
|
|
96
|
+
* effective cap.
|
|
97
|
+
*/
|
|
98
|
+
export function extractSignatureAddresses(typedData, options = {}) {
|
|
99
|
+
const maxAddresses = resolveMaxAddresses(options.maxAddresses);
|
|
100
|
+
const types = typedData?.types;
|
|
101
|
+
const primaryType = typedData?.primaryType;
|
|
102
|
+
const { message } = typedData ?? {};
|
|
103
|
+
if (!types ||
|
|
104
|
+
typeof types !== 'object' ||
|
|
105
|
+
!primaryType ||
|
|
106
|
+
!Array.isArray(types[primaryType]) ||
|
|
107
|
+
!message ||
|
|
108
|
+
typeof message !== 'object') {
|
|
109
|
+
return { addresses: [], fields: {}, overflow: false, maxAddresses };
|
|
110
|
+
}
|
|
111
|
+
// Narrowed alias so the hoisted helpers below see a defined `types`.
|
|
112
|
+
const schema = types;
|
|
113
|
+
// ZERO_ADDRESS is already canonical (lower-case, 20 bytes), so it is added
|
|
114
|
+
// directly rather than round-tripped through `normalizeAddress`.
|
|
115
|
+
const excluded = new Set([ZERO_ADDRESS]);
|
|
116
|
+
for (const address of options.exclude ?? []) {
|
|
117
|
+
const normalized = normalizeAddress(address);
|
|
118
|
+
if (normalized) {
|
|
119
|
+
excluded.add(normalized);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const excludedFields = new Set(options.excludeFields ?? []);
|
|
123
|
+
// Canonical address -> the field name it was first found under.
|
|
124
|
+
const found = new Map();
|
|
125
|
+
// Set when the message could not be fully walked, so some addresses may be
|
|
126
|
+
// unscanned: the address cap, the depth limit, the work budget, or an
|
|
127
|
+
// address-bearing type whose value could not be walked.
|
|
128
|
+
let overflow = false;
|
|
129
|
+
// Total nodes walked, bounded by MAX_TRAVERSAL_NODES.
|
|
130
|
+
let visited = 0;
|
|
131
|
+
// Stopping the walk (depth or work budget) leaves later fields unscanned, so
|
|
132
|
+
// it is treated as overflow the same way the distinct-address cap is.
|
|
133
|
+
const truncated = (depth) => {
|
|
134
|
+
if (depth > MAX_TRAVERSAL_DEPTH || visited >= MAX_TRAVERSAL_NODES) {
|
|
135
|
+
overflow = true;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Whether `type` can contain `address` values: the `address` primitive, an
|
|
142
|
+
* array of an address-bearing type, or a custom struct that contains one.
|
|
143
|
+
*
|
|
144
|
+
* @param type - The declared EIP-712 type.
|
|
145
|
+
* @param seen - Types already inspected, to break recursive structs.
|
|
146
|
+
* @returns True when walking this type can yield addresses.
|
|
147
|
+
*/
|
|
148
|
+
function isAddressBearing(type, seen = new Set()) {
|
|
149
|
+
if (seen.has(type)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
seen.add(type);
|
|
153
|
+
const structFields = schema[type];
|
|
154
|
+
if (Array.isArray(structFields)) {
|
|
155
|
+
return structFields.some((field) => Boolean(field) &&
|
|
156
|
+
typeof field.type === 'string' &&
|
|
157
|
+
isAddressBearing(field.type, seen));
|
|
158
|
+
}
|
|
159
|
+
if (type === 'address') {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (type.endsWith(']')) {
|
|
163
|
+
return isAddressBearing(type.slice(0, type.lastIndexOf('[')), seen);
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Record a candidate address value under a field name, applying exclusions,
|
|
169
|
+
* de-duplication, and the distinct-address cap.
|
|
170
|
+
*
|
|
171
|
+
* @param field - The field name the value was found under.
|
|
172
|
+
* @param value - The raw field value to normalize and collect.
|
|
173
|
+
*/
|
|
174
|
+
function collect(field, value) {
|
|
175
|
+
const address = normalizeAddress(value);
|
|
176
|
+
if (!address || excluded.has(address) || found.has(address)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (found.size >= maxAddresses) {
|
|
180
|
+
overflow = true;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
found.set(address, field);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Walk the fields of a struct type, recursing per field.
|
|
187
|
+
*
|
|
188
|
+
* @param structName - The name of the struct type in the schema.
|
|
189
|
+
* @param value - The message object corresponding to the struct.
|
|
190
|
+
* @param depth - The current traversal depth.
|
|
191
|
+
*/
|
|
192
|
+
function visitStruct(structName, value, depth) {
|
|
193
|
+
if (truncated(depth)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const structFields = schema[structName];
|
|
197
|
+
if (!Array.isArray(structFields) || !value || typeof value !== 'object') {
|
|
198
|
+
if (Array.isArray(structFields) && isAddressBearing(structName)) {
|
|
199
|
+
overflow = true;
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const field of structFields) {
|
|
204
|
+
if (truncated(depth)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!field ||
|
|
208
|
+
typeof field.name !== 'string' ||
|
|
209
|
+
typeof field.type !== 'string' ||
|
|
210
|
+
// Field exclusions only apply to the primary type (depth 0), matching
|
|
211
|
+
// the top-level field a dedicated caller already covers. Names must
|
|
212
|
+
// match the declared EIP-712 field exactly.
|
|
213
|
+
(depth === 0 && excludedFields.has(field.name))) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
visitField(field.name, field.type, value[field.name], depth);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Walk a single field value, handling custom structs, `address`, and arrays.
|
|
221
|
+
*
|
|
222
|
+
* Precedence matches `@metamask/eth-sig-util` `encodeField`: a type present
|
|
223
|
+
* in the schema is a struct first; otherwise `address`; otherwise a name
|
|
224
|
+
* ending in `]` is treated as an array (`type.slice(0, lastIndexOf('['))`).
|
|
225
|
+
*
|
|
226
|
+
* @param field - The field name.
|
|
227
|
+
* @param type - The declared EIP-712 type of the field.
|
|
228
|
+
* @param value - The field value from the message.
|
|
229
|
+
* @param depth - The current traversal depth.
|
|
230
|
+
*/
|
|
231
|
+
function visitField(field, type, value, depth) {
|
|
232
|
+
visited += 1;
|
|
233
|
+
if (truncated(depth)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(schema[type])) {
|
|
237
|
+
visitStruct(type, value, depth + 1);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (type === 'address') {
|
|
241
|
+
collect(field, value);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (type.endsWith(']')) {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
const innerType = type.slice(0, type.lastIndexOf('['));
|
|
247
|
+
for (const item of value) {
|
|
248
|
+
if (truncated(depth)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
visitField(field, innerType, item, depth + 1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (isAddressBearing(type)) {
|
|
255
|
+
overflow = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
visitStruct(primaryType, message, 0);
|
|
260
|
+
return {
|
|
261
|
+
addresses: Array.from(found.keys()),
|
|
262
|
+
fields: Object.fromEntries(found),
|
|
263
|
+
overflow,
|
|
264
|
+
maxAddresses,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=signature-address-extraction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature-address-extraction.js","sourceRoot":"","sources":["../src/signature-address-extraction.ts"],"names":[],"mappings":"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","sourcesContent":["const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';\n\n// Same as `@metamask/utils` `isStrictHexString` (`/^0x[0-9a-f]+$/iu`): the\n// signer accepts a `0X` prefix, so we must too.\nconst HEX_STRING_REGEX = /^0x[0-9a-f]+$/iu;\nconst DECIMAL_STRING_REGEX = /^[0-9]+$/u;\n\nexport const DEFAULT_MAX_SIGNATURE_ADDRESSES = 10;\n\nexport const MAX_SIGNATURE_ADDRESSES_CEILING = 50;\n\n// Limit recursion depth when walking nested types.\nconst MAX_TRAVERSAL_DEPTH = 12;\n\n// Limit total nodes walked so a large or highly-repetitive payload cannot stall\n// traversal, independent of how many distinct addresses are found.\nconst MAX_TRAVERSAL_NODES = 5000;\n\ntype Eip712Field = { name: string; type: string };\ntype Eip712Types = Record<string, Eip712Field[]>;\n\n/**\n * The result of walking an EIP-712 typed-data message for `address`-typed\n * values.\n */\nexport type ExtractedSignatureAddresses = {\n /**\n * Distinct canonical addresses to scan, capped at the effective `maxAddresses`.\n */\n addresses: string[];\n /**\n * Canonical address -> the field name it was first found under, so a caller\n * can name the specific field in an alert.\n */\n fields: Record<string, string>;\n /**\n * True when the message could not be fully walked: more distinct addresses\n * than the cap, traversal stopped by the depth or work budget, or an\n * address-bearing type could not be walked (array type with a non-array\n * value, or struct type with a non-object value). Some addresses may be\n * unscanned, so the caller should surface a caution.\n */\n overflow: boolean;\n /**\n * Effective address cap after applying the default and ceiling.\n */\n maxAddresses: number;\n};\n\n/**\n * Options for {@link extractSignatureAddresses}.\n */\nexport type ExtractSignatureAddressesOptions = {\n /**\n * Addresses to skip (e.g. the signer). The zero address is always excluded.\n */\n exclude?: string[];\n /**\n * Top-level field names to skip, used to avoid a duplicate scan/alert for a\n * field already handled elsewhere (e.g. permit `spender`). Names must match\n * the declared EIP-712 field exactly. Only applied to the primary type\n * (depth 0), not nested structs.\n */\n excludeFields?: string[];\n /**\n * Distinct-address cap for this call. Defaults to\n * {@link DEFAULT_MAX_SIGNATURE_ADDRESSES}. Clamped to\n * {@link MAX_SIGNATURE_ADDRESSES_CEILING}. Invalid values use the default.\n */\n maxAddresses?: number;\n};\n\nfunction resolveMaxAddresses(value: unknown): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n return DEFAULT_MAX_SIGNATURE_ADDRESSES;\n }\n const floored = Math.floor(value);\n if (floored < 1) {\n return DEFAULT_MAX_SIGNATURE_ADDRESSES;\n }\n return Math.min(floored, MAX_SIGNATURE_ADDRESSES_CEILING);\n}\n\n/**\n * Encode a non-negative integer as big-endian hex (even length) and take the\n * leading 20 bytes.\n *\n * @param numeric - A non-negative integer.\n * @returns Canonical lower-case 20-byte address.\n */\nfunction leadingTwentyBytesFromInteger(numeric: bigint): string {\n let digits = numeric.toString(16);\n if (digits.length % 2 === 1) {\n digits = `0${digits}`;\n }\n return `0x${digits.slice(0, 40).padStart(40, '0')}`;\n}\n\n/**\n * Reduce an `address`-typed value to canonical 20-byte hex.\n *\n * The signer accepts more than canonical hex for an `address` field (hex of any\n * length, or a decimal string) and takes the high / leading 20 bytes of the\n * big-endian encoding (`reallyStrangeAddressToBytes(value).subarray(0, 20)` /\n * `hexToBytes(value).subarray(0, 20)` in `@metamask/eth-sig-util`), so matching\n * only `0x` + 40 hex would miss an address encoded in another form.\n *\n * @param value - The raw field value from the message.\n * @returns Canonical lower-case address, or undefined if not address-like.\n */\nfunction normalizeAddress(value: unknown): string | undefined {\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (HEX_STRING_REGEX.test(trimmed)) {\n let digits = trimmed.slice(2);\n if (digits.length % 2 === 1) {\n digits = `0${digits}`;\n }\n return `0x${digits.slice(0, 40).padStart(40, '0').toLowerCase()}`;\n }\n if (DECIMAL_STRING_REGEX.test(trimmed)) {\n return leadingTwentyBytesFromInteger(BigInt(trimmed));\n }\n return undefined;\n }\n\n if (typeof value === 'number' && Number.isInteger(value) && value >= 0) {\n return leadingTwentyBytesFromInteger(BigInt(value));\n }\n\n return undefined;\n}\n\n/**\n * Collect every `address`-typed value in an EIP-712 message.\n *\n * Walks the `types` schema from `primaryType` and returns the value of each\n * field declared as `address` or `address[]`, recursing into nested structs and\n * arrays. Matching on the declared type rather than the field name means custom\n * and unknown message shapes are covered without per-protocol handling.\n *\n * Type dispatch matches the signer: a custom struct in `types` is walked first\n * (even if its name looks like `address` or `address[]`), then `address`, then\n * types whose name ends in `]` as arrays.\n *\n * `domain` is not traversed; its `verifyingContract` is expected to be scanned\n * separately by the caller.\n *\n * @param typedData - Parsed EIP-712 payload (`types`, `primaryType`, `message`).\n * @param options - Optional configuration.\n * @param options.exclude - Addresses to skip (e.g. the signer). The zero\n * address is always excluded.\n * @param options.excludeFields - Top-level field names to skip. Names must\n * match the declared EIP-712 field exactly. Only applied to the primary type\n * (depth 0), not nested structs.\n * @param options.maxAddresses - Distinct-address cap. Defaults to\n * {@link DEFAULT_MAX_SIGNATURE_ADDRESSES} and is clamped to\n * {@link MAX_SIGNATURE_ADDRESSES_CEILING}.\n * @returns Up to `maxAddresses` distinct canonical addresses, the field each\n * was found under, whether the message could not be fully walked, and the\n * effective cap.\n */\nexport function extractSignatureAddresses(\n typedData:\n | { types?: unknown; primaryType?: unknown; message?: unknown }\n | null\n | undefined,\n options: ExtractSignatureAddressesOptions = {},\n): ExtractedSignatureAddresses {\n const maxAddresses = resolveMaxAddresses(options.maxAddresses);\n const types = typedData?.types as Eip712Types | undefined;\n const primaryType = typedData?.primaryType as string | undefined;\n const { message } = typedData ?? {};\n\n if (\n !types ||\n typeof types !== 'object' ||\n !primaryType ||\n !Array.isArray(types[primaryType]) ||\n !message ||\n typeof message !== 'object'\n ) {\n return { addresses: [], fields: {}, overflow: false, maxAddresses };\n }\n\n // Narrowed alias so the hoisted helpers below see a defined `types`.\n const schema = types;\n\n // ZERO_ADDRESS is already canonical (lower-case, 20 bytes), so it is added\n // directly rather than round-tripped through `normalizeAddress`.\n const excluded = new Set<string>([ZERO_ADDRESS]);\n for (const address of options.exclude ?? []) {\n const normalized = normalizeAddress(address);\n if (normalized) {\n excluded.add(normalized);\n }\n }\n\n const excludedFields = new Set(options.excludeFields ?? []);\n\n // Canonical address -> the field name it was first found under.\n const found = new Map<string, string>();\n\n // Set when the message could not be fully walked, so some addresses may be\n // unscanned: the address cap, the depth limit, the work budget, or an\n // address-bearing type whose value could not be walked.\n let overflow = false;\n\n // Total nodes walked, bounded by MAX_TRAVERSAL_NODES.\n let visited = 0;\n\n // Stopping the walk (depth or work budget) leaves later fields unscanned, so\n // it is treated as overflow the same way the distinct-address cap is.\n const truncated = (depth: number): boolean => {\n if (depth > MAX_TRAVERSAL_DEPTH || visited >= MAX_TRAVERSAL_NODES) {\n overflow = true;\n return true;\n }\n return false;\n };\n\n /**\n * Whether `type` can contain `address` values: the `address` primitive, an\n * array of an address-bearing type, or a custom struct that contains one.\n *\n * @param type - The declared EIP-712 type.\n * @param seen - Types already inspected, to break recursive structs.\n * @returns True when walking this type can yield addresses.\n */\n function isAddressBearing(\n type: string,\n seen: Set<string> = new Set(),\n ): boolean {\n if (seen.has(type)) {\n return false;\n }\n seen.add(type);\n\n const structFields = schema[type];\n if (Array.isArray(structFields)) {\n return structFields.some(\n (field) =>\n Boolean(field) &&\n typeof field.type === 'string' &&\n isAddressBearing(field.type, seen),\n );\n }\n\n if (type === 'address') {\n return true;\n }\n\n if (type.endsWith(']')) {\n return isAddressBearing(type.slice(0, type.lastIndexOf('[')), seen);\n }\n\n return false;\n }\n\n /**\n * Record a candidate address value under a field name, applying exclusions,\n * de-duplication, and the distinct-address cap.\n *\n * @param field - The field name the value was found under.\n * @param value - The raw field value to normalize and collect.\n */\n function collect(field: string, value: unknown): void {\n const address = normalizeAddress(value);\n if (!address || excluded.has(address) || found.has(address)) {\n return;\n }\n if (found.size >= maxAddresses) {\n overflow = true;\n return;\n }\n found.set(address, field);\n }\n\n /**\n * Walk the fields of a struct type, recursing per field.\n *\n * @param structName - The name of the struct type in the schema.\n * @param value - The message object corresponding to the struct.\n * @param depth - The current traversal depth.\n */\n function visitStruct(\n structName: string,\n value: unknown,\n depth: number,\n ): void {\n if (truncated(depth)) {\n return;\n }\n const structFields = schema[structName];\n if (!Array.isArray(structFields) || !value || typeof value !== 'object') {\n if (Array.isArray(structFields) && isAddressBearing(structName)) {\n overflow = true;\n }\n return;\n }\n for (const field of structFields) {\n if (truncated(depth)) {\n return;\n }\n if (\n !field ||\n typeof field.name !== 'string' ||\n typeof field.type !== 'string' ||\n // Field exclusions only apply to the primary type (depth 0), matching\n // the top-level field a dedicated caller already covers. Names must\n // match the declared EIP-712 field exactly.\n (depth === 0 && excludedFields.has(field.name))\n ) {\n continue;\n }\n visitField(\n field.name,\n field.type,\n (value as Record<string, unknown>)[field.name],\n depth,\n );\n }\n }\n\n /**\n * Walk a single field value, handling custom structs, `address`, and arrays.\n *\n * Precedence matches `@metamask/eth-sig-util` `encodeField`: a type present\n * in the schema is a struct first; otherwise `address`; otherwise a name\n * ending in `]` is treated as an array (`type.slice(0, lastIndexOf('['))`).\n *\n * @param field - The field name.\n * @param type - The declared EIP-712 type of the field.\n * @param value - The field value from the message.\n * @param depth - The current traversal depth.\n */\n function visitField(\n field: string,\n type: string,\n value: unknown,\n depth: number,\n ): void {\n visited += 1;\n if (truncated(depth)) {\n return;\n }\n\n if (Array.isArray(schema[type])) {\n visitStruct(type, value, depth + 1);\n return;\n }\n\n if (type === 'address') {\n collect(field, value);\n return;\n }\n\n if (type.endsWith(']')) {\n if (Array.isArray(value)) {\n const innerType = type.slice(0, type.lastIndexOf('['));\n for (const item of value) {\n if (truncated(depth)) {\n return;\n }\n visitField(field, innerType, item, depth + 1);\n }\n } else if (isAddressBearing(type)) {\n overflow = true;\n }\n }\n }\n\n visitStruct(primaryType, message, 0);\n\n return {\n addresses: Array.from(found.keys()),\n fields: Object.fromEntries(found),\n overflow,\n maxAddresses,\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamask-previews/phishing-controller",
|
|
3
|
-
"version": "18.0.0-preview-
|
|
3
|
+
"version": "18.0.0-preview-e8a256c39",
|
|
4
4
|
"description": "Maintains a periodically updated list of approved and unapproved website origins",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ethereum",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@metamask/auto-changelog": "^6.1.0",
|
|
66
|
+
"@metamask/eth-sig-util": "^9.0.0",
|
|
66
67
|
"@types/jest": "^30.0.0",
|
|
67
68
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
68
69
|
"deepmerge": "^4.2.2",
|