@visualizevalue/mint-app-base 0.1.127 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/components/Token/MintTimeline.client.vue +1 -43
- package/composables/collections.ts +57 -266
- package/nuxt.config.ts +1 -0
- package/package.json +29 -1
- package/plugins/2.5.dapp-query.client.ts +40 -0
- package/queries/index.ts +144 -0
- package/queries/sources.ts +417 -0
- package/.env.example +0 -50
- package/.nuxtrc +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Visualize Value
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
:block="currentBlock"
|
|
17
17
|
>
|
|
18
18
|
<template #after>
|
|
19
|
-
<TokenMintTimelineItem
|
|
19
|
+
<TokenMintTimelineItem>
|
|
20
20
|
<Account :address="collection.owner" class="account" />
|
|
21
21
|
|
|
22
22
|
<span class="amount">1<span>×</span></span>
|
|
@@ -34,17 +34,12 @@
|
|
|
34
34
|
</TokenMintTimelineVirtualScroller>
|
|
35
35
|
</template>
|
|
36
36
|
|
|
37
|
-
<div v-if="! backfillComplete" v-show="! loading" ref="loadMore" class="load-more">
|
|
38
|
-
<Button @click="backfill">{{ $t('token.load_more')}}</Button>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
37
|
<Loading v-if="loading || ! currentBlock" :txt="$t('token.loading_mint_history')" />
|
|
42
38
|
</slot>
|
|
43
39
|
</section>
|
|
44
40
|
</template>
|
|
45
41
|
|
|
46
42
|
<script setup>
|
|
47
|
-
import { useElementVisibility } from '@vueuse/core'
|
|
48
43
|
import { useBlockNumber } from '@wagmi/vue'
|
|
49
44
|
|
|
50
45
|
const config = useRuntimeConfig()
|
|
@@ -58,7 +53,6 @@ const { token, collection } = defineProps({
|
|
|
58
53
|
const state = useOnchainStore()
|
|
59
54
|
|
|
60
55
|
const mints = computed(() => state.tokenMints(token.collection, token.tokenId))
|
|
61
|
-
const backfillComplete = computed(() => token.mintsBackfilledUntilBlock <= token.mintedBlock)
|
|
62
56
|
|
|
63
57
|
const sortBy = ref('recent')
|
|
64
58
|
const sortedMints = computed(() => {
|
|
@@ -72,46 +66,17 @@ const sortedMints = computed(() => {
|
|
|
72
66
|
})
|
|
73
67
|
|
|
74
68
|
const loading = ref(true)
|
|
75
|
-
const loadMore = ref()
|
|
76
|
-
const loadMoreVisible = useElementVisibility(loadMore)
|
|
77
|
-
const backfill = async () => {
|
|
78
|
-
loading.value = true
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
await state.backfillTokenMints(token)
|
|
82
|
-
|
|
83
|
-
// If we're not fully backfilled and we have less than 20 mints loaded,
|
|
84
|
-
// continue backfilling events.
|
|
85
|
-
while (! backfillComplete.value && mints.value?.length < 20) {
|
|
86
|
-
await delay(250)
|
|
87
|
-
await state.backfillTokenMints(token)
|
|
88
|
-
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
console.error(`Issue during backfill`, e)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
loading.value = false
|
|
94
|
-
}
|
|
95
69
|
|
|
96
70
|
onMounted(async () => {
|
|
97
71
|
loading.value = true
|
|
98
72
|
try {
|
|
99
|
-
console.info(`Attempting to load + backfill token mints for #${token.tokenId}`)
|
|
100
73
|
await state.fetchTokenMints(token)
|
|
101
|
-
await backfill()
|
|
102
74
|
} catch (e) {
|
|
103
75
|
console.error(e)
|
|
104
76
|
}
|
|
105
77
|
loading.value = false
|
|
106
78
|
})
|
|
107
79
|
|
|
108
|
-
watch(loadMoreVisible, () => {
|
|
109
|
-
// Skip if we have enough mints for the viewport or we're already loading
|
|
110
|
-
if (! loadMoreVisible.value || loading.value) return
|
|
111
|
-
|
|
112
|
-
backfill()
|
|
113
|
-
})
|
|
114
|
-
|
|
115
80
|
watch(currentBlock, () => {
|
|
116
81
|
if (loading.value) return
|
|
117
82
|
|
|
@@ -171,11 +136,4 @@ watch(currentBlock, () => {
|
|
|
171
136
|
}
|
|
172
137
|
}
|
|
173
138
|
|
|
174
|
-
|
|
175
|
-
.load-more {
|
|
176
|
-
.button {
|
|
177
|
-
display: block;
|
|
178
|
-
width: 100%;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
139
|
</style>
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { getBalance, getPublicClient, readContract } from '@wagmi/core'
|
|
2
|
-
import { type
|
|
3
|
-
import { parseAbiItem, type PublicClient } from 'viem'
|
|
2
|
+
import { type PublicClient } from 'viem'
|
|
4
3
|
import type { MintEvent } from '~/utils/types'
|
|
4
|
+
import { INDEXER_SYNCED } from '~/queries/sources'
|
|
5
5
|
|
|
6
|
-
export const CURRENT_STATE_VERSION =
|
|
7
|
-
export const MAX_BLOCK_RANGE = 1800n
|
|
6
|
+
export const CURRENT_STATE_VERSION = 10
|
|
8
7
|
export const MINT_BLOCKS = BLOCKS_PER_DAY
|
|
9
8
|
export const MAX_RENDERERS = 20
|
|
10
9
|
|
|
11
|
-
const inflightRequests = new Map<string, Promise<any>>()
|
|
12
|
-
function dedupe<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
13
|
-
if (!inflightRequests.has(key)) {
|
|
14
|
-
inflightRequests.set(key, fn().finally(() => inflightRequests.delete(key)))
|
|
15
|
-
}
|
|
16
|
-
return inflightRequests.get(key)!
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
export const useOnchainStore = () => {
|
|
20
|
-
const { $wagmi } = useNuxtApp()
|
|
11
|
+
const { $wagmi, $queryClient, $queries } = useNuxtApp()
|
|
21
12
|
const appConfig = useAppConfig()
|
|
22
13
|
const chainId = useMainChainId()
|
|
23
14
|
|
|
@@ -103,54 +94,17 @@ export const useOnchainStore = () => {
|
|
|
103
94
|
|
|
104
95
|
if (!this.hasArtist(address)) this.initializeArtist(address)
|
|
105
96
|
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
await Promise.all([
|
|
98
|
+
this.fetchArtistProfile(address),
|
|
99
|
+
this.fetchCollections(address, factory),
|
|
100
|
+
])
|
|
109
101
|
},
|
|
110
102
|
|
|
111
103
|
async fetchArtistProfile (address: `0x${string}`): Promise<Artist> {
|
|
112
|
-
|
|
113
|
-
const block = await client.getBlockNumber()
|
|
114
|
-
|
|
115
|
-
// Only update once per hour
|
|
116
|
-
if (
|
|
117
|
-
this.hasArtist(address) &&
|
|
118
|
-
this.artist(address).profileUpdatedAtBlock > 0n &&
|
|
119
|
-
(block - this.artist(address).profileUpdatedAtBlock) < BLOCKS_PER_CACHE
|
|
120
|
-
) {
|
|
121
|
-
console.info(`Artist profile already fetched...`)
|
|
122
|
-
return this.artist(address)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.info(`Updating artist profile...`)
|
|
126
|
-
|
|
127
|
-
let ens, avatar, description,
|
|
128
|
-
url, email, twitter, github
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
ens = await client.getEnsName({ address })
|
|
132
|
-
|
|
133
|
-
if (ens) {
|
|
134
|
-
[avatar, description, url, email, twitter, github] = await Promise.all([
|
|
135
|
-
client.getEnsAvatar({ name: ens }),
|
|
136
|
-
client.getEnsText({ name: ens, key: 'description' }),
|
|
137
|
-
client.getEnsText({ name: ens, key: 'url' }),
|
|
138
|
-
client.getEnsText({ name: ens, key: 'email' }),
|
|
139
|
-
client.getEnsText({ name: ens, key: 'com.twitter' }),
|
|
140
|
-
client.getEnsText({ name: ens, key: 'com.github' }),
|
|
141
|
-
])
|
|
142
|
-
}
|
|
143
|
-
} catch (e) { }
|
|
144
|
-
|
|
145
|
-
this.artists[address].ens = ens
|
|
146
|
-
this.artists[address].avatar = avatar
|
|
147
|
-
this.artists[address].description = description
|
|
148
|
-
this.artists[address].url = url
|
|
149
|
-
this.artists[address].email = email
|
|
150
|
-
this.artists[address].twitter = twitter
|
|
151
|
-
this.artists[address].github = github
|
|
152
|
-
this.artists[address].profileUpdatedAtBlock = block
|
|
104
|
+
if (!this.hasArtist(address)) this.initializeArtist(address)
|
|
153
105
|
|
|
106
|
+
const profile = await $queryClient.fetch($queries.artistProfile, address)
|
|
107
|
+
Object.assign(this.artists[address], profile)
|
|
154
108
|
return this.artist(address)
|
|
155
109
|
},
|
|
156
110
|
|
|
@@ -158,95 +112,43 @@ export const useOnchainStore = () => {
|
|
|
158
112
|
artist: `0x${string}`,
|
|
159
113
|
factory: `0x${string}`
|
|
160
114
|
) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
functionName: 'getCreatorCollections',
|
|
165
|
-
args: [artist],
|
|
166
|
-
chainId,
|
|
167
|
-
})).map((a: `0x${string}`) => a.toLowerCase() as `0x${string}`)
|
|
168
|
-
.filter((a: `0x${string}`) => !this.artists[artist].collections.includes(a))
|
|
115
|
+
const collections = await $queryClient.fetch($queries.artistCollections, artist)
|
|
116
|
+
const newCollections = collections.filter(c => !this.hasCollection(c.address))
|
|
117
|
+
await Promise.all(newCollections.map(c => this.addCollection(c)))
|
|
169
118
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
} catch (e) {
|
|
175
|
-
console.error(e)
|
|
176
|
-
}
|
|
119
|
+
this.artists[artist].collections = Array.from(new Set([
|
|
120
|
+
...this.artists[artist].collections,
|
|
121
|
+
...collections.map(c => c.address),
|
|
122
|
+
]))
|
|
177
123
|
},
|
|
178
124
|
|
|
179
125
|
async fetchCollection (address: `0x${string}`): Promise<Collection> {
|
|
180
|
-
return dedupe(`collection:${address}`, () => this._fetchCollection(address))
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
async _fetchCollection (address: `0x${string}`): Promise<Collection> {
|
|
184
126
|
this.ensureStoreVersion()
|
|
185
127
|
|
|
186
|
-
|
|
187
|
-
return this.collection(address)
|
|
188
|
-
}
|
|
128
|
+
const fresh = await $queryClient.fetch($queries.collection, address)
|
|
189
129
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
address,
|
|
194
|
-
functionName: 'contractURI',
|
|
195
|
-
chainId,
|
|
196
|
-
}) as Promise<string>,
|
|
197
|
-
readContract($wagmi, {
|
|
198
|
-
abi: MINT_ABI,
|
|
199
|
-
address,
|
|
200
|
-
functionName: 'version',
|
|
201
|
-
chainId,
|
|
202
|
-
}) as Promise<bigint>,
|
|
203
|
-
readContract($wagmi, {
|
|
204
|
-
abi: MINT_ABI,
|
|
205
|
-
address,
|
|
206
|
-
functionName: 'initBlock',
|
|
207
|
-
chainId,
|
|
208
|
-
}) as Promise<bigint>,
|
|
209
|
-
readContract($wagmi, {
|
|
210
|
-
abi: MINT_ABI,
|
|
211
|
-
address,
|
|
212
|
-
functionName: 'latestTokenId',
|
|
213
|
-
chainId,
|
|
214
|
-
}) as Promise<bigint>,
|
|
215
|
-
readContract($wagmi, {
|
|
216
|
-
abi: MINT_ABI,
|
|
217
|
-
address,
|
|
218
|
-
functionName: 'owner',
|
|
219
|
-
chainId,
|
|
220
|
-
}) as Promise<`0x${string}`>,
|
|
221
|
-
getBalance($wagmi, {
|
|
222
|
-
address,
|
|
223
|
-
}) as Promise<GetBalanceReturnType>,
|
|
224
|
-
])
|
|
130
|
+
if (this.hasCollection(address)) {
|
|
131
|
+
const existing = this.collections[address]
|
|
132
|
+
const hadNewTokens = fresh.latestTokenId > existing.latestTokenId
|
|
225
133
|
|
|
226
|
-
|
|
227
|
-
|
|
134
|
+
// Update metadata without wiping tokens/renderers
|
|
135
|
+
existing.owner = fresh.owner
|
|
136
|
+
existing.name = fresh.name
|
|
137
|
+
existing.symbol = fresh.symbol
|
|
138
|
+
existing.description = fresh.description
|
|
139
|
+
existing.image = fresh.image
|
|
140
|
+
existing.latestTokenId = fresh.latestTokenId
|
|
141
|
+
existing.initBlock = fresh.initBlock
|
|
228
142
|
|
|
229
|
-
|
|
230
|
-
|
|
143
|
+
// Force token re-fetch when new tokens exist
|
|
144
|
+
if (hadNewTokens) {
|
|
145
|
+
await $queryClient.invalidate($queries.collectionTokens, address)
|
|
146
|
+
}
|
|
231
147
|
|
|
232
|
-
return
|
|
233
|
-
image: metadata.image,
|
|
234
|
-
name: metadata.name,
|
|
235
|
-
symbol: metadata.symbol,
|
|
236
|
-
version,
|
|
237
|
-
description: metadata.description,
|
|
238
|
-
address,
|
|
239
|
-
initBlock,
|
|
240
|
-
latestTokenId,
|
|
241
|
-
owner: artist,
|
|
242
|
-
tokens: {},
|
|
243
|
-
balance: balance.value,
|
|
244
|
-
renderers: [],
|
|
245
|
-
})
|
|
246
|
-
} catch (e) {
|
|
247
|
-
console.warn(`Error parsing collection`, e)
|
|
248
|
-
this.artists[artist].collections = this.artists[artist].collections.filter(c => c !== address)
|
|
148
|
+
return existing
|
|
249
149
|
}
|
|
150
|
+
|
|
151
|
+
return await this.addCollection(fresh)
|
|
250
152
|
},
|
|
251
153
|
|
|
252
154
|
async fetchCollectionBalance (address: `0x${string}`) {
|
|
@@ -271,11 +173,11 @@ export const useOnchainStore = () => {
|
|
|
271
173
|
const rendererArgs = { abi: RENDERER_ABI, address: rendererAddress, chainId }
|
|
272
174
|
|
|
273
175
|
const [name, version] = await Promise.all([
|
|
274
|
-
|
|
176
|
+
readContract($wagmi, {
|
|
275
177
|
...rendererArgs,
|
|
276
178
|
functionName: 'name',
|
|
277
179
|
}),
|
|
278
|
-
|
|
180
|
+
readContract($wagmi, {
|
|
279
181
|
...rendererArgs,
|
|
280
182
|
functionName: 'version',
|
|
281
183
|
}),
|
|
@@ -298,43 +200,25 @@ export const useOnchainStore = () => {
|
|
|
298
200
|
},
|
|
299
201
|
|
|
300
202
|
async fetchCollectionTokens (address: `0x${string}`): Promise<Token[]> {
|
|
301
|
-
|
|
302
|
-
abi: MINT_ABI,
|
|
303
|
-
address,
|
|
304
|
-
functionName: 'latestTokenId',
|
|
305
|
-
chainId,
|
|
306
|
-
}) as Promise<bigint>
|
|
307
|
-
|
|
308
|
-
const collection = this.collection(address)
|
|
309
|
-
|
|
310
|
-
const existingTokenIds = new Set(Object.keys(collection.tokens).map(id => BigInt(id)))
|
|
203
|
+
const tokens = await $queryClient.fetch($queries.collectionTokens, address)
|
|
311
204
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// Collect missing token IDs
|
|
316
|
-
const missingIds: bigint[] = []
|
|
317
|
-
let id = collection.latestTokenId
|
|
318
|
-
while (id > 0n) {
|
|
319
|
-
if (!existingTokenIds.has(id)) {
|
|
320
|
-
missingIds.push(id)
|
|
205
|
+
for (const token of tokens) {
|
|
206
|
+
if (!this.collections[address].tokens[`${token.tokenId}`]) {
|
|
207
|
+
this.collections[address].tokens[`${token.tokenId}`] = token
|
|
321
208
|
}
|
|
322
|
-
id--
|
|
323
209
|
}
|
|
324
210
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
211
|
+
if (tokens.length > 0) {
|
|
212
|
+
const maxId = tokens.reduce((max, t) => t.tokenId > max ? t.tokenId : max, 0n)
|
|
213
|
+
if (maxId > this.collections[address].latestTokenId) {
|
|
214
|
+
this.collections[address].latestTokenId = maxId
|
|
215
|
+
}
|
|
328
216
|
}
|
|
329
217
|
|
|
330
218
|
return this.tokens(address)
|
|
331
219
|
},
|
|
332
220
|
|
|
333
221
|
async fetchToken (address: `0x${string}`, id: number | string | bigint, tries: number = 0): Promise<void> {
|
|
334
|
-
return dedupe(`token:${address}:${id}`, () => this._fetchToken(address, id, tries))
|
|
335
|
-
},
|
|
336
|
-
|
|
337
|
-
async _fetchToken (address: `0x${string}`, id: number | string | bigint, tries: number = 0): Promise<void> {
|
|
338
222
|
const client = getPublicClient($wagmi, { chainId }) as PublicClient
|
|
339
223
|
const mintContract = getContract({
|
|
340
224
|
address,
|
|
@@ -342,16 +226,13 @@ export const useOnchainStore = () => {
|
|
|
342
226
|
client,
|
|
343
227
|
})
|
|
344
228
|
|
|
345
|
-
if (this.collection(address)
|
|
346
|
-
console.info(`Token cached #${id}`)
|
|
229
|
+
if (this.collection(address)?.tokens[`${id}`]) {
|
|
347
230
|
return
|
|
348
231
|
}
|
|
349
232
|
|
|
350
|
-
// Normalize token ID
|
|
351
233
|
const tokenId = BigInt(id)
|
|
352
234
|
|
|
353
235
|
try {
|
|
354
|
-
console.info(`Fetching token #${tokenId}`)
|
|
355
236
|
const currentBlock = await client.getBlock()
|
|
356
237
|
|
|
357
238
|
const [data, dataUri] = await Promise.all([
|
|
@@ -369,12 +250,7 @@ export const useOnchainStore = () => {
|
|
|
369
250
|
const json = Buffer.from(dataUri.substring(29), `base64`).toString()
|
|
370
251
|
metadata = JSON.parse(json)
|
|
371
252
|
} catch (e) {
|
|
372
|
-
metadata = {
|
|
373
|
-
name: '',
|
|
374
|
-
description: '',
|
|
375
|
-
image: '',
|
|
376
|
-
animationUrl: '',
|
|
377
|
-
}
|
|
253
|
+
metadata = { name: '', description: '', image: '', animationUrl: '' }
|
|
378
254
|
console.warn(`Parsing data uri failed`, e)
|
|
379
255
|
}
|
|
380
256
|
|
|
@@ -385,10 +261,8 @@ export const useOnchainStore = () => {
|
|
|
385
261
|
description: metadata.description,
|
|
386
262
|
image: metadata.image,
|
|
387
263
|
animationUrl: metadata.animation_url,
|
|
388
|
-
|
|
389
|
-
mintedBlock: BigInt(`${mintedBlock}`), // Force bigint
|
|
264
|
+
mintedBlock: BigInt(`${mintedBlock}`),
|
|
390
265
|
closeAt,
|
|
391
|
-
|
|
392
266
|
mintsBackfilledUntilBlock: 0n,
|
|
393
267
|
mintsFetchedUntilBlock: 0n,
|
|
394
268
|
mints: []
|
|
@@ -396,17 +270,12 @@ export const useOnchainStore = () => {
|
|
|
396
270
|
|
|
397
271
|
this.collections[address].tokens[`${token.tokenId}`] = token
|
|
398
272
|
|
|
399
|
-
// Update latestTokenId if this token is newer than what's cached
|
|
400
273
|
if (tokenId > this.collections[address].latestTokenId) {
|
|
401
274
|
this.collections[address].latestTokenId = tokenId
|
|
402
275
|
}
|
|
403
276
|
} catch (e) {
|
|
404
|
-
// Retry 3 times
|
|
405
277
|
if (tries < 3) {
|
|
406
|
-
|
|
407
|
-
return await this._fetchToken(address, id, tries + 1)
|
|
408
|
-
} else {
|
|
409
|
-
// TODO: Handle impossible to load token
|
|
278
|
+
return await this.fetchToken(address, id, tries + 1)
|
|
410
279
|
}
|
|
411
280
|
}
|
|
412
281
|
},
|
|
@@ -439,91 +308,13 @@ export const useOnchainStore = () => {
|
|
|
439
308
|
|
|
440
309
|
async fetchTokenMints (token: Token) {
|
|
441
310
|
const storedToken = this.collections[token.collection].tokens[token.tokenId.toString()]
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// We want to sync until now, or when the mint closed
|
|
447
|
-
const toBlock = currentBlock > untilBlock ? untilBlock : currentBlock
|
|
448
|
-
|
|
449
|
-
if (token.mintsFetchedUntilBlock >= toBlock) {
|
|
450
|
-
return console.info(`mints for #${token.tokenId} already fetched`)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Initially, we want to sync backwards,
|
|
454
|
-
// but at most 5000 blocks (the general max range for an event query)
|
|
455
|
-
const maxRangeBlock = toBlock - MAX_BLOCK_RANGE
|
|
456
|
-
const fromBlock = token.mintsFetchedUntilBlock > maxRangeBlock // If we've already fetched
|
|
457
|
-
? token.mintsFetchedUntilBlock + 1n // we want to continue where we left off
|
|
458
|
-
: maxRangeBlock > token.mintedBlock // Otherwise we'll go back as far as possible
|
|
459
|
-
? maxRangeBlock // (to our max range)
|
|
460
|
-
: token.mintedBlock // (or all the way to when the token minted)
|
|
461
|
-
|
|
462
|
-
// Load mints in range
|
|
463
|
-
this.addTokenMints(token, await this.loadMintEvents(token, fromBlock, toBlock))
|
|
464
|
-
|
|
465
|
-
// Set sync status
|
|
466
|
-
storedToken.mintsFetchedUntilBlock = toBlock
|
|
467
|
-
|
|
468
|
-
// If this is our first fetch, mark until when we have backfilled
|
|
469
|
-
if (! token.mintsBackfilledUntilBlock) {
|
|
470
|
-
storedToken.mintsBackfilledUntilBlock = fromBlock
|
|
471
|
-
}
|
|
311
|
+
storedToken.mints = await $queryClient.fetch($queries.tokenMints, token.collection as `0x${string}`, token.tokenId)
|
|
312
|
+
storedToken.mintsFetchedUntilBlock = INDEXER_SYNCED
|
|
313
|
+
storedToken.mintsBackfilledUntilBlock = 0n
|
|
472
314
|
},
|
|
473
315
|
|
|
474
|
-
async backfillTokenMints (
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// If we've backfilled all the way;
|
|
478
|
-
if (storedToken.mintsBackfilledUntilBlock <= token.mintedBlock) return
|
|
479
|
-
|
|
480
|
-
// We want to fetch the tokens up until where we stopped backfilling (excluding the last block)
|
|
481
|
-
const toBlock = storedToken.mintsBackfilledUntilBlock - 1n
|
|
482
|
-
|
|
483
|
-
// We want to fetch until our max range (5000), or until when the token minted
|
|
484
|
-
const fromBlock = toBlock - MAX_BLOCK_RANGE > token.mintedBlock ? toBlock - MAX_BLOCK_RANGE : token.mintedBlock
|
|
485
|
-
console.info(`Backfilling token mints blocks ${fromBlock}-${toBlock}`)
|
|
486
|
-
|
|
487
|
-
// Finally, we update our database
|
|
488
|
-
this.addTokenMints(token, await this.loadMintEvents(token, fromBlock, toBlock), 'append')
|
|
489
|
-
|
|
490
|
-
// And save until when we have backfilled our tokens.
|
|
491
|
-
storedToken.mintsBackfilledUntilBlock = fromBlock
|
|
492
|
-
},
|
|
493
|
-
|
|
494
|
-
async loadMintEvents (token: Token, fromBlock: bigint, toBlock: bigint): Promise<MintEvent[]> {
|
|
495
|
-
const client = getPublicClient($wagmi, { chainId }) as PublicClient
|
|
496
|
-
|
|
497
|
-
const logs = await client.getLogs({
|
|
498
|
-
address: token.collection,
|
|
499
|
-
event: parseAbiItem('event NewMint(uint256 indexed tokenId, uint256 unitPrice, uint256 amount, address minter)'),
|
|
500
|
-
args: {
|
|
501
|
-
tokenId: BigInt(token.tokenId),
|
|
502
|
-
},
|
|
503
|
-
fromBlock,
|
|
504
|
-
toBlock,
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
console.info(`Token mints fetched from ${fromBlock}-${toBlock}`)
|
|
508
|
-
|
|
509
|
-
return logs.map(l => ({
|
|
510
|
-
tokenId: token.tokenId,
|
|
511
|
-
address: l.args.minter,
|
|
512
|
-
block: l.blockNumber,
|
|
513
|
-
logIndex: l.logIndex,
|
|
514
|
-
tx: l.transactionHash,
|
|
515
|
-
unitPrice: l.args.unitPrice,
|
|
516
|
-
amount: l.args.amount,
|
|
517
|
-
price: ( l.args.amount || 0n ) * ( l.args.unitPrice || 0n ),
|
|
518
|
-
}) as MintEvent).reverse()
|
|
519
|
-
},
|
|
520
|
-
|
|
521
|
-
addTokenMints (token: Token, mints: MintEvent[], location: 'prepend'|'append' = 'prepend') {
|
|
522
|
-
const storedToken = this.collections[token.collection].tokens[token.tokenId.toString()]
|
|
523
|
-
|
|
524
|
-
storedToken.mints = location === 'prepend'
|
|
525
|
-
? [ ...mints, ...storedToken.mints ]
|
|
526
|
-
: [ ...storedToken.mints, ...mints ]
|
|
316
|
+
async backfillTokenMints (_token: Token) {
|
|
317
|
+
// Both indexer and RPC sources return all mints — nothing to backfill
|
|
527
318
|
},
|
|
528
319
|
|
|
529
320
|
async addCollection (collection: Collection) {
|
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visualizevalue/mint-app-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"main": "./nuxt.config.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"app",
|
|
9
|
+
"assets",
|
|
10
|
+
"components",
|
|
11
|
+
"composables",
|
|
12
|
+
"i18n.config.ts",
|
|
13
|
+
"index.d.ts",
|
|
14
|
+
"layouts",
|
|
15
|
+
"locales",
|
|
16
|
+
"middleware",
|
|
17
|
+
"pages",
|
|
18
|
+
"plugins",
|
|
19
|
+
"public",
|
|
20
|
+
"queries",
|
|
21
|
+
"server",
|
|
22
|
+
"utils",
|
|
23
|
+
"app.config.ts",
|
|
24
|
+
"app.vue",
|
|
25
|
+
"error.vue",
|
|
26
|
+
"nuxt.config.ts",
|
|
27
|
+
"tsconfig.json"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
6
32
|
"dependencies": {
|
|
33
|
+
"@1001-digital/dapp-query-core": "^1.1.0",
|
|
34
|
+
"@1001-digital/dapp-query-vue": "^1.1.0",
|
|
7
35
|
"@csstools/postcss-global-data": "^2.1.1",
|
|
8
36
|
"@nuxtjs/i18n": "^9.5.2",
|
|
9
37
|
"@pinia-plugin-persistedstate/nuxt": "^1.2.1",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createQueryClient } from '@1001-digital/dapp-query-core'
|
|
2
|
+
import { idbCache } from '@1001-digital/dapp-query-core'
|
|
3
|
+
import { dappQueryPlugin } from '@1001-digital/dapp-query-vue'
|
|
4
|
+
import { createQueries, type MintQueries } from '~/queries'
|
|
5
|
+
import type { QueryClient } from '@1001-digital/dapp-query-core'
|
|
6
|
+
|
|
7
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
8
|
+
const queryClient = createQueryClient({
|
|
9
|
+
cache: idbCache('mint-query'),
|
|
10
|
+
defaultStaleTime: 60_000,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const config = nuxtApp.$config.public
|
|
14
|
+
const endpoints = config.indexerEndpoints
|
|
15
|
+
? String(config.indexerEndpoints).split(/\s+/).filter(Boolean)
|
|
16
|
+
: []
|
|
17
|
+
|
|
18
|
+
const queries = createQueries({
|
|
19
|
+
wagmi: nuxtApp.$wagmi as any,
|
|
20
|
+
chainId: Number(config.chainId),
|
|
21
|
+
factory: config.factoryAddress as `0x${string}`,
|
|
22
|
+
endpoints,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
nuxtApp.vueApp.use(dappQueryPlugin, queryClient)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
provide: {
|
|
29
|
+
queryClient,
|
|
30
|
+
queries,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
declare module '#app' {
|
|
36
|
+
interface NuxtApp {
|
|
37
|
+
$queryClient: QueryClient
|
|
38
|
+
$queries: MintQueries
|
|
39
|
+
}
|
|
40
|
+
}
|
package/queries/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { graphqlSource, customSource } from '@1001-digital/dapp-query-core'
|
|
2
|
+
import type { QueryDefinition } from '@1001-digital/dapp-query-core'
|
|
3
|
+
import type { Config } from '@wagmi/core'
|
|
4
|
+
import type { Collection, Token, MintEvent } from '~/utils/types'
|
|
5
|
+
import {
|
|
6
|
+
COLLECTIONS_BY_ARTIST, COLLECTION_BY_ADDRESS,
|
|
7
|
+
ARTIFACTS_BY_COLLECTION, MINTS_BY_ARTIFACT,
|
|
8
|
+
transformCollections, transformCollection,
|
|
9
|
+
transformArtifacts, transformMints, transformProfile,
|
|
10
|
+
rpcFetchProfile, rpcFetchCollections,
|
|
11
|
+
rpcFetchCollection, rpcFetchCollectionTokens,
|
|
12
|
+
rpcFetchTokenMints,
|
|
13
|
+
} from './sources'
|
|
14
|
+
|
|
15
|
+
export interface MintQueries {
|
|
16
|
+
artistProfile: QueryDefinition<Partial<Artist>, [`0x${string}`]>
|
|
17
|
+
artistCollections: QueryDefinition<Collection[], [`0x${string}`]>
|
|
18
|
+
collection: QueryDefinition<Collection, [`0x${string}`]>
|
|
19
|
+
collectionTokens: QueryDefinition<Token[], [`0x${string}`]>
|
|
20
|
+
tokenMints: QueryDefinition<MintEvent[], [`0x${string}`, bigint]>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CreateQueriesConfig {
|
|
24
|
+
wagmi: Config
|
|
25
|
+
chainId: number
|
|
26
|
+
factory: `0x${string}`
|
|
27
|
+
endpoints: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createQueries (config: CreateQueriesConfig): MintQueries {
|
|
31
|
+
const { wagmi, chainId, factory, endpoints } = config
|
|
32
|
+
const hasIndexer = endpoints.length > 0
|
|
33
|
+
|
|
34
|
+
const indexerProfileSource = hasIndexer
|
|
35
|
+
? customSource<Partial<Artist>>({
|
|
36
|
+
id: 'profile-indexer',
|
|
37
|
+
fetch: async (address: unknown) => {
|
|
38
|
+
// Multi-endpoint REST failover
|
|
39
|
+
let lastError: Error | undefined
|
|
40
|
+
for (const endpoint of endpoints) {
|
|
41
|
+
try {
|
|
42
|
+
const base = endpoint.replace(/\/$/, '')
|
|
43
|
+
const res = await fetch(`${base}/profiles/${address}`)
|
|
44
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
45
|
+
return transformProfile(await res.json())
|
|
46
|
+
} catch (e) {
|
|
47
|
+
lastError = e instanceof Error ? e : new Error(String(e))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw lastError ?? new Error('All indexer endpoints failed')
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
: null
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
artistProfile: {
|
|
57
|
+
key: (address) => `profile:${address}`,
|
|
58
|
+
staleTime: 30 * 60 * 1000, // 30 min
|
|
59
|
+
sources: [
|
|
60
|
+
...(indexerProfileSource ? [indexerProfileSource] : []),
|
|
61
|
+
customSource<Partial<Artist>>({
|
|
62
|
+
id: 'profile-rpc',
|
|
63
|
+
fetch: (address: unknown) => rpcFetchProfile(wagmi, address as `0x${string}`),
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
artistCollections: {
|
|
69
|
+
key: (artist) => `collections:${artist}`,
|
|
70
|
+
staleTime: 5 * 60 * 1000,
|
|
71
|
+
sources: [
|
|
72
|
+
...(hasIndexer ? [graphqlSource<Collection[]>({
|
|
73
|
+
endpoints,
|
|
74
|
+
query: COLLECTIONS_BY_ARTIST,
|
|
75
|
+
variables: (artist: unknown) => ({ artist: (artist as string).toLowerCase() }),
|
|
76
|
+
transform: transformCollections,
|
|
77
|
+
})] : []),
|
|
78
|
+
customSource<Collection[]>({
|
|
79
|
+
id: 'collections-rpc',
|
|
80
|
+
fetch: (artist: unknown) => rpcFetchCollections(wagmi, chainId, artist as `0x${string}`, factory),
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
collection: {
|
|
86
|
+
key: (address) => `collection:${address}`,
|
|
87
|
+
staleTime: 5 * 60 * 1000,
|
|
88
|
+
sources: [
|
|
89
|
+
...(hasIndexer ? [graphqlSource<Collection>({
|
|
90
|
+
endpoints,
|
|
91
|
+
query: COLLECTION_BY_ADDRESS,
|
|
92
|
+
variables: (address: unknown) => ({ address: (address as string).toLowerCase() }),
|
|
93
|
+
transform: transformCollection,
|
|
94
|
+
})] : []),
|
|
95
|
+
customSource<Collection>({
|
|
96
|
+
id: 'collection-rpc',
|
|
97
|
+
fetch: async (address: unknown) => {
|
|
98
|
+
const collection = await rpcFetchCollection(wagmi, chainId, address as `0x${string}`)
|
|
99
|
+
if (!collection) throw new Error('Collection not found via RPC')
|
|
100
|
+
return collection
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
collectionTokens: {
|
|
107
|
+
key: (collection) => `tokens:${collection}`,
|
|
108
|
+
staleTime: 5 * 60 * 1000,
|
|
109
|
+
sources: [
|
|
110
|
+
...(hasIndexer ? [graphqlSource<Token[]>({
|
|
111
|
+
endpoints,
|
|
112
|
+
query: ARTIFACTS_BY_COLLECTION,
|
|
113
|
+
variables: (collection: unknown) => ({ collection: (collection as string).toLowerCase() }),
|
|
114
|
+
transform: transformArtifacts,
|
|
115
|
+
})] : []),
|
|
116
|
+
customSource<Token[]>({
|
|
117
|
+
id: 'tokens-rpc',
|
|
118
|
+
fetch: (collection: unknown) => rpcFetchCollectionTokens(wagmi, chainId, collection as `0x${string}`),
|
|
119
|
+
}),
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
tokenMints: {
|
|
124
|
+
key: (collection, tokenId) => `mints:${collection}:${tokenId}`,
|
|
125
|
+
staleTime: 60 * 1000, // 1 min — mints change frequently during live mint
|
|
126
|
+
sources: [
|
|
127
|
+
...(hasIndexer ? [graphqlSource<MintEvent[]>({
|
|
128
|
+
endpoints,
|
|
129
|
+
query: MINTS_BY_ARTIFACT,
|
|
130
|
+
variables: (collection: unknown, tokenId: unknown) => ({
|
|
131
|
+
collection: (collection as string).toLowerCase(),
|
|
132
|
+
artifact: String(tokenId),
|
|
133
|
+
}),
|
|
134
|
+
transform: transformMints,
|
|
135
|
+
})] : []),
|
|
136
|
+
customSource<MintEvent[]>({
|
|
137
|
+
id: 'mints-rpc',
|
|
138
|
+
fetch: (collection: unknown, tokenId: unknown) =>
|
|
139
|
+
rpcFetchTokenMints(wagmi, chainId, collection as `0x${string}`, BigInt(tokenId as string | bigint)),
|
|
140
|
+
}),
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { getBalance, getPublicClient, readContract } from '@wagmi/core'
|
|
2
|
+
import { type GetBalanceReturnType } from '@wagmi/core'
|
|
3
|
+
import { parseAbiItem, type PublicClient } from 'viem'
|
|
4
|
+
import type { Config } from '@wagmi/core'
|
|
5
|
+
import type { Collection, Token, MintEvent } from '~/utils/types'
|
|
6
|
+
import { BLOCKS_PER_DAY } from '@visualizevalue/mint-utils/time'
|
|
7
|
+
|
|
8
|
+
// Sentinel: marks data as fully synced from the indexer.
|
|
9
|
+
export const INDEXER_SYNCED = BigInt(Number.MAX_SAFE_INTEGER)
|
|
10
|
+
|
|
11
|
+
// Ponder response types (bigints come as strings from GraphQL)
|
|
12
|
+
|
|
13
|
+
interface PonderCollection {
|
|
14
|
+
address: string
|
|
15
|
+
artist: { address: string }
|
|
16
|
+
owner: string
|
|
17
|
+
image: string
|
|
18
|
+
name: string
|
|
19
|
+
symbol: string
|
|
20
|
+
description: string
|
|
21
|
+
init_block: string
|
|
22
|
+
latest_token_id: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PonderArtifact {
|
|
26
|
+
collection: { address: string }
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
description: string
|
|
30
|
+
image: string
|
|
31
|
+
animation_url: string
|
|
32
|
+
created_block: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PonderMint {
|
|
36
|
+
artifact: { id: string }
|
|
37
|
+
hash: string
|
|
38
|
+
block_number: string
|
|
39
|
+
log_index: number
|
|
40
|
+
amount: string
|
|
41
|
+
unit_price: string
|
|
42
|
+
price: string
|
|
43
|
+
account: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PonderProfile {
|
|
47
|
+
ens: string | null
|
|
48
|
+
data: {
|
|
49
|
+
avatar: string
|
|
50
|
+
description: string
|
|
51
|
+
links: {
|
|
52
|
+
url: string
|
|
53
|
+
email: string
|
|
54
|
+
twitter: string
|
|
55
|
+
github: string
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
export const COLLECTIONS_BY_ARTIST = `
|
|
62
|
+
query($artist: String!) {
|
|
63
|
+
collections(
|
|
64
|
+
where: { artist: $artist }
|
|
65
|
+
orderBy: "init_block"
|
|
66
|
+
orderDirection: "desc"
|
|
67
|
+
limit: 1000
|
|
68
|
+
) {
|
|
69
|
+
items {
|
|
70
|
+
address artist { address } owner image name symbol description
|
|
71
|
+
init_block latest_token_id
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
export const COLLECTION_BY_ADDRESS = `
|
|
78
|
+
query($address: String!) {
|
|
79
|
+
collection(address: $address) {
|
|
80
|
+
address artist { address } owner image name symbol description
|
|
81
|
+
init_block latest_token_id
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
`
|
|
85
|
+
|
|
86
|
+
export const ARTIFACTS_BY_COLLECTION = `
|
|
87
|
+
query($collection: String!) {
|
|
88
|
+
artifacts(
|
|
89
|
+
where: { collection: $collection }
|
|
90
|
+
orderBy: "id"
|
|
91
|
+
orderDirection: "desc"
|
|
92
|
+
limit: 1000
|
|
93
|
+
) {
|
|
94
|
+
items {
|
|
95
|
+
collection { address } id name description image animation_url created_block
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`
|
|
100
|
+
|
|
101
|
+
export const MINTS_BY_ARTIFACT = `
|
|
102
|
+
query($collection: String!, $artifact: BigInt!) {
|
|
103
|
+
mints(
|
|
104
|
+
where: { collection: $collection, artifact: $artifact }
|
|
105
|
+
orderBy: "block_number"
|
|
106
|
+
orderDirection: "desc"
|
|
107
|
+
limit: 1000
|
|
108
|
+
) {
|
|
109
|
+
items {
|
|
110
|
+
artifact { id } hash block_number log_index
|
|
111
|
+
amount unit_price price account
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
export function transformCollections (data: { collections: { items: PonderCollection[] } }): Collection[] {
|
|
119
|
+
return data.collections.items.map(toCollection)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function transformCollection (data: { collection: PonderCollection | null }): Collection {
|
|
123
|
+
if (!data.collection) throw new Error('Collection not found in indexer')
|
|
124
|
+
return toCollection(data.collection)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function transformArtifacts (data: { artifacts: { items: PonderArtifact[] } }): Token[] {
|
|
128
|
+
return data.artifacts.items.map(toToken)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function transformMints (data: { mints: { items: PonderMint[] } }): MintEvent[] {
|
|
132
|
+
return data.mints.items.map(toMintEvent)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
export function transformProfile (raw: PonderProfile): Partial<Artist> {
|
|
137
|
+
return {
|
|
138
|
+
ens: raw.ens || '',
|
|
139
|
+
avatar: raw.data?.avatar || '',
|
|
140
|
+
description: raw.data?.description || '',
|
|
141
|
+
url: raw.data?.links?.url || '',
|
|
142
|
+
email: raw.data?.links?.email || '',
|
|
143
|
+
twitter: raw.data?.links?.twitter || '',
|
|
144
|
+
github: raw.data?.links?.github || '',
|
|
145
|
+
profileUpdatedAtBlock: INDEXER_SYNCED,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
export async function rpcFetchProfile (
|
|
151
|
+
wagmi: Config,
|
|
152
|
+
address: `0x${string}`,
|
|
153
|
+
): Promise<Partial<Artist>> {
|
|
154
|
+
const client = getPublicClient(wagmi, { chainId: 1 }) as PublicClient
|
|
155
|
+
const block = await client.getBlockNumber()
|
|
156
|
+
|
|
157
|
+
let ens, avatar, description, url, email, twitter, github
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
ens = await client.getEnsName({ address })
|
|
161
|
+
if (ens) {
|
|
162
|
+
[avatar, description, url, email, twitter, github] = await Promise.all([
|
|
163
|
+
client.getEnsAvatar({ name: ens }),
|
|
164
|
+
client.getEnsText({ name: ens, key: 'description' }),
|
|
165
|
+
client.getEnsText({ name: ens, key: 'url' }),
|
|
166
|
+
client.getEnsText({ name: ens, key: 'email' }),
|
|
167
|
+
client.getEnsText({ name: ens, key: 'com.twitter' }),
|
|
168
|
+
client.getEnsText({ name: ens, key: 'com.github' }),
|
|
169
|
+
])
|
|
170
|
+
}
|
|
171
|
+
} catch (e) { }
|
|
172
|
+
|
|
173
|
+
return { ens, avatar, description, url, email, twitter, github, profileUpdatedAtBlock: block }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function rpcFetchCollections (
|
|
177
|
+
wagmi: Config,
|
|
178
|
+
chainId: number,
|
|
179
|
+
artist: `0x${string}`,
|
|
180
|
+
factory: `0x${string}`,
|
|
181
|
+
): Promise<Collection[]> {
|
|
182
|
+
const addresses: `0x${string}`[] = (await readContract(wagmi, {
|
|
183
|
+
abi: FACTORY_ABI,
|
|
184
|
+
address: factory,
|
|
185
|
+
functionName: 'getCreatorCollections',
|
|
186
|
+
args: [artist],
|
|
187
|
+
chainId,
|
|
188
|
+
})).map((a: `0x${string}`) => a.toLowerCase() as `0x${string}`)
|
|
189
|
+
|
|
190
|
+
const collections = await Promise.all(addresses.map(a => rpcFetchCollection(wagmi, chainId, a)))
|
|
191
|
+
return collections.filter((c): c is Collection => c !== null)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function rpcFetchCollection (
|
|
195
|
+
wagmi: Config,
|
|
196
|
+
chainId: number,
|
|
197
|
+
address: `0x${string}`,
|
|
198
|
+
): Promise<Collection | null> {
|
|
199
|
+
try {
|
|
200
|
+
const [data, version, initBlock, latestTokenId, owner, balance] = await Promise.all([
|
|
201
|
+
readContract(wagmi, { abi: MINT_ABI, address, functionName: 'contractURI', chainId }) as Promise<string>,
|
|
202
|
+
readContract(wagmi, { abi: MINT_ABI, address, functionName: 'version', chainId }) as Promise<bigint>,
|
|
203
|
+
readContract(wagmi, { abi: MINT_ABI, address, functionName: 'initBlock', chainId }) as Promise<bigint>,
|
|
204
|
+
readContract(wagmi, { abi: MINT_ABI, address, functionName: 'latestTokenId', chainId }) as Promise<bigint>,
|
|
205
|
+
readContract(wagmi, { abi: MINT_ABI, address, functionName: 'owner', chainId }) as Promise<`0x${string}`>,
|
|
206
|
+
getBalance(wagmi, { address }) as Promise<GetBalanceReturnType>,
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
const artistAddr = owner.toLowerCase() as `0x${string}`
|
|
210
|
+
const json = Buffer.from(data.substring(29), 'base64').toString()
|
|
211
|
+
const metadata = JSON.parse(json)
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
image: metadata.image,
|
|
215
|
+
name: metadata.name,
|
|
216
|
+
symbol: metadata.symbol,
|
|
217
|
+
version,
|
|
218
|
+
description: metadata.description,
|
|
219
|
+
address,
|
|
220
|
+
initBlock,
|
|
221
|
+
latestTokenId,
|
|
222
|
+
owner: artistAddr,
|
|
223
|
+
tokens: {},
|
|
224
|
+
balance: balance.value,
|
|
225
|
+
renderers: [],
|
|
226
|
+
}
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.warn(`RPC: Error fetching collection ${address}`, e)
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function rpcFetchCollectionTokens (
|
|
234
|
+
wagmi: Config,
|
|
235
|
+
chainId: number,
|
|
236
|
+
collection: `0x${string}`,
|
|
237
|
+
): Promise<Token[]> {
|
|
238
|
+
const client = getPublicClient(wagmi, { chainId }) as PublicClient
|
|
239
|
+
const [latestTokenId, currentBlock] = await Promise.all([
|
|
240
|
+
readContract(wagmi, {
|
|
241
|
+
abi: MINT_ABI,
|
|
242
|
+
address: collection,
|
|
243
|
+
functionName: 'latestTokenId',
|
|
244
|
+
chainId,
|
|
245
|
+
}) as Promise<bigint>,
|
|
246
|
+
client.getBlock(),
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
const mintContract = getContract({ address: collection, abi: MINT_ABI, client })
|
|
250
|
+
const tokens: Token[] = []
|
|
251
|
+
const ids: bigint[] = []
|
|
252
|
+
for (let id = latestTokenId; id > 0n; id--) ids.push(id)
|
|
253
|
+
|
|
254
|
+
for (const chunk of chunkArray(ids, 10)) {
|
|
255
|
+
const results = await Promise.allSettled(
|
|
256
|
+
chunk.map(id => rpcFetchSingleToken(mintContract, collection, id, currentBlock))
|
|
257
|
+
)
|
|
258
|
+
for (const r of results) {
|
|
259
|
+
if (r.status === 'fulfilled' && r.value) tokens.push(r.value)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return tokens
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function rpcFetchSingleToken (
|
|
267
|
+
mintContract: ReturnType<typeof getContract>,
|
|
268
|
+
collection: `0x${string}`,
|
|
269
|
+
tokenId: bigint,
|
|
270
|
+
currentBlock: Awaited<ReturnType<PublicClient['getBlock']>>,
|
|
271
|
+
tries: number = 0,
|
|
272
|
+
): Promise<Token | null> {
|
|
273
|
+
try {
|
|
274
|
+
const [data, dataUri] = await Promise.all([
|
|
275
|
+
mintContract.read.get([tokenId]) as Promise<[string, string, `0x${string}`[], bigint, bigint, bigint, bigint]>,
|
|
276
|
+
mintContract.read.uri([tokenId], {
|
|
277
|
+
gas: 100_000_000_000,
|
|
278
|
+
gasPrice: currentBlock.baseFeePerGas,
|
|
279
|
+
}) as Promise<string>,
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
const [_name, _description, _artifact, _renderer, mintedBlock, closeAt, _extraData] = data
|
|
283
|
+
|
|
284
|
+
let metadata
|
|
285
|
+
try {
|
|
286
|
+
const json = Buffer.from(dataUri.substring(29), 'base64').toString()
|
|
287
|
+
metadata = JSON.parse(json)
|
|
288
|
+
} catch (e) {
|
|
289
|
+
metadata = { name: '', description: '', image: '', animation_url: '' }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
tokenId,
|
|
294
|
+
collection,
|
|
295
|
+
name: metadata.name,
|
|
296
|
+
description: metadata.description,
|
|
297
|
+
image: metadata.image,
|
|
298
|
+
animationUrl: metadata.animation_url,
|
|
299
|
+
mintedBlock: BigInt(`${mintedBlock}`),
|
|
300
|
+
closeAt,
|
|
301
|
+
mintsBackfilledUntilBlock: 0n,
|
|
302
|
+
mintsFetchedUntilBlock: 0n,
|
|
303
|
+
mints: [],
|
|
304
|
+
}
|
|
305
|
+
} catch (e) {
|
|
306
|
+
if (tries < 3) return rpcFetchSingleToken(mintContract, collection, tokenId, currentBlock, tries + 1)
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function rpcFetchTokenMints (
|
|
312
|
+
wagmi: Config,
|
|
313
|
+
chainId: number,
|
|
314
|
+
collection: `0x${string}`,
|
|
315
|
+
tokenId: bigint,
|
|
316
|
+
): Promise<MintEvent[]> {
|
|
317
|
+
const client = getPublicClient(wagmi, { chainId }) as PublicClient
|
|
318
|
+
const mintContract = getContract({ address: collection, abi: MINT_ABI, client })
|
|
319
|
+
|
|
320
|
+
const [tokenData, currentBlock] = await Promise.all([
|
|
321
|
+
mintContract.read.get([tokenId]) as Promise<[string, string, `0x${string}`[], bigint, bigint, bigint, bigint]>,
|
|
322
|
+
client.getBlockNumber(),
|
|
323
|
+
])
|
|
324
|
+
|
|
325
|
+
const mintedBlock = BigInt(`${tokenData[4]}`)
|
|
326
|
+
const untilBlock = mintedBlock + BLOCKS_PER_DAY
|
|
327
|
+
const toBlock = currentBlock > untilBlock ? untilBlock : currentBlock
|
|
328
|
+
|
|
329
|
+
return loadMintEventsRange(client, collection, tokenId, mintedBlock, toBlock)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function loadMintEventsRange (
|
|
333
|
+
client: PublicClient,
|
|
334
|
+
collection: `0x${string}`,
|
|
335
|
+
tokenId: bigint,
|
|
336
|
+
fromBlock: bigint,
|
|
337
|
+
toBlock: bigint,
|
|
338
|
+
): Promise<MintEvent[]> {
|
|
339
|
+
try {
|
|
340
|
+
const logs = await client.getLogs({
|
|
341
|
+
address: collection,
|
|
342
|
+
event: parseAbiItem('event NewMint(uint256 indexed tokenId, uint256 unitPrice, uint256 amount, address minter)'),
|
|
343
|
+
args: { tokenId },
|
|
344
|
+
fromBlock,
|
|
345
|
+
toBlock,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
return logs.map(l => ({
|
|
349
|
+
tokenId,
|
|
350
|
+
address: l.args.minter,
|
|
351
|
+
block: l.blockNumber,
|
|
352
|
+
logIndex: l.logIndex,
|
|
353
|
+
tx: l.transactionHash,
|
|
354
|
+
unitPrice: l.args.unitPrice,
|
|
355
|
+
amount: l.args.amount,
|
|
356
|
+
price: (l.args.amount || 0n) * (l.args.unitPrice || 0n),
|
|
357
|
+
}) as MintEvent).reverse()
|
|
358
|
+
} catch (e) {
|
|
359
|
+
if (toBlock - fromBlock > 100n) {
|
|
360
|
+
const mid = fromBlock + (toBlock - fromBlock) / 2n
|
|
361
|
+
const [a, b] = await Promise.all([
|
|
362
|
+
loadMintEventsRange(client, collection, tokenId, fromBlock, mid),
|
|
363
|
+
loadMintEventsRange(client, collection, tokenId, mid + 1n, toBlock),
|
|
364
|
+
])
|
|
365
|
+
return [...a, ...b]
|
|
366
|
+
}
|
|
367
|
+
throw e
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
function toCollection (raw: PonderCollection): Collection {
|
|
373
|
+
return {
|
|
374
|
+
address: raw.address.toLowerCase() as `0x${string}`,
|
|
375
|
+
owner: (raw.owner || raw.artist.address).toLowerCase() as `0x${string}`,
|
|
376
|
+
version: 0n,
|
|
377
|
+
image: raw.image || '',
|
|
378
|
+
name: raw.name || '',
|
|
379
|
+
symbol: raw.symbol || '',
|
|
380
|
+
description: raw.description || '',
|
|
381
|
+
initBlock: BigInt(raw.init_block || '0'),
|
|
382
|
+
latestTokenId: BigInt(raw.latest_token_id || '0'),
|
|
383
|
+
balance: 0n,
|
|
384
|
+
tokens: {},
|
|
385
|
+
renderers: [],
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function toToken (raw: PonderArtifact): Token {
|
|
390
|
+
const mintedBlock = BigInt(raw.created_block || '0')
|
|
391
|
+
return {
|
|
392
|
+
collection: raw.collection.address.toLowerCase() as `0x${string}`,
|
|
393
|
+
tokenId: BigInt(raw.id),
|
|
394
|
+
name: raw.name || '',
|
|
395
|
+
description: raw.description || '',
|
|
396
|
+
image: raw.image || '',
|
|
397
|
+
animationUrl: raw.animation_url || undefined,
|
|
398
|
+
closeAt: mintedBlock + BLOCKS_PER_DAY,
|
|
399
|
+
mintedBlock,
|
|
400
|
+
mintsFetchedUntilBlock: 0n,
|
|
401
|
+
mintsBackfilledUntilBlock: 0n,
|
|
402
|
+
mints: [],
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function toMintEvent (raw: PonderMint): MintEvent {
|
|
407
|
+
return {
|
|
408
|
+
tokenId: BigInt(raw.artifact.id),
|
|
409
|
+
address: raw.account.toLowerCase() as `0x${string}`,
|
|
410
|
+
block: BigInt(raw.block_number),
|
|
411
|
+
logIndex: raw.log_index,
|
|
412
|
+
tx: raw.hash,
|
|
413
|
+
unitPrice: BigInt(raw.unit_price || '0'),
|
|
414
|
+
amount: BigInt(raw.amount || '0'),
|
|
415
|
+
price: BigInt(raw.price || '0'),
|
|
416
|
+
}
|
|
417
|
+
}
|
package/.env.example
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# =========================
|
|
2
|
-
# BASE
|
|
3
|
-
# =========================
|
|
4
|
-
NITRO_PRESET=node_cluster
|
|
5
|
-
NUXT_SSR=false
|
|
6
|
-
NUXT_DEVTOOLS=true
|
|
7
|
-
NUXT_PUBLIC_DOMAIN=localhost
|
|
8
|
-
NUXT_PUBLIC_TITLE=Mint
|
|
9
|
-
NUXT_PUBLIC_DESCRIPTION=To mint is a human right.
|
|
10
|
-
|
|
11
|
-
# =========================
|
|
12
|
-
# MINT DEFAULTS
|
|
13
|
-
# =========================
|
|
14
|
-
# NUXT_PUBLIC_MINT_AMOUNT=1
|
|
15
|
-
# NUXT_PUBLIC_MINT_VALUE=5
|
|
16
|
-
|
|
17
|
-
# =========================
|
|
18
|
-
# ARTIST SCOPE
|
|
19
|
-
# =========================
|
|
20
|
-
NUXT_PUBLIC_CREATOR_ADDRESS=0xc8f8e2f59dd95ff67c3d39109eca2e2a017d4c8a
|
|
21
|
-
|
|
22
|
-
# =========================
|
|
23
|
-
# SERVICES
|
|
24
|
-
# =========================
|
|
25
|
-
NUXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
|
|
26
|
-
NUXT_PUBLIC_IPFS_GATEWAY=
|
|
27
|
-
NUXT_PUBLIC_ARWEAVE_GATEWAY=
|
|
28
|
-
|
|
29
|
-
# =========================
|
|
30
|
-
# MAINNET
|
|
31
|
-
# =========================
|
|
32
|
-
# NUXT_PUBLIC_BLOCK_EXPLORER=https://etherscan.io
|
|
33
|
-
# NUXT_PUBLIC_FACTORY_ADDRESS=0xd717Fe677072807057B03705227EC3E3b467b670
|
|
34
|
-
# NUXT_PUBLIC_CHAIN_ID=1
|
|
35
|
-
# NUXT_PUBLIC_RPC1=https://eth.llamarpc.com
|
|
36
|
-
# NUXT_PUBLIC_RPC2=https://eth.drpc.org
|
|
37
|
-
# NUXT_PUBLIC_RPC3=https://1rpc.io/eth
|
|
38
|
-
|
|
39
|
-
# This should always be present if on other networks, so we can fetch ENS data
|
|
40
|
-
NUXT_PUBLIC_MAINNET_RPC1=https://eth.llamarpc.com
|
|
41
|
-
|
|
42
|
-
# =========================
|
|
43
|
-
# SEPOLIA
|
|
44
|
-
# =========================
|
|
45
|
-
NUXT_PUBLIC_BLOCK_EXPLORER=https://sepolia.etherscan.io
|
|
46
|
-
NUXT_PUBLIC_FACTORY_ADDRESS=0x750C5a6CFD40C9CaA48C31D87AC2a26101Acd517
|
|
47
|
-
NUXT_PUBLIC_CHAIN_ID=11155111
|
|
48
|
-
NUXT_PUBLIC_RPC1=https://ethereum-sepolia-rpc.publicnode.com
|
|
49
|
-
NUXT_PUBLIC_RPC2=https://sepolia.drpc.org
|
|
50
|
-
NUXT_PUBLIC_RPC3=https://1rpc.io/sepolia
|
package/.nuxtrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
typescript.includeWorkspace = true
|