@lukso/transaction-decoder 1.0.1-dev.0f1bea5
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 +201 -0
- package/README.md +486 -0
- package/dist/browser.cjs +6912 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +6 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +131 -0
- package/dist/browser.js.map +1 -0
- package/dist/cdn/transaction-decoder.global.js +296 -0
- package/dist/cdn/transaction-decoder.global.js.map +1 -0
- package/dist/chunk-GGBHTWJL.js +437 -0
- package/dist/chunk-GGBHTWJL.js.map +1 -0
- package/dist/chunk-GXZOF3QY.js +839 -0
- package/dist/chunk-GXZOF3QY.js.map +1 -0
- package/dist/chunk-LJ6ES5XF.js +776 -0
- package/dist/chunk-LJ6ES5XF.js.map +1 -0
- package/dist/chunk-XVHJWV5U.js +4925 -0
- package/dist/chunk-XVHJWV5U.js.map +1 -0
- package/dist/data.cjs +5518 -0
- package/dist/data.cjs.map +1 -0
- package/dist/data.d.cts +43 -0
- package/dist/data.d.ts +43 -0
- package/dist/data.js +55 -0
- package/dist/data.js.map +1 -0
- package/dist/index-BzXh7poJ.d.cts +524 -0
- package/dist/index-BzXh7poJ.d.ts +524 -0
- package/dist/index.cjs +6912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +756 -0
- package/dist/index.d.ts +756 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +5644 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +217 -0
- package/dist/server.d.ts +217 -0
- package/dist/server.js +644 -0
- package/dist/server.js.map +1 -0
- package/dist/utils-CBAkjQh3.d.cts +108 -0
- package/dist/utils-xT9-km0r.d.ts +108 -0
- package/package.json +101 -0
- package/src/browser.ts +13 -0
- package/src/client/resolveAddresses.ts +157 -0
- package/src/core/addressCollector.ts +153 -0
- package/src/core/addressResolver.ts +135 -0
- package/src/core/dataModel.ts +888 -0
- package/src/core/instance.ts +33 -0
- package/src/core/integrateDecoder.ts +325 -0
- package/src/data.ts +70 -0
- package/src/decoder/GENERATOR_PROPOSAL.md +182 -0
- package/src/decoder/THREE_PHASE_EXAMPLE.md +108 -0
- package/src/decoder/aggregation.ts +218 -0
- package/src/decoder/browserCache.ts +237 -0
- package/src/decoder/cache/README.md +126 -0
- package/src/decoder/cache/index.ts +44 -0
- package/src/decoder/cache.ts +139 -0
- package/src/decoder/constants.ts +125 -0
- package/src/decoder/decodeTransaction.ts +292 -0
- package/src/decoder/errors.ts +95 -0
- package/src/decoder/events.ts +192 -0
- package/src/decoder/functionSignature.ts +344 -0
- package/src/decoder/getDataFromExternalSources.ts +248 -0
- package/src/decoder/graphqlWS.ts +22 -0
- package/src/decoder/interfaces.ts +185 -0
- package/src/decoder/keyValue.ts +5 -0
- package/src/decoder/kvCache.ts +241 -0
- package/src/decoder/lruCache.ts +184 -0
- package/src/decoder/lsp7Mint.test.ts +179 -0
- package/src/decoder/lsp7TransferBatch.test.ts +105 -0
- package/src/decoder/plugins/RegistryAbi.ts +562 -0
- package/src/decoder/plugins/enhanceBurntPix.ts +132 -0
- package/src/decoder/plugins/enhanceGraffiti.ts +70 -0
- package/src/decoder/plugins/enhanceLSP0ERC725Account.ts +179 -0
- package/src/decoder/plugins/enhanceLSP26FollowerSystem.ts +88 -0
- package/src/decoder/plugins/enhanceLSP6KeyManager.ts +231 -0
- package/src/decoder/plugins/enhanceLSP7DigitalAsset.ts +165 -0
- package/src/decoder/plugins/enhanceLSP8IdentifiableDigitalAsset.ts +170 -0
- package/src/decoder/plugins/enhanceLSP9Vault.ts +57 -0
- package/src/decoder/plugins/enhanceRetrieveAbi.ts +85 -0
- package/src/decoder/plugins/enhanceSetData.ts +135 -0
- package/src/decoder/plugins/index.ts +99 -0
- package/src/decoder/plugins/schemaDefault.ts +318 -0
- package/src/decoder/plugins/standardPlugin.ts +202 -0
- package/src/decoder/registry.ts +322 -0
- package/src/decoder/singleGQL.ts +293 -0
- package/src/decoder/transaction.ts +198 -0
- package/src/decoder/types.ts +465 -0
- package/src/decoder/utils.ts +212 -0
- package/src/example/usage.ts +172 -0
- package/src/index.ts +174 -0
- package/src/server/addressResolver.ts +68 -0
- package/src/server/caches.ts +209 -0
- package/src/server/decodeTransactionSync.ts +156 -0
- package/src/server/decodeTransactionsBatch.ts +207 -0
- package/src/server/finishDecoding.ts +116 -0
- package/src/server/index.ts +81 -0
- package/src/server/lsp23Resolver.test.ts +46 -0
- package/src/server/lsp23Resolver.ts +419 -0
- package/src/server/types.ts +168 -0
- package/src/server.ts +22 -0
- package/src/shared/addressResolver.ts +651 -0
- package/src/shared/cache.ts +144 -0
- package/src/shared/constants.ts +21 -0
- package/src/stubs/tty.ts +13 -0
- package/src/stubs/util.ts +42 -0
- package/src/types/index.ts +154 -0
- package/src/types/provider.ts +46 -0
- package/src/umd.ts +13 -0
- package/src/utils/debug.ts +49 -0
- package/src/utils/json-bigint.ts +47 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Abi,
|
|
3
|
+
AbiDecodingDataSizeTooSmallError,
|
|
4
|
+
AbiEventSignatureEmptyTopicsError,
|
|
5
|
+
AbiEventSignatureNotFoundError,
|
|
6
|
+
type AbiParameter,
|
|
7
|
+
BaseError,
|
|
8
|
+
type Chain,
|
|
9
|
+
type ContractEventName,
|
|
10
|
+
type DecodeEventLogParameters,
|
|
11
|
+
type DecodeEventLogReturnType,
|
|
12
|
+
DecodeLogDataMismatch,
|
|
13
|
+
DecodeLogTopicsMismatch,
|
|
14
|
+
decodeAbiParameters,
|
|
15
|
+
type EventDefinition,
|
|
16
|
+
type Hex,
|
|
17
|
+
type Log,
|
|
18
|
+
size,
|
|
19
|
+
toEventSelector,
|
|
20
|
+
} from 'viem'
|
|
21
|
+
import { formatAbiItem } from 'viem/utils'
|
|
22
|
+
import type { ArrayArgs, DecodeEventResult } from './types'
|
|
23
|
+
import { createNamedArgs } from './utils'
|
|
24
|
+
|
|
25
|
+
const docsPath = '/docs/contract/decodeEventLog'
|
|
26
|
+
|
|
27
|
+
function decodeTopic({ param, value }: { param: AbiParameter; value: Hex }) {
|
|
28
|
+
if (
|
|
29
|
+
param.type === 'string' ||
|
|
30
|
+
param.type === 'bytes' ||
|
|
31
|
+
param.type === 'tuple' ||
|
|
32
|
+
param.type.match(/^(.*)\[(\d+)?\]$/)
|
|
33
|
+
)
|
|
34
|
+
return value
|
|
35
|
+
const decodedArg = decodeAbiParameters([param], value) || []
|
|
36
|
+
return decodedArg[0]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PositionOutOfBoundsErrorType = PositionOutOfBoundsError & {
|
|
40
|
+
name: 'PositionOutOfBoundsError'
|
|
41
|
+
}
|
|
42
|
+
export class PositionOutOfBoundsError extends BaseError {
|
|
43
|
+
constructor({ length, position }: { length: number; position: number }) {
|
|
44
|
+
super(
|
|
45
|
+
`Position \`${position}\` is out of bounds (\`0 < position < ${length}\`).`,
|
|
46
|
+
{ name: 'PositionOutOfBoundsError' }
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Custom copy of decodeEventLog which can assemble named args.
|
|
53
|
+
* @param parameters
|
|
54
|
+
* @returns
|
|
55
|
+
*/
|
|
56
|
+
export function customDecodeEventLog<
|
|
57
|
+
const abi extends Abi | readonly unknown[],
|
|
58
|
+
eventName extends ContractEventName<abi> | undefined = undefined,
|
|
59
|
+
topics extends Hex[] = Hex[],
|
|
60
|
+
data extends Hex | undefined = undefined,
|
|
61
|
+
strict extends boolean = true,
|
|
62
|
+
>(
|
|
63
|
+
parameters: DecodeEventLogParameters<abi, eventName, topics, data, strict>
|
|
64
|
+
): DecodeEventLogReturnType<abi, eventName, topics, data, strict> {
|
|
65
|
+
const {
|
|
66
|
+
abi,
|
|
67
|
+
data,
|
|
68
|
+
strict: strict_,
|
|
69
|
+
topics,
|
|
70
|
+
} = parameters as DecodeEventLogParameters
|
|
71
|
+
|
|
72
|
+
const strict = strict_ ?? true
|
|
73
|
+
const [signature, ...argTopics] = topics
|
|
74
|
+
if (!signature) throw new AbiEventSignatureEmptyTopicsError({ docsPath })
|
|
75
|
+
|
|
76
|
+
const abiItem = (() => {
|
|
77
|
+
if (abi.length === 1) return abi[0]
|
|
78
|
+
return abi.find(
|
|
79
|
+
(x) =>
|
|
80
|
+
x.type === 'event' &&
|
|
81
|
+
signature === toEventSelector(formatAbiItem(x) as EventDefinition)
|
|
82
|
+
)
|
|
83
|
+
})()
|
|
84
|
+
|
|
85
|
+
if (!(abiItem && 'name' in abiItem) || abiItem.type !== 'event')
|
|
86
|
+
throw new AbiEventSignatureNotFoundError(signature, { docsPath })
|
|
87
|
+
|
|
88
|
+
const { name, inputs } = abiItem
|
|
89
|
+
const isUnnamed = inputs?.some((x) => !('name' in x && x.name))
|
|
90
|
+
|
|
91
|
+
let args: unknown[] | Record<string, unknown> = isUnnamed ? [] : {}
|
|
92
|
+
|
|
93
|
+
// Decode topics (indexed args).
|
|
94
|
+
const indexedInputs = inputs.filter((x) => 'indexed' in x && x.indexed)
|
|
95
|
+
for (let i = 0; i < indexedInputs.length; i++) {
|
|
96
|
+
const param = indexedInputs[i]
|
|
97
|
+
const topic = argTopics[i]
|
|
98
|
+
if (!topic)
|
|
99
|
+
throw new DecodeLogTopicsMismatch({
|
|
100
|
+
abiItem,
|
|
101
|
+
param: param as AbiParameter & { indexed: boolean },
|
|
102
|
+
})
|
|
103
|
+
if (isUnnamed && Array.isArray(args)) {
|
|
104
|
+
args[i] = decodeTopic({ param, value: topic })
|
|
105
|
+
} else if (!isUnnamed && !Array.isArray(args)) {
|
|
106
|
+
args[param.name || i] = decodeTopic({ param, value: topic })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Decode data (non-indexed args).
|
|
111
|
+
const nonIndexedInputs = inputs.filter((x) => !('indexed' in x && x.indexed))
|
|
112
|
+
if (nonIndexedInputs.length > 0) {
|
|
113
|
+
if (data && data !== '0x') {
|
|
114
|
+
try {
|
|
115
|
+
const decodedData = decodeAbiParameters(nonIndexedInputs, data)
|
|
116
|
+
if (decodedData) {
|
|
117
|
+
if (isUnnamed && Array.isArray(args)) {
|
|
118
|
+
args = [...args, ...decodedData]
|
|
119
|
+
} else if (!isUnnamed && !Array.isArray(args)) {
|
|
120
|
+
for (let i = 0; i < nonIndexedInputs.length; i++) {
|
|
121
|
+
const name = nonIndexedInputs[i].name
|
|
122
|
+
if (name) {
|
|
123
|
+
args[name] = decodedData[i]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (strict) {
|
|
130
|
+
if (
|
|
131
|
+
err instanceof AbiDecodingDataSizeTooSmallError ||
|
|
132
|
+
(err as { name?: string }).name === 'PositionOutOfBoundsError' // Unfortunately, this is not exported from viem
|
|
133
|
+
)
|
|
134
|
+
throw new DecodeLogDataMismatch({
|
|
135
|
+
abiItem,
|
|
136
|
+
data: data,
|
|
137
|
+
params: nonIndexedInputs,
|
|
138
|
+
size: size(data),
|
|
139
|
+
})
|
|
140
|
+
throw err
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else if (strict) {
|
|
144
|
+
throw new DecodeLogDataMismatch({
|
|
145
|
+
abiItem,
|
|
146
|
+
data: '0x',
|
|
147
|
+
params: nonIndexedInputs,
|
|
148
|
+
size: 0,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const { args: namedArgs } = createNamedArgs(
|
|
153
|
+
Array.isArray(args) ? args : Object.values(args),
|
|
154
|
+
inputs
|
|
155
|
+
)
|
|
156
|
+
return {
|
|
157
|
+
eventName: name,
|
|
158
|
+
args: namedArgs,
|
|
159
|
+
} as unknown as DecodeEventLogReturnType<abi, eventName, topics, data, strict>
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Decode events
|
|
164
|
+
*
|
|
165
|
+
* @param log Transaction to decode (should have data/input)
|
|
166
|
+
* @returns Promise<ResultType>
|
|
167
|
+
*/
|
|
168
|
+
export function decodeEvent<abi extends Abi | readonly unknown[] = Abi>(
|
|
169
|
+
chain: Chain,
|
|
170
|
+
abi: abi,
|
|
171
|
+
log: DecodeEventResult
|
|
172
|
+
):
|
|
173
|
+
| ({
|
|
174
|
+
eventName: string
|
|
175
|
+
args: ArrayArgs
|
|
176
|
+
} & DecodeEventResult)
|
|
177
|
+
| undefined {
|
|
178
|
+
const { data: _data, address, ...rest } = log as Log
|
|
179
|
+
|
|
180
|
+
let lastError: Error | undefined
|
|
181
|
+
try {
|
|
182
|
+
const result = customDecodeEventLog({
|
|
183
|
+
abi,
|
|
184
|
+
data: _data,
|
|
185
|
+
topics: (log as Log).topics || [],
|
|
186
|
+
})
|
|
187
|
+
return { ...log, ...result } as unknown as {
|
|
188
|
+
eventName: string
|
|
189
|
+
args: ArrayArgs
|
|
190
|
+
} & DecodeEventResult
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Abi,
|
|
3
|
+
type AbiParameter,
|
|
4
|
+
type Address,
|
|
5
|
+
type Chain,
|
|
6
|
+
decodeAbiParameters,
|
|
7
|
+
encodeAbiParameters,
|
|
8
|
+
getAddress,
|
|
9
|
+
isAddress,
|
|
10
|
+
isAddressEqual,
|
|
11
|
+
parseAbiParameters,
|
|
12
|
+
zeroAddress,
|
|
13
|
+
} from 'viem'
|
|
14
|
+
import { CacheKeyPrefix, makeCacheKey } from './cache'
|
|
15
|
+
import { FUNCTION_DICTIONARY_URL, OPENCHAIN_DICTIONARY_URL } from './constants'
|
|
16
|
+
import { createLRUCache } from './lruCache'
|
|
17
|
+
import type { DecoderOptions, ResultType } from './types'
|
|
18
|
+
import { createNamedArgs, customDecodeFunctionData } from './utils'
|
|
19
|
+
|
|
20
|
+
// Default cache for backward compatibility
|
|
21
|
+
const defaultCache = createLRUCache({
|
|
22
|
+
max: 500,
|
|
23
|
+
ttl: 60 * 1000, // 1 minute
|
|
24
|
+
errorTTL: 30 * 1000, // 30 seconds
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decode function parameter types from a function signature
|
|
29
|
+
*
|
|
30
|
+
* @param signature Function signature
|
|
31
|
+
* @returns Array of parameter types
|
|
32
|
+
* @throws Error if the signature is invalid
|
|
33
|
+
*/
|
|
34
|
+
function generateNamedAbiParameters(signature: string): AbiParameter[] {
|
|
35
|
+
const start = signature.indexOf('(')
|
|
36
|
+
const end = signature.lastIndexOf(')')
|
|
37
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
38
|
+
throw new Error(`Invalid signature: ${signature}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const paramString = signature.slice(start + 1, end)
|
|
42
|
+
|
|
43
|
+
const rawParams = parseAbiParameters(paramString) as AbiParameter[]
|
|
44
|
+
|
|
45
|
+
const assignNames = (params: AbiParameter[], prefix: string) => {
|
|
46
|
+
return params.map((param: AbiParameter, index: number) => {
|
|
47
|
+
const name = `${prefix}${index + 1}`
|
|
48
|
+
if (param.type === 'tuple' && 'components' in param) {
|
|
49
|
+
const components: AbiParameter[] = assignNames(
|
|
50
|
+
param.components as AbiParameter[],
|
|
51
|
+
`${name}_`
|
|
52
|
+
)
|
|
53
|
+
return { ...param, name, components } as AbiParameter
|
|
54
|
+
}
|
|
55
|
+
return { ...param, name } as AbiParameter
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return assignNames(rawParams, 'arg')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchAbi(
|
|
63
|
+
chain: Chain,
|
|
64
|
+
address: Address,
|
|
65
|
+
options?: DecoderOptions
|
|
66
|
+
) {
|
|
67
|
+
const explorer = chain.blockExplorers?.default?.apiUrl
|
|
68
|
+
if (!explorer) return undefined
|
|
69
|
+
|
|
70
|
+
const cache = options?.cache ?? defaultCache
|
|
71
|
+
let key = getAddress(address)
|
|
72
|
+
const cacheKey = makeCacheKey(CacheKeyPrefix.ABI, key, String(chain.id))
|
|
73
|
+
|
|
74
|
+
return cache.getOrSet(
|
|
75
|
+
cacheKey,
|
|
76
|
+
async (signal) => {
|
|
77
|
+
let isProxy = false
|
|
78
|
+
let decoderVerifiedContract = false
|
|
79
|
+
let factoryName: string | undefined
|
|
80
|
+
const originalKey = key
|
|
81
|
+
|
|
82
|
+
const addressUrl = new URL(`/api/v2/addresses/${key}`, explorer)
|
|
83
|
+
const addressRes = await fetch(addressUrl, { signal })
|
|
84
|
+
if (addressRes.ok) {
|
|
85
|
+
const data = (await addressRes.json()) as {
|
|
86
|
+
implementations?: { address?: Address; address_hash?: Address }[]
|
|
87
|
+
is_contract?: boolean
|
|
88
|
+
is_verified?: boolean
|
|
89
|
+
proxy_type?: string
|
|
90
|
+
name?: string
|
|
91
|
+
}
|
|
92
|
+
const {
|
|
93
|
+
implementations: [
|
|
94
|
+
{ address: _address, address_hash: _addressHash } = {},
|
|
95
|
+
] = [],
|
|
96
|
+
is_contract,
|
|
97
|
+
is_verified: __decoderVerifiedContract,
|
|
98
|
+
proxy_type,
|
|
99
|
+
name: __factoryName,
|
|
100
|
+
} = data
|
|
101
|
+
const address = _address || _addressHash
|
|
102
|
+
factoryName = __factoryName
|
|
103
|
+
decoderVerifiedContract = Boolean(__decoderVerifiedContract || false)
|
|
104
|
+
if (
|
|
105
|
+
address &&
|
|
106
|
+
is_contract &&
|
|
107
|
+
proxy_type !== 'null' &&
|
|
108
|
+
proxy_type != null
|
|
109
|
+
) {
|
|
110
|
+
key = address as `0x${string}`
|
|
111
|
+
isProxy = true // We resolved from proxy to implementation
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!decoderVerifiedContract) {
|
|
116
|
+
// If the address is not a verified contract, return undefined
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const url = new URL(`/api/v2/smart-contracts/${key}`, explorer)
|
|
121
|
+
const res = await fetch(url, { signal })
|
|
122
|
+
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await res.json()
|
|
128
|
+
const {
|
|
129
|
+
name = factoryName || 'UnknownName',
|
|
130
|
+
abi,
|
|
131
|
+
is_verified,
|
|
132
|
+
} = result || {}
|
|
133
|
+
|
|
134
|
+
if (abi && is_verified) {
|
|
135
|
+
return {
|
|
136
|
+
abi,
|
|
137
|
+
__decoderVerifiedContract: decoderVerifiedContract, // If we have an ABI, treat it as verified
|
|
138
|
+
__decoderIsProxy: isProxy,
|
|
139
|
+
name,
|
|
140
|
+
explorer,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined
|
|
144
|
+
},
|
|
145
|
+
{ signal: options?.signal }
|
|
146
|
+
) as Promise<
|
|
147
|
+
| {
|
|
148
|
+
abi: Abi
|
|
149
|
+
__decoderVerifiedContract: boolean
|
|
150
|
+
__decoderIsProxy: boolean
|
|
151
|
+
name: string
|
|
152
|
+
explorer: string
|
|
153
|
+
}
|
|
154
|
+
| undefined
|
|
155
|
+
>
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets a function signature. Returns an empty string if hexSignature
|
|
160
|
+
* is not provided or an error is thrown.
|
|
161
|
+
* If hexSignature is provided, it checks the cache storage.
|
|
162
|
+
* If the signature is not found, it fetches 4bytes dictionary and stores
|
|
163
|
+
* the result in the cache storage.
|
|
164
|
+
*
|
|
165
|
+
* @param string hexSignature - first 4 bytes of a payload
|
|
166
|
+
* @param boolean preferError - if true, prefer error signatures
|
|
167
|
+
* @returns function signature as a string
|
|
168
|
+
*/
|
|
169
|
+
export const getFunctionSignature = async (
|
|
170
|
+
chain: Chain,
|
|
171
|
+
input?: `0x${string}`,
|
|
172
|
+
to?: `0x${string}`,
|
|
173
|
+
preferError = false,
|
|
174
|
+
options?: DecoderOptions
|
|
175
|
+
): Promise<
|
|
176
|
+
ResultType & {
|
|
177
|
+
__decoder: string
|
|
178
|
+
__decoderVerifiedContract?: boolean
|
|
179
|
+
__decoderIsProxy?: boolean
|
|
180
|
+
standard?: string
|
|
181
|
+
}
|
|
182
|
+
> => {
|
|
183
|
+
// Remove 0x prefix
|
|
184
|
+
if (to && isAddress(to) && isAddressEqual(to, zeroAddress)) {
|
|
185
|
+
return {
|
|
186
|
+
__decoder: 'none',
|
|
187
|
+
standard: 'unknown',
|
|
188
|
+
to,
|
|
189
|
+
input,
|
|
190
|
+
resultType: 'raw',
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const data = input?.slice(2) as string
|
|
194
|
+
// Get the first 8 chars (4 bytes) of the data
|
|
195
|
+
const selector = data.slice(0, 8)
|
|
196
|
+
if (selector) {
|
|
197
|
+
if (to) {
|
|
198
|
+
// Wait for cached or new promise to resolve
|
|
199
|
+
const info = await fetchAbi(chain, to, options)
|
|
200
|
+
if (info) {
|
|
201
|
+
// The explorer returned it as a verified abi.
|
|
202
|
+
// Use the returned abi to decode the function signature.
|
|
203
|
+
const decoded = customDecodeFunctionData({
|
|
204
|
+
abi: info.abi,
|
|
205
|
+
data: input || '0x',
|
|
206
|
+
}) as ResultType & {
|
|
207
|
+
sig?: `0x${string}`
|
|
208
|
+
functionName?: string
|
|
209
|
+
args: readonly unknown[]
|
|
210
|
+
}
|
|
211
|
+
if (decoded) {
|
|
212
|
+
return {
|
|
213
|
+
...decoded,
|
|
214
|
+
__decoder: info.explorer,
|
|
215
|
+
__decoderVerifiedContract: info.__decoderVerifiedContract,
|
|
216
|
+
__decoderIsProxy: info.__decoderIsProxy,
|
|
217
|
+
standard: info.name,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// If we didn't resolve it yet, then try to fetch from one of the 4bytes dictionaries.
|
|
223
|
+
const cache = options?.cache ?? defaultCache
|
|
224
|
+
const cacheKey = makeCacheKey(CacheKeyPrefix.FUNCTION_SIGNATURE, selector)
|
|
225
|
+
|
|
226
|
+
const signatureEntry = cache.getOrSet(
|
|
227
|
+
cacheKey,
|
|
228
|
+
async (signal) => {
|
|
229
|
+
// Try our proxy first.
|
|
230
|
+
const url = `${OPENCHAIN_DICTIONARY_URL}?function=0x${selector}`
|
|
231
|
+
let __decoder = new URL(OPENCHAIN_DICTIONARY_URL).hostname
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const res = await fetch(url, { signal })
|
|
235
|
+
if (res.ok) {
|
|
236
|
+
const result = await res.json()
|
|
237
|
+
if (result) {
|
|
238
|
+
result.__decoder = __decoder
|
|
239
|
+
return result
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
// Continue to fallback
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Fallback to function dictionary
|
|
247
|
+
__decoder = new URL(FUNCTION_DICTIONARY_URL).hostname
|
|
248
|
+
const fallbackRes = await fetch(
|
|
249
|
+
`${FUNCTION_DICTIONARY_URL}/${selector}`,
|
|
250
|
+
{ signal }
|
|
251
|
+
)
|
|
252
|
+
if (!fallbackRes.ok) {
|
|
253
|
+
return undefined
|
|
254
|
+
}
|
|
255
|
+
const fallbackResult = await fallbackRes.json()
|
|
256
|
+
if (fallbackResult) {
|
|
257
|
+
fallbackResult.__decoder = __decoder
|
|
258
|
+
}
|
|
259
|
+
return fallbackResult
|
|
260
|
+
},
|
|
261
|
+
{ signal: options?.signal }
|
|
262
|
+
)
|
|
263
|
+
// Wait for cached or new promise to resolve
|
|
264
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is easier in this particular case
|
|
265
|
+
const methods = (await signatureEntry) as any
|
|
266
|
+
if (methods?.results?.length > 0) {
|
|
267
|
+
// We got one or more results.
|
|
268
|
+
let search = methods.results.reverse()
|
|
269
|
+
if (preferError) {
|
|
270
|
+
// In case we're preferring Errors,
|
|
271
|
+
// then look for all methods containing Error
|
|
272
|
+
// and put them first.
|
|
273
|
+
const withError = search.filter((result: Record<string, unknown>) =>
|
|
274
|
+
/Error/.test(result.text_signature as string)
|
|
275
|
+
)
|
|
276
|
+
const withoutError = search.filter(
|
|
277
|
+
(result: Record<string, unknown>) =>
|
|
278
|
+
!/Error/.test(result.text_signature as string)
|
|
279
|
+
)
|
|
280
|
+
search = [...withError, ...withoutError]
|
|
281
|
+
}
|
|
282
|
+
// Go through each one and try to encode/decode the data
|
|
283
|
+
// to make sure it works.
|
|
284
|
+
for (const result of search) {
|
|
285
|
+
try {
|
|
286
|
+
const params: AbiParameter[] = generateNamedAbiParameters(
|
|
287
|
+
result.text_signature || result.name
|
|
288
|
+
)
|
|
289
|
+
// Decode arguments according to function spec
|
|
290
|
+
const args = decodeAbiParameters(params, `0x${data.slice(8)}`)
|
|
291
|
+
|
|
292
|
+
// Build arg list (if there are bool values which are no 0 or 1 we can assume it's invalid)
|
|
293
|
+
// Other values are validated in encodeParameters.
|
|
294
|
+
const encodeArgs = Array.from({ length: params.length }).map(
|
|
295
|
+
(_val, index) => {
|
|
296
|
+
if (params[index].type === 'bool') {
|
|
297
|
+
const val = args[`${index}`]
|
|
298
|
+
if (val !== 1 && val !== 0 && val !== true && val !== false) {
|
|
299
|
+
// Due to javascript truthyness any non-empty or non-zero value is encoded as true without a problem.
|
|
300
|
+
// The output packet won't match but there is really no reason to even try.
|
|
301
|
+
throw new Error('Invalid boolean value')
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (params[index].type === 'address') {
|
|
305
|
+
const val = args[`${index}`] as `0x${string}`
|
|
306
|
+
if (!isAddress(val)) {
|
|
307
|
+
throw new Error(`Invalid address value ${val}`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return args[`${index}`] ?? '0x'
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
// Re-encode according to function spec.
|
|
315
|
+
const newData = encodeAbiParameters(
|
|
316
|
+
params,
|
|
317
|
+
encodeArgs as never
|
|
318
|
+
).slice(2)
|
|
319
|
+
|
|
320
|
+
if (data.slice(8) === newData) {
|
|
321
|
+
return {
|
|
322
|
+
resultType: 'execute',
|
|
323
|
+
standard: 'unknown',
|
|
324
|
+
sig: `0x${selector}`,
|
|
325
|
+
functionName: result.text_signature.replace(/\(.*$/, ''),
|
|
326
|
+
...(args
|
|
327
|
+
? {
|
|
328
|
+
...createNamedArgs(encodeArgs, params),
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
331
|
+
__decoder: methods.__decoder || FUNCTION_DICTIONARY_URL,
|
|
332
|
+
} as ResultType & {
|
|
333
|
+
__decoder: string
|
|
334
|
+
__decoderVerifiedContract?: boolean
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
// Ignore to try next record
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return undefined
|
|
344
|
+
}
|