@visualizevalue/mint-app-base 0.1.45 → 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 +10 -0
- package/components/Collection/Actions.vue +26 -0
- package/components/Collection/Intro.vue +1 -12
- package/components/Connect.client.vue +6 -1
- package/components/Form/Group.vue +1 -1
- package/components/Icon.vue +1 -0
- package/components/Mint/Action.client.vue +51 -0
- package/components/Mint/Detail.client.vue +33 -0
- package/components/Mint/Preview.client.vue +43 -0
- package/components/Mint/Renderer/Base.client.vue +92 -0
- package/components/Page/Frame.vue +1 -0
- package/components/Renderer/InstallButton.vue +62 -0
- package/components/Renderer/InstallCustom.client.vue +91 -0
- package/components/Renderer/Overview.client.vue +73 -0
- package/components/Renderer/OverviewCard.vue +32 -0
- package/components/Token/MintTimeline.client.vue +24 -21
- package/components/TransactionFlow.vue +5 -3
- package/composables/base.ts +7 -0
- package/composables/collections.ts +44 -2
- package/composables/createMint.ts +138 -0
- package/index.d.ts +11 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/pages/[id]/[collection]/mint.vue +8 -239
- package/pages/[id]/[collection]/renderers.vue +43 -0
- package/pages/[id]/create.vue +4 -1
- package/utils/abis.ts +6 -1
- package/utils/types.ts +9 -1
package/app.config.ts
ADDED
|
@@ -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
|
-
<
|
|
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>
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
@click="() => login(connector)"
|
|
19
19
|
class="choose-connector-button"
|
|
20
20
|
>
|
|
21
|
-
<img
|
|
21
|
+
<img
|
|
22
|
+
v-if="ICONS[connector.name]"
|
|
23
|
+
:src="connector.icon || `${base}icons/wallets/${ICONS[connector.name]}`"
|
|
24
|
+
:alt="connector.name"
|
|
25
|
+
>
|
|
22
26
|
{{ connector.name }}
|
|
23
27
|
</Button>
|
|
24
28
|
</div>
|
|
@@ -38,6 +42,7 @@ const ICONS = {
|
|
|
38
42
|
|
|
39
43
|
const props = defineProps(['class'])
|
|
40
44
|
const emit = defineEmits(['connected', 'disconnected'])
|
|
45
|
+
const base = useBaseURL()
|
|
41
46
|
|
|
42
47
|
const chainId = useChainId()
|
|
43
48
|
const { connectors, connect } = useConnect()
|
package/components/Icon.vue
CHANGED
|
@@ -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
|
+
|
|
@@ -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>
|
|
@@ -55,24 +55,6 @@ const mintedAtBlock = computed(() => token.untilBlock - MINT_BLOCKS)
|
|
|
55
55
|
const backfillComplete = computed(() => token.mintsBackfilledUntilBlock <= mintedAtBlock.value)
|
|
56
56
|
|
|
57
57
|
const loading = ref(true)
|
|
58
|
-
onMounted(async () => {
|
|
59
|
-
loading.value = true
|
|
60
|
-
try {
|
|
61
|
-
console.info(`Attempting to load + backfill token mints for #${token.tokenId}`)
|
|
62
|
-
await state.fetchTokenMints(token)
|
|
63
|
-
await state.backfillTokenMints(token)
|
|
64
|
-
} catch (e) {
|
|
65
|
-
console.error(e)
|
|
66
|
-
}
|
|
67
|
-
loading.value = false
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
watch(currentBlock, () => {
|
|
71
|
-
if (loading.value) return
|
|
72
|
-
|
|
73
|
-
state.fetchTokenMints(token)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
58
|
const loadMore = ref()
|
|
77
59
|
const loadMoreVisible = useElementVisibility(loadMore)
|
|
78
60
|
const backfill = async () => {
|
|
@@ -81,9 +63,11 @@ const backfill = async () => {
|
|
|
81
63
|
try {
|
|
82
64
|
await state.backfillTokenMints(token)
|
|
83
65
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
// If we're not fully backfilled and we have less than 20 mints loaded,
|
|
67
|
+
// continue backfilling events.
|
|
68
|
+
while (! backfillComplete.value && mints.value?.length < 20) {
|
|
69
|
+
await delay(250)
|
|
70
|
+
await state.backfillTokenMints(token)
|
|
87
71
|
}
|
|
88
72
|
} catch (e) {
|
|
89
73
|
console.error(`Issue during backfill`, e)
|
|
@@ -91,12 +75,31 @@ const backfill = async () => {
|
|
|
91
75
|
|
|
92
76
|
loading.value = false
|
|
93
77
|
}
|
|
78
|
+
|
|
79
|
+
onMounted(async () => {
|
|
80
|
+
loading.value = true
|
|
81
|
+
try {
|
|
82
|
+
console.info(`Attempting to load + backfill token mints for #${token.tokenId}`)
|
|
83
|
+
await state.fetchTokenMints(token)
|
|
84
|
+
await backfill()
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(e)
|
|
87
|
+
}
|
|
88
|
+
loading.value = false
|
|
89
|
+
})
|
|
90
|
+
|
|
94
91
|
watch(loadMoreVisible, () => {
|
|
95
92
|
// Skip if we have enough mints for the viewport or we're already loading
|
|
96
93
|
if (! loadMoreVisible.value || loading.value) return
|
|
97
94
|
|
|
98
95
|
backfill()
|
|
99
96
|
})
|
|
97
|
+
|
|
98
|
+
watch(currentBlock, () => {
|
|
99
|
+
if (loading.value) return
|
|
100
|
+
|
|
101
|
+
state.fetchTokenMints(token)
|
|
102
|
+
})
|
|
100
103
|
</script>
|
|
101
104
|
|
|
102
105
|
<style scoped>
|
|
@@ -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 =
|
|
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,8 +3,8 @@ 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 =
|
|
7
|
-
export const MAX_BLOCK_RANGE =
|
|
6
|
+
export const CURRENT_STATE_VERSION = 5
|
|
7
|
+
export const MAX_BLOCK_RANGE = 1800n
|
|
8
8
|
export const MINT_BLOCKS = BLOCKS_PER_DAY
|
|
9
9
|
|
|
10
10
|
export const useOnchainStore = () => {
|
|
@@ -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
package/package.json
CHANGED
|
@@ -1,202 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Authenticated>
|
|
3
|
-
<PageFrame :title="breadcrumb" class="inset wide"
|
|
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
|
-
<
|
|
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
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
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
|
|
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/pages/[id]/create.vue
CHANGED
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
<div class="card">
|
|
54
54
|
<div>
|
|
55
55
|
<FormSelectFile @change="setImage" />
|
|
56
|
-
<p v-if="! isSmall" class="muted"
|
|
56
|
+
<p v-if="! isSmall" class="muted">
|
|
57
|
+
<small>Note: This should be a small file, prefferably a simple SVG like <a :href="`${base}example-contract-icon.svg`" target="_blank">this one (273 bytes)</a>. Try to make it less than 10kb.</small>
|
|
58
|
+
</p>
|
|
57
59
|
</div>
|
|
58
60
|
<FormGroup>
|
|
59
61
|
<FormInput v-model="title" placeholder="Title" required class="title" />
|
|
@@ -77,6 +79,7 @@
|
|
|
77
79
|
|
|
78
80
|
<script setup>
|
|
79
81
|
const config = useRuntimeConfig()
|
|
82
|
+
const base = useBaseURL()
|
|
80
83
|
const store = useOnchainStore()
|
|
81
84
|
const chainId = useMainChainId()
|
|
82
85
|
|
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
|
+
}
|