Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.

Commit 4301f61

Browse files
authored
Merge pull request #3060 from kodadot/660-non-fungible-snek
Basilisk NFT Marketplace
2 parents 9dc5f9e + 70c5eff commit 4301f61

File tree

107 files changed

+2583
-1700
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+2583
-1700
lines changed

components/base/BaseCollectionForm.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const components = {
5050
5151
@Component({ components })
5252
export default class BaseCollectionForm extends Vue {
53-
@Prop({ type: String, default: 'context' }) label!: string
53+
@Prop({ type: String, default: 'mint.collection.create' }) label!: string
5454
@Prop(Boolean) protectiveMargin!: boolean
5555
@PropSync('name', { type: String }) vName!: string
5656
@PropSync('description', { type: String }) vDescription!: string

components/base/CarouselCardList.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<template #item="list">
1212
<div class="card mx-4">
1313
<div class="card-image">
14-
<nuxt-link :to="`/rmrk/gallery/${list.id}`">
14+
<nuxt-link :to="`/${urlPrefix}/gallery/${list.id}`">
1515
<PreviewMediaResolver
1616
v-if="list.animationUrl"
1717
:src="list.animationUrl"
@@ -38,7 +38,7 @@
3838
</p>
3939
</b-field>
4040
<nuxt-link
41-
:to="{ name: 'rmrk-u-id', params: { id: list.issuer } }">
41+
:to="{ name: profileUrl, params: { id: list.issuer } }">
4242
<div class="is-size-7 icon-text">
4343
<b-icon icon="palette" />
4444
<Identity
@@ -51,7 +51,7 @@
5151
<nuxt-link
5252
v-if="list.currentOwner"
5353
:to="{
54-
name: 'rmrk-u-id',
54+
name: profileUrl,
5555
params: { id: list.currentOwner },
5656
}">
5757
<div class="is-size-7 icon-text">
@@ -80,6 +80,7 @@ import { Component, mixins, Prop } from 'nuxt-property-decorator'
8080
import AuthMixin from '@/utils/mixins/authMixin'
8181
8282
import type { CarouselNFT } from './types'
83+
import PrefixMixin from '~/utils/mixins/prefixMixin'
8384
8485
const components = {
8586
// Identicon,
@@ -95,14 +96,18 @@ const components = {
9596
@Component<CarouselList>({
9697
components,
9798
})
98-
export default class CarouselList extends mixins(AuthMixin) {
99+
export default class CarouselList extends mixins(AuthMixin, PrefixMixin) {
99100
@Prop({ type: Array, required: true }) nfts!: CarouselNFT[]
100101
@Prop({ type: Number, default: 1 }) page!: number
101102
@Prop({ type: String, default: 'rmrk/gallery' }) url!: string
102103
get current() {
103104
return this.page - 1 // 0-indexed
104105
}
105106
107+
get profileUrl() {
108+
return `${this.urlPrefix}-u-id`
109+
}
110+
106111
get options() {
107112
return {
108113
itemsToShow: 2,

components/base/SubmitButton.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
:disabled="disabled"
88
:loading="loading"
99
outlined>
10-
{{ $t(label) }}
10+
<slot>
11+
{{ $t(label) }}
12+
</slot>
1113
</b-button>
1214
</b-field>
1315
</template>

components/bsx/Create/Create.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<div class="mb-6">
3+
<CreateCollection />
4+
</div>
5+
</template>
6+
7+
<script lang="ts">
8+
import { Component, Vue } from 'nuxt-property-decorator'
9+
import CreateCollection from './CreateCollection.vue'
10+
11+
@Component({
12+
components: {
13+
CreateCollection,
14+
},
15+
})
16+
export default class Create extends Vue {}
17+
</script>
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<template>
2+
<div>
3+
<Loader v-model="isLoading" :status="status" />
4+
<BaseCollectionForm v-bind.sync="base">
5+
<template v-slot:main>
6+
<b-field class="mb-5" />
7+
</template>
8+
<template v-slot:footer>
9+
<CustomAttributeInput
10+
:max="10"
11+
v-model="attributes"
12+
class="mb-3"
13+
visible="collapse.collection.attributes.show"
14+
hidden="collapse.collection.attributes.hide" />
15+
<b-field>
16+
<p class="has-text-weight-medium is-size-6 has-text-warning">
17+
{{ $t('mint.deposit') }}:
18+
<Money :value="collectionDeposit" inline />
19+
</p>
20+
</b-field>
21+
<SubmitButton
22+
label="create collection"
23+
:disabled="disabled"
24+
:loading="isLoading"
25+
@click="submit" />
26+
</template>
27+
</BaseCollectionForm>
28+
</div>
29+
</template>
30+
31+
<script lang="ts">
32+
import { Attribute } from '@/components/rmrk/types'
33+
import {
34+
getclassDeposit,
35+
getMetadataDeposit,
36+
} from '@/components/unique/apiConstants'
37+
import { getRandomValues, hasEnoughToken } from '@/components/unique/utils'
38+
import onApiConnect from '@/utils/api/general'
39+
import { uploadDirect } from '@/utils/directUpload'
40+
import formatBalance from '@/utils/formatBalance'
41+
import { mapToId } from '@/utils/mappers'
42+
import AuthMixin from '@/utils/mixins/authMixin'
43+
import ChainMixin from '@/utils/mixins/chainMixin'
44+
import MetaTransactionMixin from '@/utils/mixins/metaMixin'
45+
import PrefixMixin from '@/utils/mixins/prefixMixin'
46+
import { notificationTypes, showNotification } from '@/utils/notification'
47+
import { pinJson, PinningKey } from '@/utils/pinning'
48+
import resolveQueryPath from '@/utils/queryPathResolver'
49+
import { getImageTypeSafe, pinImageSafe } from '@/utils/safePin'
50+
import { estimate } from '@/utils/transactionExecutor'
51+
import { unwrapSafe } from '@/utils/uniquery'
52+
import { createMetadata, unSanitizeIpfsUrl } from '@kodadot1/minimark'
53+
import Connector from '@kodadot1/sub-api'
54+
import { Component, mixins } from 'nuxt-property-decorator'
55+
import { dummyIpfsCid } from '@/utils/ipfs'
56+
57+
type BaseCollectionType = {
58+
name: string
59+
file: File | null
60+
description: string
61+
}
62+
63+
const components = {
64+
Loader: () => import('@/components/shared/Loader.vue'),
65+
BasicInput: () => import('@/components/shared/form/BasicInput.vue'),
66+
BaseCollectionForm: () => import('@/components/base/BaseCollectionForm.vue'),
67+
BasicSwitch: () => import('@/components/shared/form/BasicSwitch.vue'),
68+
SubmitButton: () => import('@/components/base/SubmitButton.vue'),
69+
Money: () => import('@/components/shared/format/Money.vue'),
70+
CustomAttributeInput: () =>
71+
import('@/components/rmrk/Create/CustomAttributeInput.vue'),
72+
}
73+
74+
@Component({ components })
75+
export default class CreateCollection extends mixins(
76+
MetaTransactionMixin,
77+
ChainMixin,
78+
AuthMixin,
79+
PrefixMixin
80+
) {
81+
private base: BaseCollectionType = {
82+
name: '',
83+
file: null,
84+
description: '',
85+
}
86+
private hasSupport = true
87+
protected collectionDeposit = ''
88+
protected id = '0'
89+
protected attributes: Attribute[] = []
90+
91+
public async created() {
92+
onApiConnect(() => {
93+
const classDeposit = getclassDeposit()
94+
const metadataDeposit = getMetadataDeposit()
95+
this.collectionDeposit = (classDeposit + metadataDeposit).toString()
96+
})
97+
}
98+
99+
get disabled(): boolean {
100+
const {
101+
base: { name },
102+
accountId,
103+
} = this
104+
return !(name && accountId)
105+
}
106+
107+
get balance(): string {
108+
return this.$store.getters.getAuthBalance
109+
}
110+
111+
public async constructMeta() {
112+
const { file, name, description } = this.base
113+
114+
const pinningKey: PinningKey = await this.$store.dispatch(
115+
'pinning/fetchPinningKey',
116+
this.accountId
117+
)
118+
119+
const imageHash = await pinImageSafe(file, pinningKey.token)
120+
const type = getImageTypeSafe(file)
121+
const attributes = this.attributes.map((val) => ({
122+
...val,
123+
display_type: null,
124+
}))
125+
const meta = createMetadata(
126+
name,
127+
description,
128+
imageHash,
129+
undefined,
130+
attributes,
131+
undefined,
132+
type
133+
)
134+
const metaHash = await pinJson(meta, imageHash)
135+
136+
return unSanitizeIpfsUrl(metaHash)
137+
}
138+
139+
protected async generateNewCollectionId(): Promise<number> {
140+
const randomNumbers = getRandomValues(10).map(String)
141+
const query = await resolveQueryPath(
142+
this.urlPrefix,
143+
'existingCollectionList'
144+
)
145+
const cols = this.$apollo.query({
146+
query: query.default,
147+
client: this.urlPrefix,
148+
variables: {
149+
ids: randomNumbers,
150+
},
151+
})
152+
const {
153+
data: { collectionEntities },
154+
} = await cols
155+
const collectionList = unwrapSafe(collectionEntities)
156+
const existingIds = collectionList.map(mapToId)
157+
const newId = randomNumbers.find((id) => !existingIds.includes(id))
158+
return Number(newId)
159+
}
160+
161+
protected cretateArgs(
162+
randomId: number,
163+
metadata: string
164+
): [number, { Marketplace: null }, string] {
165+
return [randomId, { Marketplace: null }, metadata]
166+
}
167+
168+
protected tryToEstimateTx(): Promise<string> {
169+
const { api } = Connector.getInstance()
170+
const cb = api.tx.utility.batchAll
171+
const metadata = dummyIpfsCid()
172+
const randomId = 0
173+
const args = [this.cretateArgs(randomId, metadata)]
174+
return estimate(this.accountId, cb, args)
175+
}
176+
177+
protected async checkBalanceBeforeTx(): Promise<void> {
178+
const estimated = await this.tryToEstimateTx()
179+
const deposit = this.collectionDeposit
180+
const hasTokens = hasEnoughToken(this.balance, estimated, deposit)
181+
this.$consola.log('hasTokens', hasTokens)
182+
if (!hasTokens) {
183+
throw new Error(
184+
`Not enough tokens: Currently have ${formatBalance(
185+
this.balance,
186+
this.decimals,
187+
this.unit
188+
)} tokens`
189+
)
190+
}
191+
}
192+
193+
protected async submit(): Promise<void> {
194+
this.isLoading = true
195+
this.status = 'loader.checkBalance'
196+
197+
try {
198+
// await this.checkBalanceBeforeTx()
199+
showNotification(
200+
`Creating collection ${this.base.name}`,
201+
notificationTypes.info
202+
)
203+
this.status = 'loader.ipfs'
204+
const metadata = await this.constructMeta()
205+
// const metadata = 'ipfs://ipfs/QmaCWgK91teVsQuwLDt56m2xaUfBCCJLeCsPeJyHEenoES'
206+
const { api } = Connector.getInstance()
207+
const cb = api.tx.nft.createClass
208+
const randomId = await this.generateNewCollectionId()
209+
210+
const args = this.cretateArgs(randomId, metadata)
211+
212+
if (this.base.file) {
213+
this.$consola.log('[UPLOADING FILE]')
214+
uploadDirect(this.base.file, this.accountId).catch(this.$consola.warn)
215+
}
216+
217+
await this.howAboutToExecute(this.accountId, cb, args, (blockNumber) => {
218+
showNotification(
219+
`[Collection] Saved ${this.base.name} in block ${blockNumber}`,
220+
notificationTypes.success
221+
)
222+
})
223+
} catch (e: any) {
224+
showNotification(`[ERR] ${e}`, notificationTypes.danger)
225+
this.$consola.error(e)
226+
this.isLoading = false
227+
}
228+
}
229+
}
230+
</script>

0 commit comments

Comments
 (0)