@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.
- package/.env.example +26 -0
- package/README.md +24 -0
- package/app/app.vue +7 -0
- package/app/assets/styles/animation.css +50 -0
- package/app/assets/styles/base.css +34 -0
- package/app/assets/styles/cards.css +20 -0
- package/app/assets/styles/custom-media.css +4 -0
- package/app/assets/styles/custom-selectors.css +1 -0
- package/app/assets/styles/forms.css +183 -0
- package/app/assets/styles/index.css +11 -0
- package/app/assets/styles/normalize.css +541 -0
- package/app/assets/styles/prose.css +166 -0
- package/app/assets/styles/scroll.css +13 -0
- package/app/assets/styles/text.css +14 -0
- package/app/assets/styles/utils.css +24 -0
- package/app/assets/styles/variables.css +195 -0
- package/app/assets/styles/web3-modals.css +26 -0
- package/app/components/Account.client.vue +20 -0
- package/app/components/Actions.vue +25 -0
- package/app/components/AppHeader.vue +99 -0
- package/app/components/Authenticated.client.vue +17 -0
- package/app/components/Avatar.vue +61 -0
- package/app/components/BlocksTimeAgo.client.vue +20 -0
- package/app/components/Breadcrumbs.vue +51 -0
- package/app/components/Button.vue +98 -0
- package/app/components/CardLink.vue +38 -0
- package/app/components/CheckSpinner.vue +39 -0
- package/app/components/Collection/Intro.vue +111 -0
- package/app/components/Collection/OverviewCard.vue +73 -0
- package/app/components/Collection/Withdraw.client.vue +61 -0
- package/app/components/CollectionsOverview.client.vue +58 -0
- package/app/components/Connect.client.vue +88 -0
- package/app/components/CountDown.vue +153 -0
- package/app/components/DialogFrame.vue +96 -0
- package/app/components/ExpandableText.vue +50 -0
- package/app/components/Form/Errors.vue +18 -0
- package/app/components/Form/Group.vue +57 -0
- package/app/components/Form/Input.vue +48 -0
- package/app/components/Form/SelectFile.vue +60 -0
- package/app/components/GasPrice.client.vue +9 -0
- package/app/components/HeaderSection.vue +18 -0
- package/app/components/Icon.vue +37 -0
- package/app/components/IconLink.vue +29 -0
- package/app/components/Image.client.vue +120 -0
- package/app/components/Loading.vue +79 -0
- package/app/components/MintGasPrice.client.vue +20 -0
- package/app/components/MintGasPricePopover.client.vue +69 -0
- package/app/components/MintToken.vue +89 -0
- package/app/components/MintTokenBar.vue +79 -0
- package/app/components/Modal.vue +36 -0
- package/app/components/Navbar.client.vue +86 -0
- package/app/components/Page/Frame.vue +77 -0
- package/app/components/Page/FrameSM.vue +33 -0
- package/app/components/Popover.client.vue +119 -0
- package/app/components/Profile/Header.client.vue +96 -0
- package/app/components/QueryDialog.vue +38 -0
- package/app/components/ToggleDarkMode.client.vue +58 -0
- package/app/components/Token/Detail.client.vue +194 -0
- package/app/components/Token/MintTimeline.client.vue +110 -0
- package/app/components/Token/MintTimelineItem.vue +33 -0
- package/app/components/Token/OverviewCard.vue +140 -0
- package/app/components/TransactionFlow.vue +225 -0
- package/app/components/Visual/ImagePreview.vue +8 -0
- package/app/composables/account.ts +21 -0
- package/app/composables/app.ts +15 -0
- package/app/composables/artistData.ts +22 -0
- package/app/composables/chainId.ts +25 -0
- package/app/composables/collections.ts +435 -0
- package/app/composables/darkMode.ts +1 -0
- package/app/composables/gasPrice.ts +46 -0
- package/app/composables/head.ts +29 -0
- package/app/composables/priceFeed.ts +80 -0
- package/app/composables/subdomain.ts +27 -0
- package/app/error.vue +31 -0
- package/app/layouts/default.vue +42 -0
- package/app/middleware/lowercaseId.ts +1 -0
- package/app/middleware/lowercaseProfileAddress.ts +1 -0
- package/app/middleware/redirectUserScope.ts +13 -0
- package/app/pages/[id]/[collection]/[tokenId]/index.vue +66 -0
- package/app/pages/[id]/[collection]/[tokenId].vue +25 -0
- package/app/pages/[id]/[collection]/index.vue +51 -0
- package/app/pages/[id]/[collection]/mint.vue +260 -0
- package/app/pages/[id]/[collection].vue +24 -0
- package/app/pages/[id]/add.vue +40 -0
- package/app/pages/[id]/create.vue +177 -0
- package/app/pages/[id]/index.vue +43 -0
- package/app/pages/[id].vue +9 -0
- package/app/pages/index.vue +47 -0
- package/app/pages/profile/[address]/index.vue +51 -0
- package/app/pages/profile/[address].vue +9 -0
- package/app/pages/profile/index.vue +28 -0
- package/app/plugins/1.polyfill.client.ts +12 -0
- package/app/plugins/2.wagmi.ts +57 -0
- package/app/router.options.ts +25 -0
- package/app/utils/abis.ts +77 -0
- package/app/utils/arrays.ts +1 -0
- package/app/utils/artifact.ts +21 -0
- package/app/utils/breakpoints.ts +11 -0
- package/app/utils/dates.ts +23 -0
- package/app/utils/format.ts +60 -0
- package/app/utils/images.ts +27 -0
- package/app/utils/ipfs.ts +13 -0
- package/app/utils/lowercaseRouteParam.ts +10 -0
- package/app/utils/serializer.ts +18 -0
- package/app/utils/strings.ts +30 -0
- package/app/utils/time.ts +23 -0
- package/app/utils/types.ts +62 -0
- package/app/utils/urls.ts +43 -0
- package/nuxt.config.ts +130 -0
- package/package.json +44 -0
- package/public/apple-touch-icon-512x512.png +0 -0
- package/public/example-contract-icon-original.svg +5 -0
- package/public/example-contract-icon.svg +5 -0
- package/public/favicon.ico +0 -0
- package/public/icon.svg +8 -0
- package/public/icons/check.svg +3 -0
- package/public/icons/opepen.svg +264 -0
- package/public/icons/wallets/coinbase.svg +4 -0
- package/public/icons/wallets/metamask.svg +1 -0
- package/public/icons/wallets/rainbow.svg +59 -0
- package/public/icons/wallets/walletconnect.svg +1 -0
- package/public/maskable-icon-512x512.png +0 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-64x64.png +0 -0
- package/server/middleware/log.ts +3 -0
- package/server/middleware/subdomain.ts +12 -0
- package/server/tsconfig.json +3 -0
- 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
|
+
}
|