@visualizevalue/mint-app-base 0.1.47 → 0.1.49

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.
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <article class="install-custom-renderer">
3
+ <h1>Add Custom Renderer</h1>
4
+ <FormInput v-model="rendererAddressInput" placeholder="0x..." />
5
+
6
+ <Loading v-if="loading" />
7
+ <RendererOverviewCard
8
+ v-else-if="rendererAddress && rendererName"
9
+ :renderer="renderer"
10
+ >
11
+ <template #after>
12
+ <RendererInstallButton
13
+ :collection="collection"
14
+ :renderer="renderer"
15
+ #after
16
+ />
17
+ </template>
18
+ </RendererOverviewCard>
19
+ </article>
20
+ </template>
21
+
22
+ <script setup>
23
+ const { $wagmi } = useNuxtApp()
24
+ const chainId = useMainChainId()
25
+ const { collection } = defineProps(['collection'])
26
+
27
+ const rendererAddressInput = ref(``)
28
+ const rendererAddress = ref()
29
+ const rendererName = ref()
30
+ const rendererVersion = ref()
31
+
32
+ const renderer = computed(() => ({
33
+ address: rendererAddress.value,
34
+ name: rendererName.value,
35
+ version: rendererVersion.value,
36
+ }))
37
+
38
+ watch(rendererAddressInput, () => {
39
+ if (isAddress(rendererAddressInput.value)) {
40
+ rendererAddress.value = rendererAddressInput.value
41
+ } else {
42
+ rendererAddress.value = ``
43
+ }
44
+ })
45
+
46
+ const loading = ref(false)
47
+ watch(rendererAddress, async () => {
48
+ if (! rendererAddress.value) {
49
+ rendererName.value = ''
50
+ rendererVersion.value = null
51
+ return
52
+ }
53
+
54
+ loading.value = true
55
+
56
+ try {
57
+ const rendererArgs = {
58
+ abi: RENDERER_ABI,
59
+ address: rendererAddress.value,
60
+ chainId
61
+ }
62
+
63
+ const [name, version] = await Promise.all([
64
+ await readContract($wagmi, {
65
+ ...rendererArgs,
66
+ functionName: 'name',
67
+ }),
68
+ await readContract($wagmi, {
69
+ ...rendererArgs,
70
+ functionName: 'version',
71
+ }),
72
+ ])
73
+
74
+ rendererName.value = name
75
+ rendererVersion.value = version
76
+ } catch (e) {
77
+ console.error(e)
78
+ }
79
+
80
+ loading.value = false
81
+ })
82
+ </script>
83
+
84
+ <style scoped>
85
+ .install-custom-renderer {
86
+ padding: var(--spacer);
87
+ border: var(--border);
88
+ display: grid;
89
+ gap: var(--spacer);
90
+ }
91
+ </style>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <section class="renderers" id="installed-renderers">
3
+ <h1>Installed Renderers</h1>
4
+
5
+ <div>
6
+ <RendererOverviewCard
7
+ v-for="renderer of installedRenderers"
8
+ :renderer="renderer"
9
+ />
10
+ </div>
11
+ </section>
12
+
13
+ <section class="renderers" id="available-renderers">
14
+ <h1>Available Renderers</h1>
15
+
16
+ <div v-if="availableRenderers.length">
17
+ <RendererOverviewCard
18
+ v-for="renderer of availableRenderers"
19
+ :renderer="renderer"
20
+ >
21
+ <template #after>
22
+ <div class="actions">
23
+ <RendererInstallButton
24
+ :collection="collection"
25
+ :renderer="renderer"
26
+ />
27
+ </div>
28
+ </template>
29
+ </RendererOverviewCard>
30
+ </div>
31
+
32
+ <div v-if="! availableRenderers.length" class="empty">
33
+ <p>All known Renderers installed</p>
34
+ </div>
35
+
36
+ <RendererInstallCustom :collection="collection" />
37
+ </section>
38
+ </template>
39
+
40
+ <script setup>
41
+ const props = defineProps(['collection'])
42
+
43
+ const appConfig = useAppConfig()
44
+ const store = useOnchainStore()
45
+
46
+ const installedRenderers = computed(() => props.collection.renderers)
47
+
48
+ const availableRenderers = computed(
49
+ () => appConfig.knownRenderers.filter(r =>
50
+ !installedRenderers.value.map(cr => cr.address).includes(r.address)
51
+ )
52
+ )
53
+
54
+ onMounted(() => {
55
+ store.fetchCollectionRenderers(props.collection.address)
56
+ })
57
+ </script>
58
+
59
+ <style scoped>
60
+ .renderers {
61
+ display: grid;
62
+ gap: var(--spacer);
63
+ overflow-x: hidden;
64
+
65
+ > h1 {
66
+ font-size: var(--font-lg);
67
+ border-bottom: var(--border);
68
+ padding-bottom: var(--size-2);
69
+ }
70
+
71
+ .empty {
72
+ color: var(--muted);
73
+ }
74
+ }
75
+ </style>
@@ -0,0 +1,32 @@
1
+ <template>
2
+ <article class="renderer-overview-card">
3
+ <div class="details">
4
+ <h1>{{ renderer.name }} <small>v{{ renderer.version }}</small></h1>
5
+ <p v-if="renderer.description">{{ renderer.description }}</p>
6
+ <p class="address">{{ renderer.address }}</p>
7
+ </div>
8
+ <slot name="after" />
9
+ </article>
10
+ </template>
11
+
12
+ <script setup>
13
+ const { renderer } = defineProps(['renderer'])
14
+ </script>
15
+
16
+ <style scoped>
17
+ article {
18
+ display: flex;
19
+ gap: var(--spacer);
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ padding: var(--spacer-sm);
23
+
24
+ &:not(:last-child) {
25
+ border-bottom: var(--border);
26
+ }
27
+
28
+ .address {
29
+ color: var(--muted);
30
+ }
31
+ }
32
+ </style>
@@ -0,0 +1,37 @@
1
+ <template>
2
+ <menu class="tabs">
3
+ <slot name="menu" :active="active" :select="select" />
4
+ </menu>
5
+
6
+ <section class="tabs-content">
7
+ <slot name="content" :active="active" />
8
+ </section>
9
+ </template>
10
+
11
+ <script setup>
12
+ const props = defineProps(['initial'])
13
+
14
+ const active = ref(props.initial)
15
+ const select = k => active.value = k
16
+ </script>
17
+
18
+ <style>
19
+ .tabs {
20
+ border-bottom: var(--border);
21
+ padding: 0 var(--size-1);
22
+ margin: 0;
23
+ display: flex;
24
+ gap: var(--size-1);
25
+
26
+ > Button {
27
+ /* border-bottom-color: transparent; */
28
+ margin-bottom: calc(-1 * var(--border-width));
29
+
30
+ &.active {
31
+ border-color: var(--border-color);
32
+ border-bottom-color: var(--background);
33
+ }
34
+ }
35
+ }
36
+ </style>
37
+
@@ -2,7 +2,8 @@
2
2
  <article class="token-detail">
3
3
  <div class="artifact">
4
4
  <div>
5
- <Image v-if="token.artifact" :src="token.artifact" :alt="token.name" />
5
+ <Embed v-if="token.animationUrl" :src="token.animationUrl" />
6
+ <Image v-else-if="token.image" :src="token.image" :alt="token.name" />
6
7
  <ImageVoid v-else />
7
8
  </div>
8
9
  </div>
@@ -32,7 +32,8 @@
32
32
  <p v-if="mintOpen" class="closes-in">Closes in {{ blocksRemaining }} {{ pluralize('block', Number(blocksRemaining))}}</p>
33
33
  <p v-else class="closed-at">Closed at block {{ token.untilBlock }}</p>
34
34
  </header>
35
- <Image v-if="token.artifact" :src="token.artifact" :alt="token.name" />
35
+ <Embed v-if="token.animationUrl" :src="token.animationUrl" />
36
+ <Image v-else-if="token.image" :src="token.image" :alt="token.name" />
36
37
  <ImageVoid v-else />
37
38
  <CardLink :to="{
38
39
  name: 'id-collection-tokenId',
@@ -34,10 +34,8 @@
34
34
  </template>
35
35
 
36
36
  <script setup>
37
- import { useChainId } from '@wagmi/vue'
38
37
  import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
39
38
  const checkChain = useEnsureChainIdCheck()
40
- const chainId = useChainId()
41
39
 
42
40
  const { $wagmi } = useNuxtApp()
43
41
  const config = useRuntimeConfig()
@@ -83,6 +81,9 @@ watchChainId($wagmi, {
83
81
  }
84
82
  })
85
83
 
84
+ const cachedRequest = ref(props.request)
85
+ watch(props, () => { cachedRequest.value = props.request })
86
+
86
87
  const requesting = ref(false)
87
88
  const waiting = ref(false)
88
89
  const complete = ref(false)
@@ -121,7 +122,8 @@ const step = computed(() => {
121
122
  return 'error'
122
123
  })
123
124
 
124
- const initializeRequest = async (request = props.request) => {
125
+ const initializeRequest = async (request = cachedRequest.value) => {
126
+ cachedRequest.value = request
125
127
  complete.value = false
126
128
  open.value = true
127
129
  error.value = ''
@@ -3,7 +3,7 @@ import { type GetBalanceReturnType } from '@wagmi/core'
3
3
  import { parseAbiItem, type PublicClient } from 'viem'
4
4
  import type { MintEvent } from '~/utils/types'
5
5
 
6
- export const CURRENT_STATE_VERSION = 4
6
+ export const CURRENT_STATE_VERSION = 6
7
7
  export const MAX_BLOCK_RANGE = 1800n
8
8
  export const MINT_BLOCKS = BLOCKS_PER_DAY
9
9
 
@@ -220,6 +220,7 @@ export const useOnchainStore = () => {
220
220
  owner: artist,
221
221
  tokens: {},
222
222
  balance: balance.value,
223
+ renderers: [],
223
224
  })
224
225
  },
225
226
 
@@ -228,6 +229,47 @@ export const useOnchainStore = () => {
228
229
  this.collections[address].balance = balance.value
229
230
  },
230
231
 
232
+ async fetchCollectionRenderers (address: `0x${string}`) {
233
+ const renderers = this.collections[address].renderers
234
+
235
+ let index = renderers.length
236
+ while (true) {
237
+ try {
238
+ const rendererAddress = await readContract($wagmi, {
239
+ abi: MINT_ABI,
240
+ address,
241
+ functionName: 'renderers',
242
+ args: [BigInt(index)],
243
+ chainId,
244
+ })
245
+
246
+ const rendererArgs = { abi: RENDERER_ABI, address: rendererAddress, chainId }
247
+
248
+ const [name, version] = await Promise.all([
249
+ await readContract($wagmi, {
250
+ ...rendererArgs,
251
+ functionName: 'name',
252
+ }),
253
+ await readContract($wagmi, {
254
+ ...rendererArgs,
255
+ functionName: 'version',
256
+ }),
257
+ ])
258
+
259
+ this.collections[address].renderers.push({
260
+ address: rendererAddress.toLowerCase() as `0x${string}`,
261
+ name,
262
+ version,
263
+ })
264
+
265
+ index ++
266
+ } catch (e) {
267
+ console.info(`Stopped parsing renderers ${e.shortMessage || e.message}`)
268
+ return
269
+ }
270
+ }
271
+ },
272
+
231
273
  async fetchCollectionTokens (address: `0x${string}`): Promise<Token[]> {
232
274
  this.collections[address].latestTokenId = await readContract($wagmi, {
233
275
  abi: MINT_ABI,
@@ -278,7 +320,7 @@ export const useOnchainStore = () => {
278
320
  console.info(`Fetching token #${tokenId}`)
279
321
 
280
322
  const [data, untilBlock] = await Promise.all([
281
- mintContract.read.uri([tokenId], { gas: 10_000_000_000 }) as Promise<string>,
323
+ mintContract.read.uri([tokenId], { gas: 100_000_000_000 }) as Promise<string>,
282
324
  mintContract.read.mintOpenUntil([tokenId]) as Promise<bigint>,
283
325
  ])
284
326
 
@@ -290,7 +332,9 @@ export const useOnchainStore = () => {
290
332
  collection: address,
291
333
  name: metadata.name,
292
334
  description: metadata.description,
293
- artifact: metadata.image,
335
+ image: metadata.image,
336
+ animationUrl: metadata.animation_url,
337
+ scriptUrl: metadata.script_url,
294
338
  untilBlock,
295
339
  mintsBackfilledUntilBlock: 0n,
296
340
  mintsFetchedUntilBlock: 0n,
@@ -298,7 +342,9 @@ export const useOnchainStore = () => {
298
342
  }
299
343
 
300
344
  this.collections[address].tokens[`${token.tokenId}`] = token
301
- } catch (e) { }
345
+ } catch (e) {
346
+ console.error(e)
347
+ }
302
348
  },
303
349
 
304
350
  async fetchTokenBalance (token: Token, address: `0x${string}`) {
@@ -0,0 +1,155 @@
1
+ import type { TransactionReceipt } from "viem"
2
+
3
+ // Base token data
4
+ const name = ref('')
5
+ const artifact = ref('')
6
+ const description = ref('')
7
+
8
+ // Derived data based on artifact // renderer
9
+ const image = ref('')
10
+ const animationUrl = ref('')
11
+
12
+ // Renderer data
13
+ const renderer: Ref<number> = ref(0)
14
+ const extraData: Ref<bigint> = ref(0n)
15
+
16
+ // Main token creation composable
17
+ export const useCreateMintData = () => {
18
+ // Reset the creation form values
19
+ const reset = () => {
20
+ name.value = ''
21
+ artifact.value = ''
22
+ description.value = ''
23
+
24
+ image.value = ''
25
+ animationUrl.value = ''
26
+
27
+ extraData.value = 0n
28
+ }
29
+
30
+ return {
31
+ name,
32
+ artifact,
33
+ description,
34
+ image,
35
+ animationUrl,
36
+ renderer,
37
+ extraData,
38
+
39
+ reset,
40
+ }
41
+ }
42
+
43
+ // Expose the mint component based on the selected renderer
44
+ export const useCreateMintRendererComponent = (collection: Collection) => {
45
+ const appConfig = useAppConfig()
46
+ const rendererAddress: Ref<string | null> = computed(() => {
47
+ if (! collection.renderers?.length) return null
48
+
49
+ return collection.renderers[renderer.value].address.toLowerCase()
50
+ })
51
+
52
+ const component = computed(() => appConfig.knownRenderers
53
+ .find((r: Renderer) => r.address.toLowerCase() === rendererAddress.value)?.component || 'Base'
54
+ )
55
+
56
+ return {
57
+ component,
58
+ }
59
+ }
60
+
61
+ // Token creation flow
62
+ export const useCreateMintFlow = (collection: Collection, txFlow: Ref) => {
63
+ const { $wagmi } = useNuxtApp()
64
+ const id = useArtistId()
65
+ const chainId = useMainChainId()
66
+ const store = useOnchainStore()
67
+
68
+ // Mint flow
69
+ const txFlowKey = ref(0)
70
+ const mint = async () => {
71
+ if (! artifact.value) {
72
+ alert(`Empty artifact data. Please try again.`)
73
+ return
74
+ }
75
+
76
+ const artifactByteArray = toByteArray(artifact.value)
77
+ const artifactChunks = chunkArray(artifactByteArray, 4)
78
+ const multiTransactionPrepare = artifactChunks.length > 1
79
+
80
+ try {
81
+ if (multiTransactionPrepare) {
82
+ if (! confirm(`Due to the large artifact size, we have to split it into ${artifactChunks.length} chunks and store them in separate transactions. You will be prompted with multiple transaction requests before minting the final token.`)) {
83
+ return
84
+ }
85
+
86
+ // On the first iteration we want to clear existing artifact data
87
+ let clearExisting = true
88
+
89
+ for (const chunk of artifactChunks) {
90
+ await txFlow.value.initializeRequest(() => writeContract($wagmi, {
91
+ abi: MINT_ABI,
92
+ chainId,
93
+ address: collection.address,
94
+ functionName: 'prepareArtifact',
95
+ args: [
96
+ collection.latestTokenId + 1n,
97
+ chunk,
98
+ clearExisting
99
+ ],
100
+ }))
101
+
102
+ // Make sure to rerender the tx flow component
103
+ txFlowKey.value ++
104
+
105
+ // On following iterations we want to keep existing artifact data
106
+ clearExisting = false
107
+ }
108
+ }
109
+
110
+ await txFlow.value.initializeRequest(() => writeContract($wagmi, {
111
+ abi: MINT_ABI,
112
+ chainId,
113
+ address: collection.address,
114
+ functionName: 'create',
115
+ args: [
116
+ name.value,
117
+ description.value,
118
+ multiTransactionPrepare ? [] : artifactByteArray,
119
+ renderer.value,
120
+ 0n, // Additional Data
121
+ ],
122
+ }))
123
+ } catch (e) {
124
+ console.error(e)
125
+ }
126
+ }
127
+
128
+ // On created
129
+ const mintCreated = async (receipt: TransactionReceipt) => {
130
+ const logs = receipt.logs.map(log => decodeEventLog({
131
+ abi: MINT_ABI,
132
+ data: log.data,
133
+ topics: log.topics,
134
+ strict: false,
135
+ }))
136
+
137
+ const mintedEvent = logs.find(log => log.eventName === 'TransferSingle')
138
+
139
+ await store.fetchToken(collection.address, mintedEvent.args.id)
140
+
141
+ // Force update the collection mint ID
142
+ store.collections[collection.address].latestTokenId = mintedEvent.args.id
143
+
144
+ await navigateTo({
145
+ name: 'id-collection-tokenId',
146
+ params: { id: id.value, collection: collection.address, tokenId: mintedEvent.args.id }
147
+ })
148
+ }
149
+
150
+ return {
151
+ mint,
152
+ mintCreated,
153
+ }
154
+ }
155
+
package/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Renderer } from './utils/types'
2
+
3
+ declare module 'nuxt/schema' {
4
+ interface AppConfigInput {
5
+ // Known renderers besides the base renderer
6
+ knownRenderers: Renderer[],
7
+ }
8
+ }
9
+
10
+ // It is always important to ensure you import/export something when augmenting a type
11
+ export {}
package/nuxt.config.ts CHANGED
@@ -37,7 +37,7 @@ export default defineNuxtConfig({
37
37
  link: [
38
38
  { rel: 'icon', href: '/icon.svg', type: 'image/svg+xml' },
39
39
  ]
40
- }
40
+ },
41
41
  },
42
42
 
43
43
  css: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visualizevalue/mint-app-base",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "dependencies": {
@@ -14,6 +14,8 @@
14
14
  "@wagmi/core": "^2.13.5",
15
15
  "@wagmi/vue": "^0.0.40",
16
16
  "buffer": "^6.0.3",
17
+ "codemirror": "^5",
18
+ "codemirror-editor-vue3": "^2.8.0",
17
19
  "multiformats": "^13.2.2",
18
20
  "postcss-custom-media": "^10.0.6",
19
21
  "postcss-custom-selectors": "^7.1.10",
@@ -25,6 +27,7 @@
25
27
  "@visualizevalue/mint-utils": "^0.0.3"
26
28
  },
27
29
  "devDependencies": {
30
+ "@types/codemirror": "^5.60.15",
28
31
  "nuxt": "^3.13.2",
29
32
  "typescript": "^5.5.4"
30
33
  },