@visualizevalue/mint-app-base 0.0.1

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.
Files changed (129) hide show
  1. package/.env.example +26 -0
  2. package/README.md +24 -0
  3. package/app/app.vue +7 -0
  4. package/app/assets/styles/animation.css +50 -0
  5. package/app/assets/styles/base.css +34 -0
  6. package/app/assets/styles/cards.css +20 -0
  7. package/app/assets/styles/custom-media.css +4 -0
  8. package/app/assets/styles/custom-selectors.css +1 -0
  9. package/app/assets/styles/forms.css +183 -0
  10. package/app/assets/styles/index.css +11 -0
  11. package/app/assets/styles/normalize.css +541 -0
  12. package/app/assets/styles/prose.css +166 -0
  13. package/app/assets/styles/scroll.css +13 -0
  14. package/app/assets/styles/text.css +14 -0
  15. package/app/assets/styles/utils.css +24 -0
  16. package/app/assets/styles/variables.css +195 -0
  17. package/app/assets/styles/web3-modals.css +26 -0
  18. package/app/components/Account.client.vue +20 -0
  19. package/app/components/Actions.vue +25 -0
  20. package/app/components/AppHeader.vue +99 -0
  21. package/app/components/Authenticated.client.vue +17 -0
  22. package/app/components/Avatar.vue +61 -0
  23. package/app/components/BlocksTimeAgo.client.vue +20 -0
  24. package/app/components/Breadcrumbs.vue +51 -0
  25. package/app/components/Button.vue +98 -0
  26. package/app/components/CardLink.vue +38 -0
  27. package/app/components/CheckSpinner.vue +39 -0
  28. package/app/components/Collection/Intro.vue +111 -0
  29. package/app/components/Collection/OverviewCard.vue +73 -0
  30. package/app/components/Collection/Withdraw.client.vue +61 -0
  31. package/app/components/CollectionsOverview.client.vue +58 -0
  32. package/app/components/Connect.client.vue +88 -0
  33. package/app/components/CountDown.vue +153 -0
  34. package/app/components/DialogFrame.vue +96 -0
  35. package/app/components/ExpandableText.vue +50 -0
  36. package/app/components/Form/Errors.vue +18 -0
  37. package/app/components/Form/Group.vue +57 -0
  38. package/app/components/Form/Input.vue +48 -0
  39. package/app/components/Form/SelectFile.vue +60 -0
  40. package/app/components/GasPrice.client.vue +9 -0
  41. package/app/components/HeaderSection.vue +18 -0
  42. package/app/components/Icon.vue +37 -0
  43. package/app/components/IconLink.vue +29 -0
  44. package/app/components/Image.client.vue +120 -0
  45. package/app/components/Loading.vue +79 -0
  46. package/app/components/MintGasPrice.client.vue +20 -0
  47. package/app/components/MintGasPricePopover.client.vue +69 -0
  48. package/app/components/MintToken.vue +89 -0
  49. package/app/components/MintTokenBar.vue +79 -0
  50. package/app/components/Modal.vue +36 -0
  51. package/app/components/Navbar.client.vue +86 -0
  52. package/app/components/Page/Frame.vue +77 -0
  53. package/app/components/Page/FrameSM.vue +33 -0
  54. package/app/components/Popover.client.vue +119 -0
  55. package/app/components/Profile/Header.client.vue +96 -0
  56. package/app/components/QueryDialog.vue +38 -0
  57. package/app/components/ToggleDarkMode.client.vue +58 -0
  58. package/app/components/Token/Detail.client.vue +194 -0
  59. package/app/components/Token/MintTimeline.client.vue +110 -0
  60. package/app/components/Token/MintTimelineItem.vue +33 -0
  61. package/app/components/Token/OverviewCard.vue +140 -0
  62. package/app/components/TransactionFlow.vue +225 -0
  63. package/app/components/Visual/ImagePreview.vue +8 -0
  64. package/app/composables/account.ts +21 -0
  65. package/app/composables/app.ts +15 -0
  66. package/app/composables/artistData.ts +22 -0
  67. package/app/composables/chainId.ts +25 -0
  68. package/app/composables/collections.ts +435 -0
  69. package/app/composables/darkMode.ts +1 -0
  70. package/app/composables/gasPrice.ts +46 -0
  71. package/app/composables/head.ts +29 -0
  72. package/app/composables/priceFeed.ts +80 -0
  73. package/app/composables/subdomain.ts +27 -0
  74. package/app/error.vue +31 -0
  75. package/app/layouts/default.vue +42 -0
  76. package/app/middleware/lowercaseId.ts +1 -0
  77. package/app/middleware/lowercaseProfileAddress.ts +1 -0
  78. package/app/middleware/redirectUserScope.ts +13 -0
  79. package/app/pages/[id]/[collection]/[tokenId]/index.vue +66 -0
  80. package/app/pages/[id]/[collection]/[tokenId].vue +25 -0
  81. package/app/pages/[id]/[collection]/index.vue +51 -0
  82. package/app/pages/[id]/[collection]/mint.vue +260 -0
  83. package/app/pages/[id]/[collection].vue +24 -0
  84. package/app/pages/[id]/add.vue +40 -0
  85. package/app/pages/[id]/create.vue +177 -0
  86. package/app/pages/[id]/index.vue +43 -0
  87. package/app/pages/[id].vue +9 -0
  88. package/app/pages/index.vue +47 -0
  89. package/app/pages/profile/[address]/index.vue +51 -0
  90. package/app/pages/profile/[address].vue +9 -0
  91. package/app/pages/profile/index.vue +28 -0
  92. package/app/plugins/1.polyfill.client.ts +12 -0
  93. package/app/plugins/2.wagmi.ts +57 -0
  94. package/app/router.options.ts +25 -0
  95. package/app/utils/abis.ts +77 -0
  96. package/app/utils/arrays.ts +1 -0
  97. package/app/utils/artifact.ts +21 -0
  98. package/app/utils/breakpoints.ts +11 -0
  99. package/app/utils/dates.ts +23 -0
  100. package/app/utils/format.ts +60 -0
  101. package/app/utils/images.ts +27 -0
  102. package/app/utils/ipfs.ts +13 -0
  103. package/app/utils/lowercaseRouteParam.ts +10 -0
  104. package/app/utils/serializer.ts +18 -0
  105. package/app/utils/strings.ts +30 -0
  106. package/app/utils/time.ts +23 -0
  107. package/app/utils/types.ts +62 -0
  108. package/app/utils/urls.ts +43 -0
  109. package/nuxt.config.ts +130 -0
  110. package/package.json +44 -0
  111. package/public/apple-touch-icon-512x512.png +0 -0
  112. package/public/example-contract-icon-original.svg +5 -0
  113. package/public/example-contract-icon.svg +5 -0
  114. package/public/favicon.ico +0 -0
  115. package/public/icon.svg +8 -0
  116. package/public/icons/check.svg +3 -0
  117. package/public/icons/opepen.svg +264 -0
  118. package/public/icons/wallets/coinbase.svg +4 -0
  119. package/public/icons/wallets/metamask.svg +1 -0
  120. package/public/icons/wallets/rainbow.svg +59 -0
  121. package/public/icons/wallets/walletconnect.svg +1 -0
  122. package/public/maskable-icon-512x512.png +0 -0
  123. package/public/pwa-192x192.png +0 -0
  124. package/public/pwa-512x512.png +0 -0
  125. package/public/pwa-64x64.png +0 -0
  126. package/server/middleware/log.ts +3 -0
  127. package/server/middleware/subdomain.ts +12 -0
  128. package/server/tsconfig.json +3 -0
  129. package/tsconfig.json +4 -0
@@ -0,0 +1,21 @@
1
+ import { useAccount } from '@wagmi/vue'
2
+
3
+ export const useIsMeCheck = (checkAddress: Ref|ComputedRef<`0x${string}`>|`0x${string}`|null) => {
4
+ const { address } = useAccount()
5
+ const toCheck = toValue(checkAddress)
6
+
7
+ const isMe = ref()
8
+
9
+ watchEffect(() => {
10
+ isMe.value = address.value && address.value?.toLowerCase() === toCheck?.toLowerCase()
11
+ })
12
+
13
+ return isMe
14
+ }
15
+
16
+ export const useIsMe = () => {
17
+ const id = useArtistId()
18
+
19
+ return useIsMeCheck(id.value)
20
+ }
21
+
@@ -0,0 +1,15 @@
1
+ export const useAppTitle = () => {
2
+ const config = useRuntimeConfig()
3
+ const subdomain = useSubdomain()
4
+ const state = useOnchainStore()
5
+
6
+ return computed(() => {
7
+ return subdomain.value
8
+ ? state.ens(subdomain.value as `0x${string}`) || shortAddress(subdomain.value)
9
+ : config.public.title
10
+ })
11
+ }
12
+
13
+ export const useAppBreadcrumb = () => {
14
+ return useState<Breadcrumbs>('breadcrumb', () => [])
15
+ }
@@ -0,0 +1,22 @@
1
+ export const useLoadArtistData = (_id: ComputedRef|Ref<`0x${string}`>|`0x${string}`) => {
2
+ const config = useRuntimeConfig()
3
+ const store = useOnchainStore()
4
+
5
+ const id = toValue(_id)
6
+
7
+ const loading = ref(true)
8
+
9
+ const load = async () => {
10
+ loading.value = true
11
+ if (! id) return
12
+ await store.fetchArtistScope(id, config.public.factoryAddress as `0x${string}`)
13
+ loading.value = false
14
+ }
15
+
16
+ onMounted(() => load())
17
+
18
+ return {
19
+ load,
20
+ loading,
21
+ }
22
+ }
@@ -0,0 +1,25 @@
1
+ import { useAccount, useSwitchChain } from "@wagmi/vue"
2
+
3
+ export const useMainChainId = () => {
4
+ const config = useRuntimeConfig()
5
+
6
+ return config.public.chainId as 1 | 11155111 | 17000 | 1337
7
+ }
8
+
9
+ export const useEnsureChainIdCheck = () => {
10
+ const chainId = useMainChainId()
11
+ const { switchChain } = useSwitchChain()
12
+ const { chainId: currentChainId } = useAccount()
13
+
14
+ return async () => {
15
+ if (chainId !== currentChainId.value) {
16
+ await switchChain({ chainId })
17
+ }
18
+
19
+ if (chainId === currentChainId.value) {
20
+ return true
21
+ }
22
+
23
+ return false
24
+ }
25
+ }
@@ -0,0 +1,435 @@
1
+ import { getBalance, getPublicClient, readContract } from '@wagmi/core'
2
+ import { type GetBalanceReturnType } from '@wagmi/core'
3
+ import { parseAbiItem } from 'viem'
4
+ import type { MintEvent } from '~/app/utils/types'
5
+
6
+ export const CURRENT_STATE_VERSION = 3
7
+
8
+ export const useOnchainStore = () => {
9
+ const { $wagmi } = useNuxtApp()
10
+ const chainId = useMainChainId()
11
+
12
+ return defineStore('onchainStore', {
13
+
14
+ state: () => ({
15
+ version: CURRENT_STATE_VERSION,
16
+ artists: {} as { [key: `0x${string}`]: Artist },
17
+ collections: {} as { [key: `0x${string}`]: Collection },
18
+ // Collection -> Balance (just for the current user)
19
+ tokenBalances: {} as { [key: `0x${string}`]: { [key: string]: bigint } },
20
+ }),
21
+
22
+ getters: {
23
+ all (state) {
24
+ return Object.values(state.collections)
25
+ },
26
+ hasArtist (state) {
27
+ return (address: `0x${string}`) => state.artists[address] !== undefined
28
+ },
29
+ artist (state) {
30
+ return (address: `0x${string}`) => state.artists[address]
31
+ },
32
+ ens () {
33
+ return (address: `0x${string}`) => this.artist(address)?.ens
34
+ },
35
+ displayName () {
36
+ return (address: `0x${string}`) => this.ens(address) || shortAddress(address)
37
+ },
38
+ forArtist (state) {
39
+ return (address: `0x${string}`) => {
40
+ if (! this.hasArtist(address)) return []
41
+
42
+ return this.artist(address).collections
43
+ .map(c => state.collections[c])
44
+ .sort((a, b) => a.initBlock > b.initBlock ? -1 : 1)
45
+ }
46
+ },
47
+ forArtistOnlyMinted () {
48
+ return (address: `0x${string}`) => this.forArtist(address).filter(c => c.latestTokenId > 0n)
49
+ },
50
+ hasCollection: (state) => (address: `0x${string}`) => state.collections[address] !== undefined,
51
+ collection: (state) => (address: `0x${string}`) => state.collections[address],
52
+ tokens: (state) => (address: `0x${string}`) =>
53
+ Object.values(state.collections[address].tokens)
54
+ .sort((a: Token, b: Token) => a.tokenId > b.tokenId ? -1 : 1),
55
+ tokenMints: (state) => (address: `0x${string}`, tokenId: bigint): MintEvent[] =>
56
+ state.collections[address].tokens[tokenId.toString()].mints,
57
+ tokenBalance: (state) => (address: `0x${string}`, tokenId: bigint): bigint | null =>
58
+ (state.tokenBalances[address] && (state.tokenBalances[address][`${tokenId}`] !== undefined))
59
+ ? state.tokenBalances[address][`${tokenId}`]
60
+ : null,
61
+ },
62
+
63
+ actions: {
64
+ initializeArtist (address: `0x${string}`) {
65
+ const artist: Artist = {
66
+ address,
67
+ ens: '',
68
+ avatar: '',
69
+ description: '',
70
+ collections: [],
71
+ profileUpdatedAtBlock: 0n,
72
+ }
73
+
74
+ this.artists[artist.address] = artist
75
+
76
+ return artist
77
+ },
78
+
79
+ ensureStoreVersion () {
80
+ if (this.version < CURRENT_STATE_VERSION) {
81
+ console.info(`Reset store`)
82
+ this.$reset()
83
+ }
84
+ },
85
+
86
+ async fetchArtistScope (address: `0x${string}`, factory: `0x${string}`) {
87
+ this.ensureStoreVersion()
88
+
89
+ if (!this.hasArtist(address)) this.initializeArtist(address)
90
+
91
+ await this.fetchArtistProfile(address)
92
+
93
+ await this.fetchCollections(address, factory)
94
+ },
95
+
96
+ async fetchArtistProfile (address: `0x${string}`): Promise<Artist> {
97
+ const client = getPublicClient($wagmi, { chainId: 1 })
98
+ const block = await client.getBlockNumber()
99
+
100
+ // Only update once per hour
101
+ if (
102
+ this.hasArtist(address) &&
103
+ this.artist(address).profileUpdatedAtBlock > 0n &&
104
+ (block - this.artist(address).profileUpdatedAtBlock) < BLOCKS_PER_CACHE
105
+ ) {
106
+ console.info(`Artist profile already fetched...`)
107
+ return this.artist(address)
108
+ }
109
+
110
+ console.info(`Updating artist profile...`)
111
+
112
+ let ens, avatar, description,
113
+ url, email, twitter, github
114
+
115
+ try {
116
+ ens = await client.getEnsName({ address })
117
+
118
+ if (ens) {
119
+ [avatar, description, url, email, twitter, github] = await Promise.all([
120
+ client.getEnsAvatar({ name: ens }),
121
+ client.getEnsText({ name: ens, key: 'description' }),
122
+ client.getEnsText({ name: ens, key: 'url' }),
123
+ client.getEnsText({ name: ens, key: 'email' }),
124
+ client.getEnsText({ name: ens, key: 'com.twitter' }),
125
+ client.getEnsText({ name: ens, key: 'com.github' }),
126
+ ])
127
+ }
128
+ } catch (e) { }
129
+
130
+ this.artists[address].ens = ens
131
+ this.artists[address].avatar = avatar
132
+ this.artists[address].description = description
133
+ this.artists[address].url = url
134
+ this.artists[address].email = email
135
+ this.artists[address].twitter = twitter
136
+ this.artists[address].github = github
137
+ this.artists[address].profileUpdatedAtBlock = block
138
+
139
+ return this.artist(address)
140
+ },
141
+
142
+ async fetchCollections (
143
+ artist: `0x${string}`,
144
+ factory: `0x${string}`
145
+ ) {
146
+ const collectionAddresses: `0x${string}`[] = (await readContract($wagmi, {
147
+ abi: FACTORY_ABI,
148
+ address: factory,
149
+ functionName: 'getCreatorCollections',
150
+ args: [artist],
151
+ chainId,
152
+ })).map((a: `0x${string}`) => a.toLowerCase())
153
+
154
+ if (this.artists[artist].collections.length === collectionAddresses.length) {
155
+ console.info(`Collections fetched already (${collectionAddresses.length} collections)`)
156
+ return
157
+ }
158
+
159
+ try {
160
+ await Promise.all(collectionAddresses.map(address => this.fetchCollection(address)))
161
+
162
+ this.artists[artist].collections = collectionAddresses
163
+ } catch (e) {
164
+ console.error(e)
165
+ }
166
+ },
167
+
168
+ async fetchCollection (address: `0x${string}`): Promise<Collection> {
169
+ this.ensureStoreVersion()
170
+
171
+ if (this.hasCollection(address) && this.collection(address).latestTokenId > 0n) {
172
+ return this.collection(address)
173
+ }
174
+
175
+ const [data, initBlock, latestTokenId, owner, balance] = await Promise.all([
176
+ readContract($wagmi, {
177
+ abi: MINT_ABI,
178
+ address,
179
+ functionName: 'contractURI',
180
+ chainId,
181
+ }) as Promise<string>,
182
+ readContract($wagmi, {
183
+ abi: MINT_ABI,
184
+ address,
185
+ functionName: 'initBlock',
186
+ chainId,
187
+ }) as Promise<bigint>,
188
+ readContract($wagmi, {
189
+ abi: MINT_ABI,
190
+ address,
191
+ functionName: 'latestTokenId',
192
+ chainId,
193
+ }) as Promise<bigint>,
194
+ readContract($wagmi, {
195
+ abi: MINT_ABI,
196
+ address,
197
+ functionName: 'owner',
198
+ chainId,
199
+ }) as Promise<`0x${string}`>,
200
+ getBalance($wagmi, {
201
+ address,
202
+ }) as Promise<GetBalanceReturnType>,
203
+ ])
204
+
205
+ const json = Buffer.from(data.substring(29), `base64`).toString()
206
+ const metadata = JSON.parse(json)
207
+
208
+ const artist = owner.toLowerCase() as `0x${string}`
209
+
210
+ return await this.addCollection({
211
+ image: metadata.image,
212
+ name: metadata.name,
213
+ symbol: metadata.symbol,
214
+ description: metadata.description,
215
+ address,
216
+ initBlock,
217
+ latestTokenId,
218
+ owner: artist,
219
+ tokens: {},
220
+ balance: balance.value,
221
+ })
222
+ },
223
+
224
+ async fetchCollectionBalance (address: `0x${string}`) {
225
+ const balance = await getBalance($wagmi, { address })
226
+ this.collections[address].balance = balance.value
227
+ },
228
+
229
+ async fetchCollectionTokens (address: `0x${string}`): Promise<Token[]> {
230
+ this.collections[address].latestTokenId = await readContract($wagmi, {
231
+ abi: MINT_ABI,
232
+ address,
233
+ functionName: 'latestTokenId',
234
+ chainId,
235
+ })
236
+
237
+ const collection = this.collection(address)
238
+
239
+ const existingTokenIds = new Set(Object.keys(collection.tokens).map(id => BigInt(id)))
240
+
241
+ // If we have all tokens we don't need to do anything
242
+ if (BigInt(existingTokenIds.size) === collection.latestTokenId) return this.tokens(address)
243
+
244
+ // Go over each token
245
+ let id = collection.latestTokenId
246
+ while (id > 0n) {
247
+ if (! existingTokenIds.has(id)) {
248
+ await this.fetchToken(address, id)
249
+ } else {
250
+ console.info(`Skipping token #${id} since we already have it.`)
251
+ }
252
+
253
+ id --
254
+ }
255
+
256
+ return this.tokens(address)
257
+ },
258
+
259
+ async fetchToken (address: `0x${string}`, id: number | string | bigint) {
260
+ const client = getPublicClient($wagmi, { chainId })
261
+ const mintContract = getContract({
262
+ address,
263
+ abi: MINT_ABI,
264
+ client,
265
+ })
266
+
267
+ if (this.collection(address).tokens[`${id}`]) {
268
+ console.info(`Token cached #${id}`)
269
+ return
270
+ }
271
+
272
+ // Normalize token ID
273
+ const tokenId = BigInt(id)
274
+
275
+ try {
276
+ console.info(`Fetching token #${tokenId}`)
277
+
278
+ const [data, untilBlock] = await Promise.all([
279
+ mintContract.read.uri([tokenId], { gas: 10_000_000_000 }) as Promise<string>,
280
+ mintContract.read.mintOpenUntil([tokenId]) as Promise<bigint>,
281
+ ])
282
+
283
+ const json = Buffer.from(data.substring(29), `base64`).toString()
284
+ const metadata = JSON.parse(json)
285
+
286
+ const token: Token = {
287
+ tokenId,
288
+ collection: address,
289
+ name: metadata.name,
290
+ description: metadata.description,
291
+ artifact: metadata.image,
292
+ untilBlock,
293
+ mintsBackfilledUntilBlock: 0n,
294
+ mintsFetchedUntilBlock: 0n,
295
+ mints: []
296
+ }
297
+
298
+ this.collections[address].tokens[`${token.tokenId}`] = token
299
+ } catch (e) { }
300
+ },
301
+
302
+ async fetchTokenBalance (token: Token, address: `0x${string}`) {
303
+ const client = getPublicClient($wagmi, { chainId })
304
+ const mintContract = getContract({
305
+ address: token.collection,
306
+ abi: MINT_ABI,
307
+ client
308
+ })
309
+
310
+ if (! this.tokenBalances[token.collection]) {
311
+ this.tokenBalances[token.collection] = {}
312
+ }
313
+
314
+ this.tokenBalances[token.collection][`${token.tokenId}`] =
315
+ await mintContract.read.balanceOf([address, token.tokenId])
316
+
317
+ console.info('fetched token balance', this.tokenBalances[token.collection][`${token.tokenId}`])
318
+ },
319
+
320
+ async fetchTokenMints (token: Token) {
321
+ const client = getPublicClient($wagmi)
322
+
323
+ // We want to sync backwards from now
324
+ const currentBlock = await client.getBlockNumber({ chainId })
325
+
326
+ // Until when
327
+ const toBlock = currentBlock > token.untilBlock ? token.untilBlock : currentBlock
328
+
329
+ if (token.mintsFetchedUntilBlock >= toBlock) return console.info(`token mints already fetched`)
330
+
331
+ // From when
332
+ const maxRangeBlock = toBlock - 5000n
333
+ const mintedAtBlock = token.untilBlock - 7200n
334
+ const fromBlock = token.mintsFetchedUntilBlock > maxRangeBlock
335
+ ? token.mintsFetchedUntilBlock
336
+ : maxRangeBlock > mintedAtBlock
337
+ ? maxRangeBlock
338
+ : mintedAtBlock
339
+
340
+ // Load mints
341
+ this.collections[token.collection].tokens[token.tokenId.toString()].mints = [
342
+ ...await this.loadMintEvents(
343
+ token,
344
+ fromBlock,
345
+ toBlock
346
+ ),
347
+ ...(this.collections[token.collection].tokens[token.tokenId.toString()].mints) || [],
348
+ ]
349
+
350
+ console.info(`Token mints fetched from ${fromBlock}-${toBlock}`)
351
+
352
+ // Set sync status
353
+ this.collections[token.collection].tokens[token.tokenId.toString()].mintsFetchedUntilBlock = toBlock
354
+
355
+ // If this is our first fetch, mark until when we have backfilled
356
+ if (! token.mintsBackfilledUntilBlock) {
357
+ this.collections[token.collection].tokens[token.tokenId.toString()].mintsBackfilledUntilBlock = fromBlock
358
+ }
359
+ },
360
+
361
+ async backfillTokenMints (token: Token) {
362
+ const mintedAtBlock = token.untilBlock - 7200n
363
+
364
+ while (
365
+ this.collections[token.collection].tokens[token.tokenId.toString()].mintsBackfilledUntilBlock > mintedAtBlock
366
+ ) {
367
+ const toBlock = this.collections[token.collection].tokens[token.tokenId.toString()].mintsBackfilledUntilBlock
368
+ const fromBlock = toBlock - 5000n > mintedAtBlock ? toBlock - 5000n : mintedAtBlock
369
+ console.log(`Backfilling token mints blocks ${fromBlock}-${toBlock}`)
370
+
371
+ this.collections[token.collection].tokens[token.tokenId.toString()].mints = [
372
+ ...this.collections[token.collection].tokens[token.tokenId.toString()].mints,
373
+ ...await this.loadMintEvents(token, fromBlock, toBlock)
374
+ ]
375
+
376
+ this.collections[token.collection].tokens[token.tokenId.toString()].mintsBackfilledUntilBlock = fromBlock
377
+ }
378
+ },
379
+
380
+ async loadMintEvents (token: Token, fromBlock: bigint, toBlock: bigint) {
381
+ const client = getPublicClient($wagmi, { chainId })
382
+
383
+ const logs = await client.getLogs({
384
+ address: token.collection,
385
+ event: parseAbiItem('event NewMint(uint256 indexed tokenId, uint256 unitPrice, uint256 amount, address minter)'),
386
+ args: {
387
+ tokenId: BigInt(token.tokenId),
388
+ },
389
+ fromBlock,
390
+ toBlock,
391
+ })
392
+
393
+ return logs.map(l => ({
394
+ tokenId: token.tokenId,
395
+ address: l.args.minter,
396
+ block: l.blockNumber,
397
+ logIndex: l.logIndex,
398
+ tx: l.transactionHash,
399
+ unitPrice: l.args.unitPrice,
400
+ amount: l.args.amount,
401
+ price: l.args.amount * l.args.unitPrice,
402
+ })).reverse()
403
+ },
404
+
405
+ async addCollection (collection: Collection) {
406
+ if (this.hasCollection(collection.address)) {
407
+ console.warn(`Replacing existing collection`)
408
+ }
409
+
410
+ this.collections[collection.address] = collection
411
+
412
+ if (! this.hasArtist(collection.owner)) {
413
+ this.initializeArtist(collection.owner)
414
+ this.fetchArtistProfile(collection.owner)
415
+ }
416
+
417
+ this.artists[collection.owner].collections = Array.from(new Set([
418
+ ...this.artists[collection.owner].collections,
419
+ collection.address
420
+ ]))
421
+
422
+ return collection
423
+ },
424
+ },
425
+
426
+ persist: {
427
+ storage: persistedState.localStorage,
428
+ serializer: {
429
+ serialize: stringifyJSON,
430
+ deserialize: parseJSON,
431
+ },
432
+ },
433
+
434
+ })()
435
+ }
@@ -0,0 +1 @@
1
+ export const isDark = ref(localStorage.getItem('color-scheme') !== 'light')
@@ -0,0 +1,46 @@
1
+ import type { WatchStopHandle } from 'vue'
2
+ import { formatEther, formatGwei } from 'viem'
3
+ import { getGasPrice } from '@wagmi/core'
4
+ import { useConfig, useBlockNumber } from '@wagmi/vue'
5
+
6
+ let priceWatcher: WatchStopHandle|null = null
7
+ const price: Ref<bigint> = ref(0n)
8
+ export const useGasPrice = async () => {
9
+ const config = useConfig()
10
+ const { data: blockNumber } = useBlockNumber()
11
+
12
+ if (! priceWatcher) {
13
+ priceWatcher = watch(blockNumber, async () => price.value = await getGasPrice(config))
14
+ }
15
+
16
+ if (price.value === null && priceWatcher === null) {
17
+ price.value = await getGasPrice(config)
18
+ }
19
+
20
+ const unitPrice = computed(() => ({
21
+ wei: price.value,
22
+ gwei: formatGwei(price.value as bigint),
23
+ eth: formatEther(price.value as bigint),
24
+
25
+ formatted: {
26
+ gwei: price.value as bigint > 20000000000n
27
+ ? roundNumber(formatGwei(price.value as bigint))
28
+ : toFloat(formatGwei(price.value as bigint), 1),
29
+ eth: formatEther(price.value as bigint),
30
+ }
31
+ }))
32
+
33
+ return unitPrice
34
+ }
35
+
36
+ export const useMintPrice = async (mintCount: Ref<number>) => {
37
+ const gasPrice = await useGasPrice()
38
+
39
+ const price = computed(() => (gasPrice.value.wei || 0n) * 60_000n * BigInt(mintCount.value))
40
+ const displayPrice = computed(() => customFormatEther(price.value))
41
+
42
+ return {
43
+ price,
44
+ displayPrice,
45
+ }
46
+ }
@@ -0,0 +1,29 @@
1
+ type HeadConfig = {
2
+ title: string;
3
+ description?: string;
4
+ og?: string;
5
+ append?: string;
6
+ meta?: { name: string, content: string }[];
7
+ }
8
+
9
+ const makeHead = ({
10
+ title,
11
+ append = '',
12
+ description = '',
13
+ og = '',
14
+ meta = [],
15
+ }: HeadConfig) => {
16
+ const titleCompleted = `${title}${append}`
17
+ return {
18
+ title: titleCompleted,
19
+ meta: [
20
+ { name: 'description', content: description },
21
+ { property: 'og:title', content: titleCompleted },
22
+ { property: 'og:description', content: description },
23
+ { property: 'og:image', content: og },
24
+ ...meta,
25
+ ]
26
+ }
27
+ }
28
+
29
+ export const useMetaData = (config: HeadConfig) => useHead(makeHead(config))
@@ -0,0 +1,80 @@
1
+ import { getPublicClient } from '@wagmi/core'
2
+
3
+ const CHAINLINK_PRICE_FEED_ABI = [
4
+ {
5
+ inputs: [],
6
+ name: 'latestRoundData',
7
+ outputs: [
8
+ { internalType: 'uint80', name: 'roundId', type: 'uint80' },
9
+ { internalType: 'int256', name: 'answer', type: 'int256' },
10
+ { internalType: 'uint256', name: 'startedAt', type: 'uint256' },
11
+ { internalType: 'uint256', name: 'updatedAt', type: 'uint256' },
12
+ { internalType: 'uint80', name: 'answeredInRound', type: 'uint80' }
13
+ ],
14
+ stateMutability: 'view',
15
+ type: 'function'
16
+ }
17
+ ] as const
18
+
19
+ const PRICE_FEED_ADDRESS = '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419'
20
+
21
+ export const usePriceFeedStore = () => {
22
+ const { $wagmi } = useNuxtApp()
23
+
24
+ return defineStore('priceFeedStore', {
25
+
26
+ state: () => ({
27
+ version: 1,
28
+ lastUpdated: 0,
29
+ ethUSDRaw: null as bigint|null,
30
+ }),
31
+ getters: {
32
+ ethUSD: store => store.ethUSDRaw ? (store.ethUSDRaw / BigInt(1e8)) : 0n,
33
+ ethUSC: store => store.ethUSDRaw ? (store.ethUSDRaw / BigInt(1e6)) : 0n,
34
+ ethUSDFormatted () {
35
+ return formatNumber(Number(this.ethUSC) / 100, 2)
36
+ },
37
+ weiToUSD (store) {
38
+ return (wei: bigint) => {
39
+ const cents = (wei * (store.ethUSDRaw || 0n)) / (10n ** 18n) / (10n ** 6n)
40
+
41
+ return formatNumber(Number(cents) / 100, 2)
42
+ }
43
+ },
44
+ },
45
+
46
+ actions: {
47
+ async fetchEthUsdPrice () {
48
+ const client = getPublicClient($wagmi, { chainId: 1 })
49
+
50
+ if (nowInSeconds() - this.lastUpdated < 3_600) {
51
+ return this.ethUSD
52
+ }
53
+
54
+ try {
55
+ const [, answer] = await client.readContract({
56
+ address: PRICE_FEED_ADDRESS,
57
+ abi: CHAINLINK_PRICE_FEED_ABI,
58
+ functionName: 'latestRoundData'
59
+ })
60
+
61
+ this.ethUSDRaw = answer
62
+ this.lastUpdated = nowInSeconds()
63
+ } catch (error) {
64
+ console.error('Error fetching price:', error)
65
+ }
66
+
67
+ return this.ethUSD
68
+ }
69
+ },
70
+
71
+ persist: {
72
+ storage: persistedState.localStorage,
73
+ serializer: {
74
+ serialize: stringifyJSON,
75
+ deserialize: parseJSON,
76
+ },
77
+ },
78
+
79
+ })()
80
+ }