@visualizevalue/mint-app-base 0.1.47 → 0.1.48

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/app.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ export default defineAppConfig({
2
+ knownRenderers: [
3
+ {
4
+ name: 'P5 Renderer',
5
+ version: 1n,
6
+ address: '0x6a08e806c7ad85b4f4720e76fdd8219a364b710e',
7
+ description: 'Allows using P5 scripts as the artifact content'
8
+ },
9
+ ],
10
+ })
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <menu v-if="ownedByMe">
3
+ <CollectionWithdraw :collection="collection" />
4
+ <Button
5
+ :to="{ name: 'id-collection-mint', params: { id, collection: collection.address } }"
6
+ id="mint-new"
7
+ >
8
+ <Icon type="add" />
9
+ <span>Mint</span>
10
+ </Button>
11
+ <Button
12
+ :to="{ name: 'id-collection-renderers', params: { id, collection: collection.address } }"
13
+ id="renderers"
14
+ >
15
+ <Icon type="code" />
16
+ <span>Renderers</span>
17
+ </Button>
18
+ </menu>
19
+ </template>
20
+
21
+ <script setup>
22
+ const { collection } = defineProps({ collection: Object })
23
+
24
+ const id = useArtistId()
25
+ const ownedByMe = useIsMeCheck(collection.owner)
26
+ </script>
@@ -26,16 +26,7 @@
26
26
  </p>
27
27
  </div>
28
28
 
29
- <menu v-if="ownedByMe">
30
- <CollectionWithdraw :collection="collection" />
31
- <Button
32
- :to="{ name: 'id-collection-mint', params: { id, collection: collection.address } }"
33
- id="mint-new"
34
- >
35
- <Icon type="add" />
36
- <span>Mint New</span>
37
- </Button>
38
- </menu>
29
+ <CollectionActions :collection="collection" />
39
30
  </div>
40
31
  </slot>
41
32
  </header>
@@ -48,8 +39,6 @@ const { collection } = defineProps<{
48
39
 
49
40
  const id = useArtistId()
50
41
  const store = useOnchainStore()
51
-
52
- const ownedByMe = useIsMeCheck(collection.owner)
53
42
  </script>
54
43
 
55
44
  <style scoped>
@@ -13,7 +13,7 @@
13
13
  width: 100%;
14
14
  }
15
15
 
16
- fieldset {
16
+ .fieldset-wrapper > fieldset {
17
17
  width: 100%;
18
18
  max-width: -webkit-fill-available;
19
19
  display: flex;
@@ -16,6 +16,7 @@ const ICONS = {
16
16
  'chevron-right': '➡️',
17
17
  'chevron-up': '⬆️',
18
18
  'close': '✖️',
19
+ 'code': '🩻',
19
20
  'discord': '🤖',
20
21
  'edit': '📝',
21
22
  'email': '📧',
@@ -0,0 +1,51 @@
1
+ <template>
2
+ <Actions class="borderless">
3
+ <Button @click="mint">Mint</Button>
4
+ </Actions>
5
+
6
+ <TransactionFlow
7
+ ref="txFlow"
8
+ :text="{
9
+ title: {
10
+ chain: 'Switch Chain',
11
+ requesting: 'Confirm In Wallet',
12
+ waiting: 'Transaction Submitted',
13
+ complete: 'Success!'
14
+ },
15
+ lead: {
16
+ chain: 'Requesting to switch chain...',
17
+ requesting: 'Requesting Signature...',
18
+ waiting: 'Checking mint Transaction...',
19
+ complete: `New token minted...`,
20
+ },
21
+ action: {
22
+ confirm: 'Mint',
23
+ error: 'Retry',
24
+ complete: 'OK',
25
+ },
26
+ }"
27
+ skip-confirmation
28
+ auto-close-success
29
+ @complete="mintCreated"
30
+ />
31
+ </template>
32
+
33
+ <script setup>
34
+ const props = defineProps({
35
+ collection: Object,
36
+ })
37
+
38
+ const txFlow = ref()
39
+
40
+ const { mint, mintCreated } = useCreateMintFlow(props.collection, txFlow)
41
+ </script>
42
+
43
+ <style scoped>
44
+ menu {
45
+ justify-content: flex-end;
46
+
47
+ @media (--md) {
48
+ grid-column: 2;
49
+ }
50
+ }
51
+ </style>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div class="mint-detail">
3
+ <MintPreview />
4
+
5
+ <MintRendererBase class="card" />
6
+
7
+ <MintAction :collection="collection" />
8
+ </div>
9
+ </template>
10
+
11
+ <script setup>
12
+ const props = defineProps(['collection'])
13
+ </script>
14
+
15
+ <style>
16
+ .mint-detail {
17
+ display: grid;
18
+ gap: var(--spacer);
19
+
20
+ > * {
21
+ border: var(--border);
22
+ padding: var(--spacer);
23
+ }
24
+
25
+ @media (--md) {
26
+ grid-template-columns: 40% 1fr;
27
+ }
28
+
29
+ @media (--lg) {
30
+ grid-template-columns: repeat(2, minmax(0, 1fr));
31
+ }
32
+ }
33
+ </style>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <article class="mint-preview">
3
+ <Image v-if="image" :src="image" alt="Preview" />
4
+ <ImagePreview v-else />
5
+ <h1 :class="{ '': !name }">{{ name || 'Token' }}</h1>
6
+ <p :class="{ '': !description }">
7
+ {{ description || 'No description' }}
8
+ </p>
9
+ </article>
10
+ </template>
11
+
12
+ <script setup>
13
+ const { image, name, description } = useCreateMintData()
14
+ </script>
15
+
16
+ <style scoped>
17
+ .mint-preview {
18
+ height: 100%;
19
+ place-content: center;
20
+
21
+ .image,
22
+ svg {
23
+ margin-bottom: var(--spacer-sm);
24
+ width: 100%;
25
+ }
26
+
27
+ svg {
28
+ box-shadow: var(--border-shadow);
29
+ }
30
+
31
+ h1 {
32
+ display: flex;
33
+ gap: var(--spacer-sm);
34
+ align-items: baseline;
35
+ font-size: var(--font-lg);
36
+ }
37
+
38
+ p {
39
+ color: var(--muted);
40
+ }
41
+ }
42
+ </style>
43
+
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div class="mint-renderer-base">
3
+ <Actions>
4
+ <select class="select choose-mode" v-model="mode">
5
+ <option value="file" title="Data URI Encoded File Upload">DATA-URI</option>
6
+ <option value="ipfs" title="Interplanetary File System">IPFS</option>
7
+ <option value="ar" title="Arweave">ARWEAVE</option>
8
+ <option value="http" title="Hypertext Transfer Protocol" disabled>HTTP</option>
9
+ <option value="svg" title="Scalable Vector Graphic" disabled>SVG</option>
10
+ </select>
11
+ </Actions>
12
+
13
+ <div>
14
+ <div v-if="mode === 'file'">
15
+ <FormSelectFile @change="setArtifact" />
16
+ <p v-if="! isSmall" class="muted">
17
+ <small>
18
+ Note: This should be a small file, prefferably an SVG like <a href="https://presence.art/tokens/perspective.svg" target="_blank">this one (810 bytes)</a>.
19
+ If it is larger than what we can store within one transaction, the token creation will be split up into multiple transactions.
20
+ </small>
21
+ </p>
22
+ </div>
23
+ <FormInput v-else-if="mode === 'ipfs'" v-model="ipfsCid" placeholder="CID (qmx...)" prefix="ipfs://" required />
24
+ <FormInput v-else-if="mode === 'ar'" v-model="arTxId" placeholder="TX ID (frV...)" prefix="ar://" required />
25
+
26
+ <FormInput v-model="name" placeholder="Title" required />
27
+ <FormInput v-model="description" placeholder="Description" />
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ const {
34
+ artifact,
35
+ image,
36
+ name,
37
+ description,
38
+ } = useCreateMintData()
39
+
40
+ const mode = ref('file')
41
+ const ipfsCid = ref('')
42
+ const arTxId= ref('')
43
+
44
+ const artifactSize = ref(0)
45
+ const isSmall = computed(() => artifactSize.value / 1024 < 10)
46
+ const setArtifact = async (file) => {
47
+ try {
48
+ artifact.value = await imageFileToDataUri(file)
49
+ artifactSize.value = file.size
50
+ } catch (e) {
51
+ artifact.value = ''
52
+ artifactSize.value = 0
53
+ }
54
+ }
55
+ watch(ipfsCid, () => {
56
+ const validated = validateCID(ipfsCid.value)
57
+ if (! validated) {
58
+ artifact.value = ''
59
+ } else {
60
+ artifact.value = ipfsToHttpURI(`ipfs://${validated}`)
61
+ }
62
+ })
63
+ watch(arTxId, () => {
64
+ artifact.value = `https://arweave.net/${arTxId.value}`
65
+ })
66
+ watch(mode, () => artifact.value = '')
67
+
68
+ // Simple and stupid for the base renderer..
69
+ watch(artifact, () => image.value = artifact.value)
70
+ </script>
71
+
72
+ <style scoped>
73
+ .mint-renderer-base {
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: var(--spacer);
77
+ height: 100%;
78
+
79
+ > div {
80
+ display: flex;
81
+ height: 100%;
82
+ flex-direction: column;
83
+ justify-content: center;
84
+ gap: var(--spacer);
85
+ }
86
+ }
87
+
88
+ select.choose-mode {
89
+ width: fit-content;
90
+ }
91
+ </style>
92
+
@@ -33,6 +33,7 @@ watch(props, () => updateBreadcrumbs())
33
33
  &.inset {
34
34
  gap: var(--spacer);
35
35
  padding: var(--spacer);
36
+ overflow-x: hidden;
36
37
 
37
38
  > *:not(.borderless) {
38
39
  border: var(--border);
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <Button @click="install">Install</Button>
3
+ <TransactionFlow
4
+ ref="txFlow"
5
+ :request="installRequest"
6
+ :text="{
7
+ title: {
8
+ chain: 'Switch Chain',
9
+ requesting: 'Confirm In Wallet',
10
+ waiting: 'Transaction Submitted',
11
+ complete: 'Success!'
12
+ },
13
+ lead: {
14
+ chain: 'Requesting to switch chain...',
15
+ requesting: 'Requesting Signature...',
16
+ waiting: 'Checking Transaction...',
17
+ complete: `New Renderer registered...`,
18
+ },
19
+ action: {
20
+ confirm: 'Register Renderer',
21
+ error: 'Retry',
22
+ complete: 'OK',
23
+ },
24
+ }"
25
+ skip-confirmation
26
+ auto-close-success
27
+ />
28
+ </template>
29
+
30
+ <script setup>
31
+ const { $wagmi } = useNuxtApp()
32
+ const { collection, renderer } = defineProps(['collection', 'renderer'])
33
+ const store = useOnchainStore()
34
+ const chainId = useMainChainId()
35
+
36
+ const installRequest = computed(() => async () => {
37
+ return writeContract($wagmi, {
38
+ abi: MINT_ABI,
39
+ chainId,
40
+ address: collection.address,
41
+ functionName: 'registerRenderer',
42
+ args: [
43
+ renderer.address,
44
+ ],
45
+ })
46
+ })
47
+ const txFlow = ref()
48
+ const installing = ref(false)
49
+ const install = async () => {
50
+ installing.value = true
51
+
52
+ try {
53
+ await txFlow.value.initializeRequest(installRequest.value)
54
+
55
+ await store.fetchCollectionRenderers(collection.address)
56
+ } catch (e) {
57
+ console.error(e)
58
+ }
59
+
60
+ installing.value = false
61
+ }
62
+ </script>
@@ -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
+ <RendererInstallRendererButton
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,73 @@
1
+ <template>
2
+ <section class="renderers" id="installed-renderers">
3
+ <h1>Installed Renderers</h1>
4
+
5
+ <div>
6
+ <RendererOverviewCard
7
+ v-for="renderer of collection.renderers"
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
+ <RendererInstallRendererButton
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 { collection } = defineProps(['collection'])
42
+
43
+ const appConfig = useAppConfig()
44
+ const store = useOnchainStore()
45
+
46
+ const availableRenderers = computed(
47
+ () => appConfig.knownRenderers.filter(r =>
48
+ !collection.renderers.map(cr => cr.address).includes(r.address)
49
+ )
50
+ )
51
+
52
+ onMounted(() => {
53
+ store.fetchCollectionRenderers(collection.address)
54
+ })
55
+ </script>
56
+
57
+ <style scoped>
58
+ .renderers {
59
+ display: grid;
60
+ gap: var(--spacer);
61
+ overflow-x: hidden;
62
+
63
+ > h1 {
64
+ font-size: var(--font-lg);
65
+ border-bottom: var(--border);
66
+ padding-bottom: var(--size-2);
67
+ }
68
+
69
+ .empty {
70
+ color: var(--muted);
71
+ }
72
+ }
73
+ </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>
@@ -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 = 5
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,
@@ -0,0 +1,138 @@
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(0)
14
+ const extraData = 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
+ renderer.value = 0
28
+ extraData.value = 0n
29
+ }
30
+
31
+ return {
32
+ name,
33
+ artifact,
34
+ description,
35
+ image,
36
+ animationUrl,
37
+ renderer,
38
+ extraData,
39
+
40
+ reset,
41
+ }
42
+ }
43
+
44
+ // Token creation flow
45
+ export const useCreateMintFlow = (collection: Collection, txFlow: Ref) => {
46
+ const { $wagmi } = useNuxtApp()
47
+ const id = useArtistId()
48
+ const chainId = useMainChainId()
49
+ const store = useOnchainStore()
50
+
51
+ // Mint flow
52
+ const txFlowKey = ref(0)
53
+ const mint = async () => {
54
+ if (! artifact.value) {
55
+ alert(`Empty artifact data. Please try again.`)
56
+ return
57
+ }
58
+
59
+ const artifactByteArray = toByteArray(artifact.value)
60
+ const artifactChunks = chunkArray(artifactByteArray, 4)
61
+ const multiTransactionPrepare = artifactChunks.length > 1
62
+
63
+ try {
64
+ if (multiTransactionPrepare) {
65
+ 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.`)) {
66
+ return
67
+ }
68
+
69
+ // On the first iteration we want to clear existing artifact data
70
+ let clearExisting = true
71
+
72
+ for (const chunk of artifactChunks) {
73
+ await txFlow.value.initializeRequest(() => writeContract($wagmi, {
74
+ abi: MINT_ABI,
75
+ chainId,
76
+ address: collection.address,
77
+ functionName: 'prepareArtifact',
78
+ args: [
79
+ collection.latestTokenId + 1n,
80
+ chunk,
81
+ clearExisting
82
+ ],
83
+ }))
84
+
85
+ // Make sure to rerender the tx flow component
86
+ txFlowKey.value ++
87
+
88
+ // On following iterations we want to keep existing artifact data
89
+ clearExisting = false
90
+ }
91
+ }
92
+
93
+ await txFlow.value.initializeRequest(() => writeContract($wagmi, {
94
+ abi: MINT_ABI,
95
+ chainId,
96
+ address: collection.address,
97
+ functionName: 'create',
98
+ args: [
99
+ name.value,
100
+ description.value,
101
+ multiTransactionPrepare ? [] : artifactByteArray,
102
+ 0, // Renderer
103
+ 0n, // Additional Data
104
+ ],
105
+ }))
106
+ } catch (e) {
107
+ console.error(e)
108
+ }
109
+ }
110
+
111
+ // On created
112
+ const mintCreated = async (receipt: TransactionReceipt) => {
113
+ const logs = receipt.logs.map(log => decodeEventLog({
114
+ abi: MINT_ABI,
115
+ data: log.data,
116
+ topics: log.topics,
117
+ strict: false,
118
+ }))
119
+
120
+ const mintedEvent = logs.find(log => log.eventName === 'TransferSingle')
121
+
122
+ await store.fetchToken(collection.address, mintedEvent.args.id)
123
+
124
+ // Force update the collection mint ID
125
+ store.collections[collection.address].latestTokenId = mintedEvent.args.id
126
+
127
+ await navigateTo({
128
+ name: 'id-collection-tokenId',
129
+ params: { id: id.value, collection: collection.address, tokenId: mintedEvent.args.id }
130
+ })
131
+ }
132
+
133
+ return {
134
+ mint,
135
+ mintCreated,
136
+ }
137
+ }
138
+
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
+ renderers: 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.48",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "dependencies": {
@@ -1,202 +1,23 @@
1
1
  <template>
2
2
  <Authenticated>
3
- <PageFrame :title="breadcrumb" class="inset wide" id="mint-token">
4
- <article class="preview">
5
- <Image v-if="image" :src="image" alt="Preview" />
6
- <ImagePreview v-else />
7
- <h1 :class="{ '': !name }">{{ name || 'Token' }}</h1>
8
- <p :class="{ '': !description }">
9
- {{ description || 'No description' }}
10
- </p>
11
- </article>
3
+ <PageFrame :title="breadcrumb" class="inset wide">
12
4
 
13
- <form @submit.stop.prevent="mint" class="card">
14
- <Actions>
15
- <select class="select choose-mode" v-model="mode">
16
- <option value="file" title="Data URI Encoded File Upload">DATA-URI</option>
17
- <option value="ipfs" title="Interplanetary File System">IPFS</option>
18
- <option value="ar" title="Arweave">ARWEAVE</option>
19
- <option value="http" title="Hypertext Transfer Protocol" disabled>HTTP</option>
20
- <option value="svg" title="Scalable Vector Graphic" disabled>SVG</option>
21
- </select>
22
- </Actions>
23
-
24
- <div>
25
- <div v-if="mode === 'file'">
26
- <FormSelectFile @change="setImage" />
27
- <p v-if="! isSmall" class="muted">
28
- <small>
29
- Note: This should be a small file, prefferably an SVG like <a href="https://presence.art/tokens/perspective.svg" target="_blank">this one (810 bytes)</a>.
30
- If it is larger than what we can store within one transaction, the token creation will be split up into multiple transactions.
31
- </small>
32
- </p>
33
- </div>
34
- <FormInput v-else-if="mode === 'ipfs'" v-model="ipfsCid" placeholder="CID (qmx...)" prefix="ipfs://" required />
35
- <FormInput v-else-if="mode === 'ar'" v-model="arTxId" placeholder="TX ID (frV...)" prefix="ar://" required />
36
-
37
- <FormInput v-model="name" placeholder="Title" required />
38
- <FormInput v-model="description" placeholder="Description" />
39
- </div>
40
-
41
- <Actions>
42
- <Button>Mint</Button>
43
- </Actions>
44
- <TransactionFlow
45
- ref="txFlow"
46
- :text="{
47
- title: {
48
- chain: 'Switch Chain',
49
- requesting: 'Confirm In Wallet',
50
- waiting: 'Transaction Submitted',
51
- complete: 'Success!'
52
- },
53
- lead: {
54
- chain: 'Requesting to switch chain...',
55
- requesting: 'Requesting Signature...',
56
- waiting: 'Checking mint Transaction...',
57
- complete: `New token minted...`,
58
- },
59
- action: {
60
- confirm: 'Mint',
61
- error: 'Retry',
62
- complete: 'OK',
63
- },
64
- }"
65
- skip-confirmation
66
- auto-close-success
67
- />
68
-
69
- </form>
5
+ <MintDetail :collection="collection" class="borderless" />
70
6
 
71
7
  </PageFrame>
72
8
  </Authenticated>
73
9
  </template>
74
10
 
75
11
  <script setup>
76
- const { $wagmi } = useNuxtApp()
77
- const id = useArtistId()
78
- const chainId = useMainChainId()
12
+ // Reset any previously set data on initial load
13
+ const { reset } = useCreateMintData()
14
+ onMounted(() => reset())
79
15
 
16
+ // Prepare breadcrumbs
80
17
  const props = defineProps(['collection'])
81
- const store = useOnchainStore()
82
18
  const collection = computed(() => props.collection)
83
-
84
- const mode = ref('file')
85
- const ipfsCid = ref('')
86
- // TODO: Rework to plugin architecture. Or at least per renderer logic.
87
- const arTxId= ref('')
88
- const image = ref('')
89
- const name = ref('')
90
- const description = ref('')
91
-
92
- const imageSize = ref(0)
93
- const isSmall = computed(() => imageSize.value / 1024 < 10)
94
- const setImage = async (file) => {
95
- try {
96
- image.value = await imageFileToDataUri(file)
97
- imageSize.value = file.size
98
- } catch (e) {
99
- image.value = ''
100
- imageSize.value = 0
101
- }
102
- }
103
- watch(ipfsCid, () => {
104
- const validated = validateCID(ipfsCid.value)
105
- if (! validated) {
106
- image.value = ''
107
- } else {
108
- image.value = ipfsToHttpURI(`ipfs://${validated}`)
109
- }
110
- })
111
- watch(arTxId, () => {
112
- image.value = `https://arweave.net/${arTxId.value}`
113
- })
114
- watch(mode, () => image.value = '')
115
-
116
- const txFlow = ref()
117
- const txFlowKey = ref(0)
118
- const minting = ref(false)
119
- const mint = async () => {
120
- if (! image.value) {
121
- alert(`Empty image data. Please try again.`)
122
- return
123
- }
124
-
125
- const artifact = toByteArray(image.value)
126
- const artifactChunks = chunkArray(artifact, 4)
127
- const multiTransactionPrepare = artifactChunks.length > 1
128
-
129
- minting.value = true
130
-
131
- try {
132
- if (multiTransactionPrepare) {
133
- 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.`)) {
134
- return
135
- }
136
-
137
- // On the first iteration we want to clear existing artifact data
138
- let clearExisting = true
139
-
140
- for (const chunk of artifactChunks) {
141
- await txFlow.value.initializeRequest(() => writeContract($wagmi, {
142
- abi: MINT_ABI,
143
- chainId,
144
- address: collection.value.address,
145
- functionName: 'prepareArtifact',
146
- args: [
147
- collection.value.latestTokenId + 1n,
148
- chunk,
149
- clearExisting
150
- ],
151
- }))
152
-
153
- // Make sure to rerender the tx flow component
154
- txFlowKey.value ++
155
-
156
- // On following iterations we want to keep existing artifact data
157
- clearExisting = false
158
- }
159
- }
160
-
161
- const receipt = await txFlow.value.initializeRequest(() => writeContract($wagmi, {
162
- abi: MINT_ABI,
163
- chainId,
164
- address: collection.value.address,
165
- functionName: 'create',
166
- args: [
167
- name.value,
168
- description.value,
169
- multiTransactionPrepare ? [] : artifact,
170
- 0,
171
- 0n,
172
- ],
173
- }))
174
-
175
- const logs = receipt.logs.map(log => decodeEventLog({
176
- abi: MINT_ABI,
177
- data: log.data,
178
- topics: log.topics,
179
- strict: false,
180
- }))
181
-
182
- const mintedEvent = logs.find(log => log.eventName === 'TransferSingle')
183
-
184
- await store.fetchToken(collection.value.address, mintedEvent.args.id)
185
-
186
- // Force update the collection mint ID
187
- store.collections[collection.value.address].latestTokenId = mintedEvent.args.id
188
-
189
- await navigateTo({
190
- name: 'id-collection-tokenId',
191
- params: { id: id.value, collection: collection.value.address, tokenId: mintedEvent.args.id }
192
- })
193
- } catch (e) {
194
- console.error(e)
195
- }
196
-
197
- minting.value = false
198
- }
199
-
19
+ const id = useArtistId()
20
+ const store = useOnchainStore()
200
21
  const subdomain = useSubdomain()
201
22
  const isMe = useIsMe()
202
23
 
@@ -225,55 +46,3 @@ useMetaData({
225
46
  })
226
47
  </script>
227
48
 
228
- <style scoped>
229
- #mint-token {
230
- display: grid;
231
-
232
- @media (--md) {
233
- grid-template-columns: 40% 1fr;
234
- }
235
-
236
- @media (--lg) {
237
- grid-template-columns: repeat(2, minmax(0, 1fr));
238
- }
239
- }
240
-
241
- .preview {
242
- height: 100%;
243
- place-content: center;
244
-
245
- .image,
246
- svg {
247
- margin-bottom: var(--spacer-sm);
248
- width: 100%;
249
- }
250
-
251
- svg {
252
- box-shadow: var(--border-shadow);
253
- }
254
-
255
- h1 {
256
- display: flex;
257
- gap: var(--spacer-sm);
258
- align-items: baseline;
259
- font-size: var(--font-lg);
260
- }
261
-
262
- p {
263
- color: var(--muted);
264
- }
265
- }
266
-
267
- form {
268
- width: 100%;
269
-
270
- > div {
271
- display: grid;
272
- gap: var(--spacer);
273
- }
274
- }
275
-
276
- select.choose-mode {
277
- width: fit-content;
278
- }
279
- </style>
@@ -0,0 +1,43 @@
1
+ <template>
2
+ <Authenticated>
3
+ <PageFrame :title="breadcrumb" class="inset" id="manage-renderers">
4
+ <RendererOverview :collection="collection" />
5
+ </PageFrame>
6
+ </Authenticated>
7
+ </template>
8
+
9
+ <script setup>
10
+ const id = useArtistId()
11
+
12
+ const props = defineProps(['collection'])
13
+ const store = useOnchainStore()
14
+ const collection = computed(() => props.collection)
15
+
16
+ const subdomain = useSubdomain()
17
+ const isMe = useIsMe()
18
+
19
+ const breadcrumb = computed(() => {
20
+ const path = subdomain.value || isMe.value ? [] : [
21
+ {
22
+ text: store.displayName(id.value),
23
+ to: { name: 'id', params: { id: id.value } }
24
+ }
25
+ ]
26
+
27
+ return [
28
+ ...path,
29
+ {
30
+ text: `${ collection.value.name }`,
31
+ to: { name: 'id-collection', params: { id: id.value, collection: collection.value.address } }
32
+ },
33
+ {
34
+ text: `Manage Renderers`
35
+ },
36
+ ]
37
+ })
38
+
39
+ useMetaData({
40
+ title: `Manage Renderers | ${collection.value.name}`,
41
+ })
42
+ </script>
43
+
package/utils/abis.ts CHANGED
@@ -19,7 +19,6 @@ export const FACTORY_ABI = parseAbi([
19
19
  'function version() pure returns (uint256)'
20
20
  ])
21
21
 
22
-
23
22
  export const MINT_ABI = parseAbi([
24
23
  'error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId)',
25
24
  'error ERC1155InvalidApprover(address approver)',
@@ -75,3 +74,9 @@ export const MINT_ABI = parseAbi([
75
74
  'function version() view returns (uint256)',
76
75
  'function withdraw()'
77
76
  ])
77
+
78
+ export const RENDERER_ABI = parseAbi([
79
+ 'function name() external pure returns (string memory)',
80
+ 'function version() external pure returns (uint version)',
81
+ ])
82
+
package/utils/types.ts CHANGED
@@ -34,8 +34,9 @@ export interface Collection {
34
34
  description: string
35
35
  initBlock: bigint
36
36
  latestTokenId: bigint
37
- tokens: { [key: string]: Token }
38
37
  balance: bigint
38
+ tokens: { [key: string]: Token }
39
+ renderers: Renderer[]
39
40
  }
40
41
 
41
42
  export interface Token {
@@ -60,3 +61,10 @@ export interface MintEvent {
60
61
  amount: bigint
61
62
  price: bigint
62
63
  }
64
+
65
+ export interface Renderer {
66
+ address: `0x${string}`
67
+ name: string
68
+ description?: string
69
+ version: bigint
70
+ }