@visualizevalue/mint-app-base 0.1.117 → 0.1.118
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 +7 -0
- package/app.config.ts +21 -0
- package/components/Mint/Detail.client.vue +2 -0
- package/components/Mint/Renderer/Code.client.vue +5 -81
- package/components/Mint/Renderer/P5.client.vue +5 -81
- package/components/Mint/Renderer/ScriptRenderer.client.vue +89 -0
- package/components/Mint/Renderer/Tone.client.vue +7 -0
- package/components/MintTokenBar.vue +4 -1
- package/components/Token/Detail.client.vue +5 -1
- package/composables/mintConfig.ts +11 -0
- package/composables/mintDefault.ts +29 -0
- package/locales/en.json +4 -0
- package/nuxt.config.ts +3 -0
- package/package.json +1 -1
- package/utils/toneScript.ts +36 -0
package/.env.example
CHANGED
|
@@ -8,6 +8,13 @@ NUXT_PUBLIC_DOMAIN=localhost
|
|
|
8
8
|
NUXT_PUBLIC_TITLE=Mint
|
|
9
9
|
NUXT_PUBLIC_DESCRIPTION=To mint is a human right.
|
|
10
10
|
|
|
11
|
+
# =========================
|
|
12
|
+
# MINT DEFAULTS
|
|
13
|
+
# =========================
|
|
14
|
+
# NUXT_PUBLIC_MINT_AMOUNT=1
|
|
15
|
+
# NUXT_PUBLIC_MINT_STEP=1
|
|
16
|
+
# NUXT_PUBLIC_MINT_VALUE=5
|
|
17
|
+
|
|
11
18
|
# =========================
|
|
12
19
|
# ARTIST SCOPE
|
|
13
20
|
# =========================
|
package/app.config.ts
CHANGED
|
@@ -9,6 +9,13 @@ export default defineAppConfig({
|
|
|
9
9
|
address: '0xe5d2da253c7d4b7609afce15332bb1a1fb461d09',
|
|
10
10
|
description: 'The default renderer',
|
|
11
11
|
},
|
|
12
|
+
// {
|
|
13
|
+
// component: 'Markdown',
|
|
14
|
+
// name: 'Markdown Renderer',
|
|
15
|
+
// version: 1n,
|
|
16
|
+
// address: '0x0000000000000000000000000000000000000000',
|
|
17
|
+
// description: 'Renders markdown content as onchain text artifacts',
|
|
18
|
+
// },
|
|
12
19
|
{
|
|
13
20
|
component: 'P5',
|
|
14
21
|
name: 'P5 Renderer',
|
|
@@ -23,6 +30,13 @@ export default defineAppConfig({
|
|
|
23
30
|
address: '0xcb681409046e45e6187ec2205498e4adbe19749c',
|
|
24
31
|
description: 'Allows linking to both an image and an animation url',
|
|
25
32
|
},
|
|
33
|
+
// {
|
|
34
|
+
// component: 'Tone',
|
|
35
|
+
// name: 'Tone Renderer',
|
|
36
|
+
// version: 1n,
|
|
37
|
+
// address: '0x0000000000000000000000000000000000000000',
|
|
38
|
+
// description: 'Allows using Tone.js scripts for generative audio',
|
|
39
|
+
// },
|
|
26
40
|
{
|
|
27
41
|
component: 'P5',
|
|
28
42
|
name: 'P5 Renderer',
|
|
@@ -62,6 +76,13 @@ export default defineAppConfig({
|
|
|
62
76
|
address: '0xeeaf428251c477002d52c69e022b357a91f36517',
|
|
63
77
|
description: 'Allows linking to both an image and an animation url',
|
|
64
78
|
},
|
|
79
|
+
{
|
|
80
|
+
component: 'Tone',
|
|
81
|
+
name: 'Tone Renderer',
|
|
82
|
+
version: 1n,
|
|
83
|
+
address: '0xcc95b3cbbefa6ef3a657f1fb7848115b79bfef24',
|
|
84
|
+
description: 'Allows using Tone.js scripts for generative audio',
|
|
85
|
+
},
|
|
65
86
|
{
|
|
66
87
|
component: 'P5',
|
|
67
88
|
name: 'P5 Renderer (Sepolia)',
|
|
@@ -4,6 +4,7 @@ import Base from './Renderer/Base.client.vue'
|
|
|
4
4
|
import Code from './Renderer/Code.client.vue'
|
|
5
5
|
import Markdown from './Renderer/Markdown.client.vue'
|
|
6
6
|
import P5 from './Renderer/P5.client.vue'
|
|
7
|
+
import Tone from './Renderer/Tone.client.vue'
|
|
7
8
|
|
|
8
9
|
const components = {
|
|
9
10
|
Animation,
|
|
@@ -11,6 +12,7 @@ const components = {
|
|
|
11
12
|
Code,
|
|
12
13
|
Markdown,
|
|
13
14
|
P5,
|
|
15
|
+
Tone,
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const props = defineProps(['collection'])
|
|
@@ -1,83 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</Button>
|
|
8
|
-
<Button @click="select('script')" :class="{ active: active === 'script' }">
|
|
9
|
-
{{ $t('mint.code.script') }}
|
|
10
|
-
</Button>
|
|
11
|
-
</template>
|
|
12
|
-
|
|
13
|
-
<template #content="{ active }">
|
|
14
|
-
<!-- We only hide the form to maintain the file select state -->
|
|
15
|
-
<MintRendererBase v-show="active === 'base'" decouple-artifact />
|
|
16
|
-
<!-- We force rerender the code editor on active -->
|
|
17
|
-
<CodeEditor
|
|
18
|
-
v-if="active === 'script'"
|
|
19
|
-
v-model="script"
|
|
20
|
-
class="full"
|
|
21
|
-
ref="codeEditor"
|
|
22
|
-
/>
|
|
23
|
-
</template>
|
|
24
|
-
</Tabs>
|
|
25
|
-
</div>
|
|
2
|
+
<MintRendererScriptRenderer
|
|
3
|
+
i18n-key="code"
|
|
4
|
+
:default-script="DEFAULT_P5_SCRIPT"
|
|
5
|
+
:get-html-uri="getP5HtmlUri"
|
|
6
|
+
/>
|
|
26
7
|
</template>
|
|
27
|
-
|
|
28
|
-
<script setup>
|
|
29
|
-
import { watchDebounced } from '@vueuse/core'
|
|
30
|
-
|
|
31
|
-
const { artifact, image, animationUrl } = useCreateMintData()
|
|
32
|
-
|
|
33
|
-
const script = ref(DEFAULT_P5_SCRIPT)
|
|
34
|
-
|
|
35
|
-
// Keep the animationURL (for the preview) up to date
|
|
36
|
-
const updateUrl = () => {
|
|
37
|
-
animationUrl.value = getP5HtmlUri('Preview', script.value)
|
|
38
|
-
}
|
|
39
|
-
watchDebounced(script, updateUrl, { debounce: 500, maxWait: 3000 })
|
|
40
|
-
onMounted(updateUrl)
|
|
41
|
-
|
|
42
|
-
// Encode the artifact as per how the P5Renderer.sol contract expects it.
|
|
43
|
-
watchEffect(() => {
|
|
44
|
-
artifact.value = encodeAbiParameters(
|
|
45
|
-
[
|
|
46
|
-
{ type: 'string', name: 'image' },
|
|
47
|
-
{ type: 'string', name: 'script' },
|
|
48
|
-
],
|
|
49
|
-
[image.value, script.value],
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<style>
|
|
55
|
-
.mint-renderer-p5 {
|
|
56
|
-
padding: 0 !important;
|
|
57
|
-
border: 0 !important;
|
|
58
|
-
display: flex;
|
|
59
|
-
flex-direction: column;
|
|
60
|
-
gap: 0;
|
|
61
|
-
overflow: hidden;
|
|
62
|
-
width: 100%;
|
|
63
|
-
|
|
64
|
-
> .tabs {
|
|
65
|
-
height: min-content;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
> .tabs-content {
|
|
69
|
-
border: var(--border);
|
|
70
|
-
border-radius: var(--card-border-radius);
|
|
71
|
-
overflow: hidden;
|
|
72
|
-
height: 100%;
|
|
73
|
-
|
|
74
|
-
> * {
|
|
75
|
-
height: 100%;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
> *:not(.full) {
|
|
79
|
-
padding: var(--spacer);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
</style>
|
|
@@ -1,83 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</Button>
|
|
8
|
-
<Button @click="select('script')" :class="{ active: active === 'script' }">
|
|
9
|
-
{{ $t('mint.p5.script') }}
|
|
10
|
-
</Button>
|
|
11
|
-
</template>
|
|
12
|
-
|
|
13
|
-
<template #content="{ active }">
|
|
14
|
-
<!-- We only hide the form to maintain the file select state -->
|
|
15
|
-
<MintRendererBase v-show="active === 'base'" decouple-artifact />
|
|
16
|
-
<!-- We force rerender the code editor on active -->
|
|
17
|
-
<CodeEditor
|
|
18
|
-
v-if="active === 'script'"
|
|
19
|
-
v-model="script"
|
|
20
|
-
class="full"
|
|
21
|
-
ref="codeEditor"
|
|
22
|
-
/>
|
|
23
|
-
</template>
|
|
24
|
-
</Tabs>
|
|
25
|
-
</div>
|
|
2
|
+
<MintRendererScriptRenderer
|
|
3
|
+
i18n-key="p5"
|
|
4
|
+
:default-script="DEFAULT_P5_SCRIPT"
|
|
5
|
+
:get-html-uri="getP5HtmlUri"
|
|
6
|
+
/>
|
|
26
7
|
</template>
|
|
27
|
-
|
|
28
|
-
<script setup>
|
|
29
|
-
import { watchDebounced } from '@vueuse/core'
|
|
30
|
-
|
|
31
|
-
const { artifact, image, animationUrl } = useCreateMintData()
|
|
32
|
-
|
|
33
|
-
const script = ref(DEFAULT_P5_SCRIPT)
|
|
34
|
-
|
|
35
|
-
// Keep the animationURL (for the preview) up to date
|
|
36
|
-
const updateUrl = () => {
|
|
37
|
-
animationUrl.value = getP5HtmlUri('Preview', script.value)
|
|
38
|
-
}
|
|
39
|
-
watchDebounced(script, updateUrl, { debounce: 500, maxWait: 3000 })
|
|
40
|
-
onMounted(updateUrl)
|
|
41
|
-
|
|
42
|
-
// Encode the artifact as per how the P5Renderer.sol contract expects it.
|
|
43
|
-
watchEffect(() => {
|
|
44
|
-
artifact.value = encodeAbiParameters(
|
|
45
|
-
[
|
|
46
|
-
{ type: 'string', name: 'image' },
|
|
47
|
-
{ type: 'string', name: 'script' },
|
|
48
|
-
],
|
|
49
|
-
[image.value, script.value],
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
</script>
|
|
53
|
-
|
|
54
|
-
<style>
|
|
55
|
-
.mint-renderer-p5 {
|
|
56
|
-
padding: 0 !important;
|
|
57
|
-
border: 0 !important;
|
|
58
|
-
display: flex;
|
|
59
|
-
flex-direction: column;
|
|
60
|
-
gap: 0;
|
|
61
|
-
overflow: hidden;
|
|
62
|
-
width: 100%;
|
|
63
|
-
|
|
64
|
-
> .tabs {
|
|
65
|
-
height: min-content;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
> .tabs-content {
|
|
69
|
-
border: var(--border);
|
|
70
|
-
border-radius: var(--card-border-radius);
|
|
71
|
-
overflow: hidden;
|
|
72
|
-
height: 100%;
|
|
73
|
-
|
|
74
|
-
> * {
|
|
75
|
-
height: 100%;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
> *:not(.full) {
|
|
79
|
-
padding: var(--spacer);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mint-renderer-script">
|
|
3
|
+
<Tabs initial="base">
|
|
4
|
+
<template #menu="{ active, select }">
|
|
5
|
+
<Button @click="select('base')" :class="{ active: active === 'base' }">
|
|
6
|
+
{{ $t(`mint.${i18nKey}.static`) }}
|
|
7
|
+
</Button>
|
|
8
|
+
<Button @click="select('script')" :class="{ active: active === 'script' }">
|
|
9
|
+
{{ $t(`mint.${i18nKey}.script`) }}
|
|
10
|
+
</Button>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<template #content="{ active }">
|
|
14
|
+
<!-- We only hide the form to maintain the file select state -->
|
|
15
|
+
<MintRendererBase v-show="active === 'base'" decouple-artifact />
|
|
16
|
+
<!-- We force rerender the code editor on active -->
|
|
17
|
+
<CodeEditor
|
|
18
|
+
v-if="active === 'script'"
|
|
19
|
+
v-model="script"
|
|
20
|
+
class="full"
|
|
21
|
+
ref="codeEditor"
|
|
22
|
+
/>
|
|
23
|
+
</template>
|
|
24
|
+
</Tabs>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup>
|
|
29
|
+
import { watchDebounced } from '@vueuse/core'
|
|
30
|
+
|
|
31
|
+
const props = defineProps({
|
|
32
|
+
i18nKey: { type: String, required: true },
|
|
33
|
+
defaultScript: { type: String, required: true },
|
|
34
|
+
getHtmlUri: { type: Function, required: true },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const { artifact, image, animationUrl } = useCreateMintData()
|
|
38
|
+
|
|
39
|
+
const script = ref(props.defaultScript)
|
|
40
|
+
|
|
41
|
+
// Keep the animationURL (for the preview) up to date
|
|
42
|
+
const updateUrl = () => {
|
|
43
|
+
animationUrl.value = props.getHtmlUri('Preview', script.value)
|
|
44
|
+
}
|
|
45
|
+
watchDebounced(script, updateUrl, { debounce: 500, maxWait: 3000 })
|
|
46
|
+
onMounted(updateUrl)
|
|
47
|
+
|
|
48
|
+
// Encode the artifact as per how the renderer contract expects it.
|
|
49
|
+
watchEffect(() => {
|
|
50
|
+
artifact.value = encodeAbiParameters(
|
|
51
|
+
[
|
|
52
|
+
{ type: 'string', name: 'image' },
|
|
53
|
+
{ type: 'string', name: 'script' },
|
|
54
|
+
],
|
|
55
|
+
[image.value, script.value],
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.mint-renderer-script {
|
|
62
|
+
padding: 0 !important;
|
|
63
|
+
border: 0 !important;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: 0;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
width: 100%;
|
|
69
|
+
|
|
70
|
+
> .tabs {
|
|
71
|
+
height: min-content;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
> .tabs-content {
|
|
75
|
+
border: var(--border);
|
|
76
|
+
border-radius: var(--card-border-radius);
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
height: 100%;
|
|
79
|
+
|
|
80
|
+
> * {
|
|
81
|
+
height: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
> *:not(.full) {
|
|
85
|
+
padding: var(--spacer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
type="number"
|
|
6
6
|
v-model="mintCount"
|
|
7
7
|
min="1"
|
|
8
|
+
:step="step"
|
|
8
9
|
required
|
|
9
10
|
class="amount"
|
|
10
11
|
/>
|
|
@@ -40,11 +41,13 @@ const props = defineProps({
|
|
|
40
41
|
mintRequest: Function,
|
|
41
42
|
transactionFlowConfig: Object,
|
|
42
43
|
minted: Function,
|
|
44
|
+
step: { type: Number, default: 1 },
|
|
45
|
+
defaultAmount: { type: Number, default: 1 },
|
|
43
46
|
})
|
|
44
47
|
|
|
45
48
|
const mintCount = defineModel('mintCount', { default: '1' })
|
|
46
49
|
const onMinted = () => {
|
|
47
|
-
mintCount.value =
|
|
50
|
+
mintCount.value = String(props.defaultAmount)
|
|
48
51
|
props.minted()
|
|
49
52
|
}
|
|
50
53
|
</script>
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
mintRequest,
|
|
42
42
|
transactionFlowConfig,
|
|
43
43
|
minted,
|
|
44
|
+
step,
|
|
45
|
+
defaultAmount,
|
|
44
46
|
}"
|
|
45
47
|
/>
|
|
46
48
|
</div>
|
|
@@ -72,7 +74,9 @@ const { token } = defineProps<{
|
|
|
72
74
|
const store = useOnchainStore()
|
|
73
75
|
const collection = computed(() => store.collection(token.collection))
|
|
74
76
|
|
|
75
|
-
const
|
|
77
|
+
const { defaultAmount, step } = useMintDefault()
|
|
78
|
+
const mintCount = ref(String(defaultAmount.value))
|
|
79
|
+
watch(defaultAmount, (v) => { mintCount.value = String(v) })
|
|
76
80
|
const ownedBalance = computed(
|
|
77
81
|
() => collection.value && store.tokenBalance(collection.value.address, token.tokenId),
|
|
78
82
|
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const useMintConfig = () => {
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
const config = useRuntimeConfig()
|
|
4
|
+
|
|
5
|
+
// Query param > ENV > default
|
|
6
|
+
const amount = computed(() => Number(route.query.amount) || Number(config.public.mintAmount) || 1)
|
|
7
|
+
const step = computed(() => Number(route.query.step) || Number(config.public.mintStep) || 1)
|
|
8
|
+
const value = computed(() => Number(route.query.value) || Number(config.public.mintValue) || 0)
|
|
9
|
+
|
|
10
|
+
return { amount, step, value }
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const useMintDefault = () => {
|
|
2
|
+
const { amount, step, value } = useMintConfig()
|
|
3
|
+
const gasPrice = useGasPrice()
|
|
4
|
+
const priceFeed = usePriceFeedStore()
|
|
5
|
+
|
|
6
|
+
const defaultAmount = computed(() => {
|
|
7
|
+
if (! value.value) return amount.value
|
|
8
|
+
|
|
9
|
+
const gasPriceWei = gasPrice.value.wei || 0n
|
|
10
|
+
const ethUSDRaw = priceFeed.ethUSDRaw || 0n
|
|
11
|
+
|
|
12
|
+
if (! gasPriceWei || ! ethUSDRaw) return amount.value
|
|
13
|
+
|
|
14
|
+
// Unit price in wei: basefee * 60_000
|
|
15
|
+
const unitPriceWei = gasPriceWei * 60_000n
|
|
16
|
+
|
|
17
|
+
// Convert target USD to cents, compute unit price in cents
|
|
18
|
+
// ethUSDRaw has 8 decimals from Chainlink
|
|
19
|
+
// unitPriceCents = (unitPriceWei * ethUSDRaw) / 1e18 / 1e6
|
|
20
|
+
const targetCents = BigInt(Math.round(value.value * 100))
|
|
21
|
+
const unitPriceCents = (unitPriceWei * ethUSDRaw) / (10n ** 18n) / (10n ** 6n)
|
|
22
|
+
|
|
23
|
+
if (! unitPriceCents) return amount.value
|
|
24
|
+
|
|
25
|
+
return Math.max(1, Number(targetCents / unitPriceCents))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
return { defaultAmount, step }
|
|
29
|
+
}
|
package/locales/en.json
CHANGED
package/nuxt.config.ts
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const DEFAULT_TONE_SCRIPT =
|
|
2
|
+
`const synth = new Tone.Synth().toDestination()
|
|
3
|
+
|
|
4
|
+
document.addEventListener('click', async () => {
|
|
5
|
+
await Tone.start()
|
|
6
|
+
synth.triggerAttackRelease('C4', '8n')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
// Display a simple visual indicator
|
|
10
|
+
const canvas = document.createElement('canvas')
|
|
11
|
+
document.body.appendChild(canvas)
|
|
12
|
+
canvas.width = canvas.height = Math.min(window.innerWidth, window.innerHeight)
|
|
13
|
+
const ctx = canvas.getContext('2d')
|
|
14
|
+
|
|
15
|
+
ctx.fillStyle = '#000'
|
|
16
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
17
|
+
ctx.fillStyle = '#fff'
|
|
18
|
+
ctx.font = canvas.width / 20 + 'px monospace'
|
|
19
|
+
ctx.textAlign = 'center'
|
|
20
|
+
ctx.fillText('Click to play', canvas.width / 2, canvas.height / 2)
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
export const getToneHtml = (title: string, script: string) =>
|
|
24
|
+
`<html>
|
|
25
|
+
<head>
|
|
26
|
+
<title>${title}</title>
|
|
27
|
+
<link rel="stylesheet" href="data:text/css;base64,aHRtbHtoZWlnaHQ6MTAwJX1ib2R5e21pbi1oZWlnaHQ6MTAwJTttYXJnaW46MDtwYWRkaW5nOjB9Y2FudmFze3BhZGRpbmc6MDttYXJnaW46YXV0bztkaXNwbGF5OmJsb2NrO3Bvc2l0aW9uOmFic29sdXRlO3RvcDowO2JvdHRvbTowO2xlZnQ6MDtyaWdodDowfQ==">
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
|
|
31
|
+
<script>${script}</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>`
|
|
34
|
+
|
|
35
|
+
export const getToneHtmlUri = (title: string, script: string) =>
|
|
36
|
+
`data:text/html;base64,${Buffer.from(getToneHtml(title, script)).toString('base64')}`
|