@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,248 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DecodeDataOutput,
|
|
3
|
+
ERC725JSONSchema,
|
|
4
|
+
GetDataExternalSourcesOutput,
|
|
5
|
+
URLDataWithHash,
|
|
6
|
+
} from '@erc725/erc725.js'
|
|
7
|
+
import { isDataAuthentic, patchIPFSUrlsIfApplicable } from '@erc725/erc725.js'
|
|
8
|
+
|
|
9
|
+
export async function loadFile(
|
|
10
|
+
_url: string,
|
|
11
|
+
ipfsGateway: string
|
|
12
|
+
): Promise<ArrayBuffer | null> {
|
|
13
|
+
let url = _url
|
|
14
|
+
if (url.startsWith('data:')) {
|
|
15
|
+
// Handle data URLs
|
|
16
|
+
const base64Data = url.split(',')[1]
|
|
17
|
+
const binaryString = atob(base64Data)
|
|
18
|
+
const len = binaryString.length
|
|
19
|
+
const bytes = new Uint8Array(len)
|
|
20
|
+
for (let i = 0; i < len; i++) {
|
|
21
|
+
bytes[i] = binaryString.charCodeAt(i)
|
|
22
|
+
}
|
|
23
|
+
return Promise.resolve(bytes.buffer)
|
|
24
|
+
}
|
|
25
|
+
if (url.startsWith('ipfs://')) {
|
|
26
|
+
// Handle IPFS URLs
|
|
27
|
+
const ipfsHash = url.replace('ipfs://', '')
|
|
28
|
+
url = `${ipfsGateway}${ipfsHash}`
|
|
29
|
+
}
|
|
30
|
+
return fetch(url)
|
|
31
|
+
.then((response) => {
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error('Network response was not ok')
|
|
34
|
+
}
|
|
35
|
+
return response.arrayBuffer()
|
|
36
|
+
})
|
|
37
|
+
.catch(() => null)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const getDataFromExternalSources = (
|
|
41
|
+
schemas: ERC725JSONSchema[],
|
|
42
|
+
dataFromChain: DecodeDataOutput[],
|
|
43
|
+
ipfsGateway: string,
|
|
44
|
+
throwException = true
|
|
45
|
+
): Promise<
|
|
46
|
+
Array<
|
|
47
|
+
GetDataExternalSourcesOutput & {
|
|
48
|
+
error?: string
|
|
49
|
+
method?: string
|
|
50
|
+
data?: string
|
|
51
|
+
url?: string
|
|
52
|
+
src?: string
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
> => {
|
|
56
|
+
const promises = dataFromChain.map(async (dataEntry) => {
|
|
57
|
+
const schemaElement = schemas.find((schema) => schema.key === dataEntry.key)
|
|
58
|
+
|
|
59
|
+
if (!schemaElement) {
|
|
60
|
+
// It is weird if we can't find the schema element for the key...
|
|
61
|
+
// Let's simply ignore and return it...
|
|
62
|
+
return dataEntry
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!['jsonurl', 'asseturl', 'verifiableuri'].includes(
|
|
67
|
+
schemaElement.valueContent.toLowerCase()
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
return dataEntry
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const urlDataWithHash: URLDataWithHash = dataEntry.value as URLDataWithHash // Type URLDataWithHash
|
|
74
|
+
let url: string | undefined
|
|
75
|
+
let src: string | undefined
|
|
76
|
+
try {
|
|
77
|
+
// At this stage, value should be of type jsonurl, verifiableuri or asseturl
|
|
78
|
+
if (typeof dataEntry.value === 'string') {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Value is string instead of ${schemaElement.valueContent}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!dataEntry.value) {
|
|
85
|
+
throw new Error(`${schemaElement.valueContent} empty`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(dataEntry.value)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Value is Array instead of ${schemaElement.valueContent}`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
url = urlDataWithHash.url
|
|
95
|
+
;({ url: src } = patchIPFSUrlsIfApplicable(
|
|
96
|
+
urlDataWithHash as URLDataWithHash,
|
|
97
|
+
ipfsGateway
|
|
98
|
+
))
|
|
99
|
+
let length = 'unknown'
|
|
100
|
+
if (!url.startsWith('data:') && /[=?/]$/.test(url)) {
|
|
101
|
+
// this URL is not verifiable and the URL ends with a / or ? or = meaning it's not a file
|
|
102
|
+
// and more likely to be some kind of directory or query BaseU=== "errorRI
|
|
103
|
+
return dataEntry
|
|
104
|
+
}
|
|
105
|
+
const receivedData = /^data:|^ipfs:\/\/|\/ipfs\//.test(url)
|
|
106
|
+
? await loadFile(src, ipfsGateway).then((buffer) =>
|
|
107
|
+
buffer
|
|
108
|
+
? new Uint8Array(buffer)
|
|
109
|
+
: Promise.reject(new Error('No Data'))
|
|
110
|
+
)
|
|
111
|
+
: await fetch(src).then(async (response) => {
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(response.statusText)
|
|
114
|
+
}
|
|
115
|
+
return response
|
|
116
|
+
.arrayBuffer()
|
|
117
|
+
.then((buffer) => new Uint8Array(buffer))
|
|
118
|
+
})
|
|
119
|
+
length = receivedData.length.toString()
|
|
120
|
+
const captureHashes: Record<string, string> = {}
|
|
121
|
+
const captureErrors: string[] = []
|
|
122
|
+
if (receivedData.length >= 2) {
|
|
123
|
+
// JSON data cannot be less than 2 characters long.
|
|
124
|
+
try {
|
|
125
|
+
// - Build a string containing the first and last byte of the received data
|
|
126
|
+
// and try to convert it to utf8. If that succeeds then
|
|
127
|
+
// - check whether those could represent valid JSON data.
|
|
128
|
+
// - then validate the data as JSON
|
|
129
|
+
// - then verfiy the data against the verification method
|
|
130
|
+
// Improved JSON detection. We now check the first and up to the last 3 bytes.
|
|
131
|
+
// The 3 bytes can be `]` or '}' with either SPACE, LF or CRLF at the end.
|
|
132
|
+
// When editing JSON using a text editor, a lot of time it's pretty printed
|
|
133
|
+
// and an empty line added to the end. This is a common pattern.
|
|
134
|
+
const capture: number[] = []
|
|
135
|
+
capture.push(receivedData[0])
|
|
136
|
+
if (receivedData.length > 3) {
|
|
137
|
+
capture.push(receivedData[receivedData.length - 3])
|
|
138
|
+
}
|
|
139
|
+
if (receivedData.length > 2) {
|
|
140
|
+
capture.push(receivedData[receivedData.length - 2])
|
|
141
|
+
}
|
|
142
|
+
if (receivedData.length > 1) {
|
|
143
|
+
capture.push(receivedData[receivedData.length - 1])
|
|
144
|
+
}
|
|
145
|
+
const key = String.fromCharCode.apply(null, capture)
|
|
146
|
+
// Currently not supported even though they could be added and can represent valid JSON.
|
|
147
|
+
// " " => JSON.stringify("") NOT SUPPORTED as valid JSON
|
|
148
|
+
// t or f and e => JSON.stringify(true) or JSON.stringify(false) NOT SUPPORTED as valid JSON
|
|
149
|
+
// 0-9 => JSON.stringify(0) integer or float (note .5 is not legitimate JSON) NOT SUPPORTED as valid JSON
|
|
150
|
+
// if (/^(\[\]|\{\}|(tf)e|\d\d)$/.test(key)) {
|
|
151
|
+
// Check if the beginning or end are
|
|
152
|
+
// { and } => JSON.stringify({...}) => pretty much 100% of our JSON will be this.
|
|
153
|
+
// [ and ] => JSON.stringify([...])
|
|
154
|
+
if (/^(\[.*\]|\{.*\})\s*$/s.test(key)) {
|
|
155
|
+
const json = new TextDecoder().decode(new Uint8Array(receivedData))
|
|
156
|
+
const value = JSON.parse(json)
|
|
157
|
+
if (
|
|
158
|
+
isDataAuthentic(
|
|
159
|
+
value,
|
|
160
|
+
urlDataWithHash.verification,
|
|
161
|
+
captureErrors
|
|
162
|
+
)
|
|
163
|
+
) {
|
|
164
|
+
return {
|
|
165
|
+
...dataEntry,
|
|
166
|
+
value,
|
|
167
|
+
data: urlDataWithHash.verification?.data,
|
|
168
|
+
method: urlDataWithHash.verification?.method,
|
|
169
|
+
url,
|
|
170
|
+
src,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
captureHashes[captureErrors.pop() || ''] =
|
|
174
|
+
urlDataWithHash.verification?.data || ''
|
|
175
|
+
if (
|
|
176
|
+
isDataAuthentic(
|
|
177
|
+
receivedData,
|
|
178
|
+
urlDataWithHash.verification,
|
|
179
|
+
captureErrors
|
|
180
|
+
)
|
|
181
|
+
) {
|
|
182
|
+
return {
|
|
183
|
+
...dataEntry,
|
|
184
|
+
value,
|
|
185
|
+
data: urlDataWithHash.verification?.data,
|
|
186
|
+
method: urlDataWithHash.verification?.method,
|
|
187
|
+
url,
|
|
188
|
+
src,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
captureHashes[captureErrors.pop() || ''] =
|
|
192
|
+
urlDataWithHash.verification?.data || ''
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Data does not validate, ignored ${Object.entries(captureHashes)
|
|
195
|
+
.map(
|
|
196
|
+
([expected, got]) =>
|
|
197
|
+
`calculated(${got}) != expected(${expected})`
|
|
198
|
+
)
|
|
199
|
+
.join(', ')}`
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// ignore
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
isDataAuthentic(
|
|
208
|
+
receivedData,
|
|
209
|
+
urlDataWithHash.verification,
|
|
210
|
+
captureErrors
|
|
211
|
+
)
|
|
212
|
+
) {
|
|
213
|
+
return {
|
|
214
|
+
...dataEntry,
|
|
215
|
+
value: receivedData,
|
|
216
|
+
data: urlDataWithHash.verification?.data,
|
|
217
|
+
method: urlDataWithHash.verification?.method,
|
|
218
|
+
url,
|
|
219
|
+
src,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
captureHashes[captureErrors.pop() || ''] =
|
|
223
|
+
urlDataWithHash.verification?.data || ''
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Data does not validate, ignored ${Object.entries(captureHashes)
|
|
226
|
+
.map(
|
|
227
|
+
([expected, got]) => `calculated(${got}) != expected(${expected})`
|
|
228
|
+
)
|
|
229
|
+
.join(', ')}`
|
|
230
|
+
)
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const message = (error as { message: string }).message
|
|
233
|
+
if (throwException) {
|
|
234
|
+
throw error
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
...dataEntry,
|
|
238
|
+
value: null,
|
|
239
|
+
error: message,
|
|
240
|
+
data: urlDataWithHash.verification?.data,
|
|
241
|
+
method: urlDataWithHash.verification?.method,
|
|
242
|
+
url,
|
|
243
|
+
src,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
return Promise.all(promises)
|
|
248
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Client, createClient } from 'graphql-ws'
|
|
2
|
+
import type { Chain } from 'viem'
|
|
3
|
+
import { lukso } from 'viem/chains'
|
|
4
|
+
|
|
5
|
+
const wsClients: Record<string, Client> = {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get web socket graphql client for a given chain
|
|
9
|
+
*
|
|
10
|
+
* @param chain Chain to use
|
|
11
|
+
* @returns web socket graphql client
|
|
12
|
+
*/
|
|
13
|
+
export function getWSClient(chain: Chain) {
|
|
14
|
+
if (wsClients[chain.id]) return wsClients[chain.id]
|
|
15
|
+
wsClients[chain.id] = createClient({
|
|
16
|
+
url:
|
|
17
|
+
chain.id === lukso.id
|
|
18
|
+
? 'wss://envio.lukso-mainnet.universal.tech/v1/graphql'
|
|
19
|
+
: 'wss://envio.lukso-testnet.universal.tech/v1/graphql',
|
|
20
|
+
})
|
|
21
|
+
return wsClients[chain.id]
|
|
22
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { abi as KeyManager } from '@lukso/lsp6-contracts/artifacts/LSP6KeyManager.json'
|
|
2
|
+
import { LRUCache } from 'lru-cache'
|
|
3
|
+
import {
|
|
4
|
+
type Chain,
|
|
5
|
+
type ChainContract,
|
|
6
|
+
encodeFunctionData,
|
|
7
|
+
getContract,
|
|
8
|
+
} from 'viem'
|
|
9
|
+
import { ITEMS } from './constants'
|
|
10
|
+
import type { AddressModifier } from './types'
|
|
11
|
+
import { getPublicClient } from './utils'
|
|
12
|
+
|
|
13
|
+
const TARGETS = new LRUCache<string, Promise<`0x${string}` | undefined>>({
|
|
14
|
+
max: 1000,
|
|
15
|
+
ttl: 1000 * 60 * 60,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the profile from the key manager.
|
|
20
|
+
*
|
|
21
|
+
* @param chain which chain to use
|
|
22
|
+
* @param address address of key manager
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
export async function getKMTarget(
|
|
26
|
+
chain: Chain,
|
|
27
|
+
address?: `0x${string}` | null
|
|
28
|
+
): Promise<`0x${string}` | undefined> {
|
|
29
|
+
if (!address) {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
const info = TARGETS.get(address)
|
|
33
|
+
if (info) {
|
|
34
|
+
return info
|
|
35
|
+
}
|
|
36
|
+
const client = getPublicClient(chain)
|
|
37
|
+
const out = client
|
|
38
|
+
.readContract({
|
|
39
|
+
address,
|
|
40
|
+
abi: KeyManager,
|
|
41
|
+
functionName: 'target',
|
|
42
|
+
})
|
|
43
|
+
.catch(() => undefined) as Promise<`0x${string}` | undefined>
|
|
44
|
+
TARGETS.set(address, out)
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const INTERFACE_INFO = new LRUCache<string, Promise<string | undefined>>({
|
|
49
|
+
max: 1000,
|
|
50
|
+
ttl: 1000 * 60 * 60,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Retrieve interface information, LSP7/8/0 for an address
|
|
55
|
+
*
|
|
56
|
+
* @param address
|
|
57
|
+
* @param blockNumber
|
|
58
|
+
* @param log
|
|
59
|
+
* @returns
|
|
60
|
+
*/
|
|
61
|
+
export async function retrieveInterfaces(
|
|
62
|
+
chain: Chain,
|
|
63
|
+
modifier: AddressModifier
|
|
64
|
+
): Promise<string | undefined> {
|
|
65
|
+
const info = INTERFACE_INFO.get(modifier.address)
|
|
66
|
+
if (info) {
|
|
67
|
+
return info
|
|
68
|
+
}
|
|
69
|
+
const out = _retrieveInterfaces(chain, modifier)
|
|
70
|
+
INTERFACE_INFO.set(modifier.address, out)
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const LSP2Fetcher = [
|
|
75
|
+
{
|
|
76
|
+
inputs: [
|
|
77
|
+
{
|
|
78
|
+
internalType: 'bytes4',
|
|
79
|
+
name: 'interfaceId',
|
|
80
|
+
type: 'bytes4',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
name: 'supportsInterface',
|
|
84
|
+
outputs: [
|
|
85
|
+
{
|
|
86
|
+
internalType: 'bool',
|
|
87
|
+
name: '',
|
|
88
|
+
type: 'bool',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
stateMutability: 'view',
|
|
92
|
+
type: 'function',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'function',
|
|
96
|
+
name: 'aggregate3',
|
|
97
|
+
inputs: [
|
|
98
|
+
{
|
|
99
|
+
name: 'calls',
|
|
100
|
+
type: 'tuple[]',
|
|
101
|
+
internalType: 'struct LSP2FetcherWithMulticall3.Call3[]',
|
|
102
|
+
components: [
|
|
103
|
+
{ name: 'target', type: 'address', internalType: 'address' },
|
|
104
|
+
{ name: 'allowFailure', type: 'bool', internalType: 'bool' },
|
|
105
|
+
{ name: 'callData', type: 'bytes', internalType: 'bytes' },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
outputs: [
|
|
110
|
+
{
|
|
111
|
+
name: 'returnData',
|
|
112
|
+
type: 'tuple[]',
|
|
113
|
+
internalType: 'struct LSP2FetcherWithMulticall3.Result[]',
|
|
114
|
+
components: [
|
|
115
|
+
{ name: 'success', type: 'bool', internalType: 'bool' },
|
|
116
|
+
{ name: 'returnData', type: 'bytes', internalType: 'bytes' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
stateMutability: 'payable',
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Lowlevel function to retrieve interface information
|
|
126
|
+
*
|
|
127
|
+
* @param address
|
|
128
|
+
* @returns
|
|
129
|
+
*/
|
|
130
|
+
async function _retrieveInterfaces(
|
|
131
|
+
chain: Chain,
|
|
132
|
+
modifier: AddressModifier
|
|
133
|
+
): Promise<string | undefined> {
|
|
134
|
+
const { multicall3 } = chain.contracts as Record<string, ChainContract>
|
|
135
|
+
if (!multicall3) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
const client = getPublicClient(chain)
|
|
139
|
+
const contract = getContract({
|
|
140
|
+
client,
|
|
141
|
+
address: multicall3.address,
|
|
142
|
+
abi: LSP2Fetcher,
|
|
143
|
+
})
|
|
144
|
+
const args = ITEMS.map(({ key }) => {
|
|
145
|
+
return {
|
|
146
|
+
target: modifier.address,
|
|
147
|
+
allowFailure: true,
|
|
148
|
+
callData: encodeFunctionData({
|
|
149
|
+
abi: LSP2Fetcher,
|
|
150
|
+
functionName: 'supportsInterface',
|
|
151
|
+
args: [key],
|
|
152
|
+
}),
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
return client
|
|
156
|
+
.readContract({
|
|
157
|
+
address: multicall3.address,
|
|
158
|
+
abi: LSP2Fetcher,
|
|
159
|
+
functionName: 'aggregate3',
|
|
160
|
+
args: [args],
|
|
161
|
+
})
|
|
162
|
+
.catch((error) => {
|
|
163
|
+
console.error(error)
|
|
164
|
+
return []
|
|
165
|
+
})
|
|
166
|
+
.then((val) => {
|
|
167
|
+
const response = val as Array<{ success: boolean; returnData: boolean }>
|
|
168
|
+
for (const [index, value] of response.map(
|
|
169
|
+
({ success, returnData }, index: number) =>
|
|
170
|
+
[index, success && Number(returnData)] as [number, boolean]
|
|
171
|
+
)) {
|
|
172
|
+
const item = ITEMS[index]
|
|
173
|
+
if (!item) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (value) {
|
|
177
|
+
return item.standard
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return undefined
|
|
181
|
+
})
|
|
182
|
+
.then((type) => {
|
|
183
|
+
return type
|
|
184
|
+
})
|
|
185
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KV (Key-Value store) implementation of DecoderCache interface
|
|
3
|
+
* Suitable for Cloudflare Workers KV, Deno KV, or similar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CacheEntry, CacheOptions, DecoderCache } from './cache'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* KV store interface that implementations must provide
|
|
10
|
+
*/
|
|
11
|
+
export interface KVStore {
|
|
12
|
+
get<T = unknown>(key: string): Promise<T | null>
|
|
13
|
+
put(
|
|
14
|
+
key: string,
|
|
15
|
+
value: string,
|
|
16
|
+
options?: { expirationTtl?: number }
|
|
17
|
+
): Promise<void>
|
|
18
|
+
delete(key: string): Promise<void>
|
|
19
|
+
list?(options?: {
|
|
20
|
+
prefix?: string
|
|
21
|
+
}): Promise<{ keys: Array<{ name: string }> }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for creating a KV cache instance
|
|
26
|
+
*/
|
|
27
|
+
export interface KVCacheOptions {
|
|
28
|
+
/** KV store instance (e.g., Cloudflare KV namespace) */
|
|
29
|
+
kv: KVStore
|
|
30
|
+
/** Prefix for all keys */
|
|
31
|
+
prefix?: string
|
|
32
|
+
/** Default TTL in milliseconds */
|
|
33
|
+
ttl?: number
|
|
34
|
+
/** Default error TTL in milliseconds */
|
|
35
|
+
errorTTL?: number
|
|
36
|
+
/** Default stale-while-revalidate time in milliseconds */
|
|
37
|
+
staleWhileRevalidate?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* KV implementation of DecoderCache
|
|
42
|
+
*/
|
|
43
|
+
export class KVDecoderCache implements DecoderCache {
|
|
44
|
+
private kv: KVStore
|
|
45
|
+
private prefix: string
|
|
46
|
+
private promises: Map<string, Promise<unknown>>
|
|
47
|
+
private defaultTTL: number
|
|
48
|
+
private defaultErrorTTL: number
|
|
49
|
+
private defaultStaleWhileRevalidate?: number
|
|
50
|
+
|
|
51
|
+
constructor(options: KVCacheOptions) {
|
|
52
|
+
this.kv = options.kv
|
|
53
|
+
this.prefix = options.prefix ?? 'decoder:'
|
|
54
|
+
this.defaultTTL = options.ttl ?? 5 * 60 * 1000 // 5 minutes
|
|
55
|
+
this.defaultErrorTTL = options.errorTTL ?? 30 * 1000 // 30 seconds
|
|
56
|
+
this.defaultStaleWhileRevalidate = options.staleWhileRevalidate
|
|
57
|
+
this.promises = new Map()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private getKey(key: string): string {
|
|
61
|
+
return `${this.prefix}${key}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async get<T>(key: string): Promise<CacheEntry<T> | undefined> {
|
|
65
|
+
try {
|
|
66
|
+
const data = await this.kv.get<CacheEntry<T>>(this.getKey(key))
|
|
67
|
+
if (!data) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if expired
|
|
72
|
+
if (data.expires && Date.now() > data.expires) {
|
|
73
|
+
// Don't delete here as it might be used for stale-while-revalidate
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return data
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('KV get error:', error)
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async set<T>(key: string, value: T, options?: CacheOptions): Promise<void> {
|
|
85
|
+
const now = Date.now()
|
|
86
|
+
const ttl =
|
|
87
|
+
options?.ttl ?? (options?.errorTTL ? options.errorTTL : this.defaultTTL)
|
|
88
|
+
const isError = options?.errorTTL !== undefined
|
|
89
|
+
|
|
90
|
+
const entry: CacheEntry<T> = {
|
|
91
|
+
value,
|
|
92
|
+
expires: now + ttl,
|
|
93
|
+
isError,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const staleWhileRevalidate =
|
|
97
|
+
options?.staleWhileRevalidate ?? this.defaultStaleWhileRevalidate
|
|
98
|
+
if (staleWhileRevalidate) {
|
|
99
|
+
entry.stale = now + ttl + staleWhileRevalidate
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Calculate actual TTL for KV store (including stale time)
|
|
104
|
+
const kvTtl = entry.stale ? entry.stale - now : ttl
|
|
105
|
+
|
|
106
|
+
await this.kv.put(
|
|
107
|
+
this.getKey(key),
|
|
108
|
+
JSON.stringify(entry),
|
|
109
|
+
{ expirationTtl: Math.ceil(kvTtl / 1000) } // KV usually wants seconds
|
|
110
|
+
)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('KV set error:', error)
|
|
113
|
+
throw error
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async delete(key: string): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
await this.kv.delete(this.getKey(key))
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('KV delete error:', error)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getMany<T>(keys: string[]): Promise<Map<string, CacheEntry<T>>> {
|
|
126
|
+
const result = new Map<string, CacheEntry<T>>()
|
|
127
|
+
|
|
128
|
+
// Most KV stores don't support batch get, so we do it in parallel
|
|
129
|
+
const promises = keys.map(async (key) => {
|
|
130
|
+
const entry = await this.get<T>(key)
|
|
131
|
+
if (entry) {
|
|
132
|
+
result.set(key, entry)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await Promise.all(promises)
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async setMany<T>(
|
|
141
|
+
entries: Array<{ key: string; value: T; options?: CacheOptions }>
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
// Most KV stores don't support batch set, so we do it in parallel
|
|
144
|
+
const promises = entries.map(({ key, value, options }) =>
|
|
145
|
+
this.set(key, value, options)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
await Promise.all(promises)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async clear(): Promise<void> {
|
|
152
|
+
// Clear in-flight promises
|
|
153
|
+
this.promises.clear()
|
|
154
|
+
|
|
155
|
+
// If KV supports listing, delete all with prefix
|
|
156
|
+
if (this.kv.list) {
|
|
157
|
+
try {
|
|
158
|
+
const { keys } = await this.kv.list({ prefix: this.prefix })
|
|
159
|
+
const deletePromises = keys.map(({ name }) => this.kv.delete(name))
|
|
160
|
+
await Promise.all(deletePromises)
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('KV clear error:', error)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getOrSet<T>(
|
|
168
|
+
key: string,
|
|
169
|
+
factory: (signal?: AbortSignal) => Promise<T>,
|
|
170
|
+
options?: CacheOptions & { signal?: AbortSignal }
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
// Check cache first
|
|
173
|
+
const cached = await this.get<T>(key)
|
|
174
|
+
const now = Date.now()
|
|
175
|
+
|
|
176
|
+
// If we have a valid (non-expired) entry, return it
|
|
177
|
+
if (cached && (!cached.expires || now < cached.expires)) {
|
|
178
|
+
return cached.value
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// If we have a stale entry and stale-while-revalidate is enabled
|
|
182
|
+
if (
|
|
183
|
+
cached &&
|
|
184
|
+
cached.stale &&
|
|
185
|
+
now < cached.stale &&
|
|
186
|
+
!this.promises.has(key)
|
|
187
|
+
) {
|
|
188
|
+
// Return stale value and revalidate in background
|
|
189
|
+
const backgroundPromise = factory(options?.signal)
|
|
190
|
+
.then(async (fresh) => {
|
|
191
|
+
await this.set(key, fresh, options)
|
|
192
|
+
return fresh
|
|
193
|
+
})
|
|
194
|
+
.catch(() => {
|
|
195
|
+
// On error, keep the stale value
|
|
196
|
+
return cached.value
|
|
197
|
+
})
|
|
198
|
+
.finally(() => {
|
|
199
|
+
this.promises.delete(key)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
this.promises.set(key, backgroundPromise)
|
|
203
|
+
return cached.value
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if we have an in-flight promise
|
|
207
|
+
const existingPromise = this.promises.get(key) as Promise<T> | undefined
|
|
208
|
+
if (existingPromise) {
|
|
209
|
+
return existingPromise
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// No valid cache entry, fetch new value
|
|
213
|
+
const promise = factory(options?.signal)
|
|
214
|
+
.then(async (value) => {
|
|
215
|
+
await this.set(key, value, options)
|
|
216
|
+
return value
|
|
217
|
+
})
|
|
218
|
+
.catch(async (error) => {
|
|
219
|
+
// Cache the error with shorter TTL
|
|
220
|
+
await this.set(key, error as T, {
|
|
221
|
+
...options,
|
|
222
|
+
ttl: options?.errorTTL ?? this.defaultErrorTTL,
|
|
223
|
+
errorTTL: options?.errorTTL ?? this.defaultErrorTTL,
|
|
224
|
+
})
|
|
225
|
+
throw error
|
|
226
|
+
})
|
|
227
|
+
.finally(() => {
|
|
228
|
+
this.promises.delete(key)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
this.promises.set(key, promise)
|
|
232
|
+
return promise as Promise<T>
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a KV cache instance
|
|
238
|
+
*/
|
|
239
|
+
export function createKVCache(options: KVCacheOptions): DecoderCache {
|
|
240
|
+
return new KVDecoderCache(options)
|
|
241
|
+
}
|