@visualizevalue/mint-app-base 0.1.126 → 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.
@@ -1,23 +1,14 @@
1
1
  import { getBalance, getPublicClient, readContract } from '@wagmi/core'
2
- import { type GetBalanceReturnType } from '@wagmi/core'
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 = 9
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 this.fetchArtistProfile(address)
107
-
108
- await this.fetchCollections(address, factory)
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
- const client = getPublicClient($wagmi, { chainId: 1 }) as PublicClient
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 collectionAddresses: `0x${string}`[] = (await readContract($wagmi, {
162
- abi: FACTORY_ABI,
163
- address: factory,
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
- try {
171
- await Promise.all(collectionAddresses.map(address => this.fetchCollection(address)))
172
-
173
- this.artists[artist].collections = Array.from(new Set([...this.artists[artist].collections, ...collectionAddresses]))
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
- if (this.hasCollection(address) && this.collection(address).latestTokenId > 0n) {
187
- return this.collection(address)
188
- }
128
+ const fresh = await $queryClient.fetch($queries.collection, address)
189
129
 
190
- const [data, version, initBlock, latestTokenId, owner, balance] = await Promise.all([
191
- readContract($wagmi, {
192
- abi: MINT_ABI,
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
- const artist = owner.toLowerCase() as `0x${string}`
227
- const json = Buffer.from(data.substring(29), `base64`).toString()
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
- try {
230
- const metadata = JSON.parse(json)
143
+ // Force token re-fetch when new tokens exist
144
+ if (hadNewTokens) {
145
+ await $queryClient.invalidate($queries.collectionTokens, address)
146
+ }
231
147
 
232
- return await this.addCollection({
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
- await readContract($wagmi, {
176
+ readContract($wagmi, {
275
177
  ...rendererArgs,
276
178
  functionName: 'name',
277
179
  }),
278
- await readContract($wagmi, {
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
- this.collections[address].latestTokenId = await readContract($wagmi, {
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
- // If we have all tokens we don't need to do anything
313
- if (BigInt(existingTokenIds.size) === collection.latestTokenId) return this.tokens(address)
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
- // Fetch in parallel chunks of 5
326
- for (const chunk of chunkArray(missingIds, 5)) {
327
- await Promise.all(chunk.map(tokenId => this.fetchToken(address, tokenId)))
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).tokens[`${id}`]) {
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
- console.info(`Retrying fetching data ${tries + 1}`)
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
- const client = getPublicClient($wagmi, { chainId }) as PublicClient
443
- const currentBlock = await client.getBlockNumber()
444
- const untilBlock = token.mintedBlock + BLOCKS_PER_DAY
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 (token: Token) {
475
- const storedToken = this.collections[token.collection].tokens[token.tokenId.toString()]
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) {
@@ -0,0 +1,51 @@
1
+ import { waitForTransactionReceipt } from '@wagmi/core'
2
+ import type { TransactionReceipt } from 'viem'
3
+
4
+ export interface TrackedTransaction {
5
+ hash: string
6
+ status: 'waiting' | 'complete' | 'error'
7
+ waitingText: string
8
+ completeText: string
9
+ txLink: string
10
+ }
11
+
12
+ export const useTransactionStore = defineStore('transactions', () => {
13
+ const { $wagmi } = useNuxtApp()
14
+ const config = useRuntimeConfig()
15
+
16
+ const pending = ref<TrackedTransaction[]>([])
17
+
18
+ const track = (
19
+ hash: string,
20
+ waitingText: string,
21
+ completeText: string,
22
+ onComplete?: (receipt: TransactionReceipt) => void,
23
+ ) => {
24
+ const entry: TrackedTransaction = reactive({
25
+ hash,
26
+ status: 'waiting',
27
+ waitingText,
28
+ completeText,
29
+ txLink: `${config.public.blockExplorer}/tx/${hash}`,
30
+ })
31
+
32
+ pending.value.push(entry)
33
+
34
+ waitForTransactionReceipt($wagmi, { hash: hash as `0x${string}` })
35
+ .then((receipt) => {
36
+ entry.status = 'complete'
37
+ onComplete?.(receipt)
38
+ setTimeout(() => remove(hash), 4000)
39
+ })
40
+ .catch(() => {
41
+ entry.status = 'error'
42
+ setTimeout(() => remove(hash), 8000)
43
+ })
44
+ }
45
+
46
+ const remove = (hash: string) => {
47
+ pending.value = pending.value.filter((t) => t.hash !== hash)
48
+ }
49
+
50
+ return { pending, track, remove }
51
+ })
package/nuxt.config.ts CHANGED
@@ -27,6 +27,7 @@ export default defineNuxtConfig({
27
27
  mintAmount: 1,
28
28
  mintValue: 0,
29
29
  title: 'Mint',
30
+ indexerEndpoints: '',
30
31
  walletConnectProjectId: '',
31
32
  },
32
33
  },
package/package.json CHANGED
@@ -1,9 +1,37 @@
1
1
  {
2
2
  "name": "@visualizevalue/mint-app-base",
3
- "version": "0.1.126",
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
+ }
@@ -0,0 +1,13 @@
1
+ import { createVNode, render } from 'vue'
2
+ import TransactionToasts from '~/components/TransactionToasts.vue'
3
+
4
+ export default defineNuxtPlugin((nuxtApp) => {
5
+ nuxtApp.hook('app:mounted', () => {
6
+ const container = document.createElement('div')
7
+ document.body.appendChild(container)
8
+
9
+ const vnode = createVNode(TransactionToasts)
10
+ vnode.appContext = nuxtApp.vueApp._context
11
+ render(vnode, container)
12
+ })
13
+ })