Skip to content

Commit

Permalink
feat: mint nft collection (#1510)
Browse files Browse the repository at this point in the history
* initial mint nft collection form

Co-authored-by: MarkNerdi996 <[email protected]>

* initial mint nft collection confirmation form

* initial mint nft collection function

* initial mint nft for collection

* improve mint nft collection including id substitute

Co-authored-by: Tuditi <[email protected]>

* fix: mint collection activity

* fix: remove unnecessary on mount call backs

---------

Co-authored-by: MarkNerdi996 <[email protected]>
Co-authored-by: Tuditi <[email protected]>
Co-authored-by: Tuditi <[email protected]>
  • Loading branch information
4 people authored Feb 20, 2024
1 parent 89ce0e0 commit 9226fa6
Show file tree
Hide file tree
Showing 20 changed files with 512 additions and 28 deletions.
4 changes: 4 additions & 0 deletions packages/desktop/components/popup/Popup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import ManageVotingPowerPopup from './popups/ManageVotingPowerPopup.svelte'
import MintNativeTokenConfirmationPopup from './popups/MintNativeTokenConfirmationPopup.svelte'
import MintNativeTokenFormPopup from './popups/MintNativeTokenFormPopup.svelte'
import MintNftCollectionConfirmationPopup from './popups/MintNftCollectionConfirmationPopup.svelte'
import MintNftCollectionFormPopup from './popups/MintNftCollectionFormPopup.svelte'
import MintNftConfirmationPopup from './popups/MintNftConfirmationPopup.svelte'
import MintNftFormPopup from './popups/MintNftFormPopup.svelte'
import NodeAuthRequiredPopup from './popups/NodeAuthRequiredPopup.svelte'
Expand Down Expand Up @@ -122,6 +124,8 @@
[PopupId.MintNativeTokenForm]: MintNativeTokenFormPopup,
[PopupId.MintNftConfirmation]: MintNftConfirmationPopup,
[PopupId.MintNftForm]: MintNftFormPopup,
[PopupId.MintNftCollectionForm]: MintNftCollectionFormPopup,
[PopupId.MintNftCollectionConfirmation]: MintNftCollectionConfirmationPopup,
[PopupId.NodeAuthRequired]: NodeAuthRequiredPopup,
[PopupId.NodeInfo]: NodeInfoPopup,
[PopupId.ReceiveAddress]: ReceiveAddressPopup,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<script lang="ts">
import { Table, Avatar, Tabs } from '@bloomwalletio/ui'
import { selectedAccount } from '@core/account/stores'
import { handleError } from '@core/error/handlers/handleError'
import { localize } from '@core/i18n'
import { CURRENT_IRC27_VERSION } from '@core/nfts'
import { getClient } from '@core/profile-manager'
import { checkActiveProfileAuthAsync, getBaseToken } from '@core/profile/actions'
import { formatTokenAmountPrecise } from '@core/token'
import { buildNftOutputBuilderParams, mintNftCollection, mintNftCollectionDetails } from '@core/wallet'
import { PopupId, closePopup, openPopup } from '@desktop/auxiliary/popup'
import { MediaIcon, PopupTab, getTabItems } from '@ui'
import { onMount } from 'svelte'
import PopupTemplate from '../PopupTemplate.svelte'
const TABS = getTabItems([PopupTab.Transaction, PopupTab.Nft, PopupTab.NftMetadata])
let selectedTab = TABS[0]
let storageDeposit: number = 0
const { standard, type, uri, name, issuerName, description, attributes } = $mintNftCollectionDetails || {}
$: irc27Metadata = {
standard,
version: CURRENT_IRC27_VERSION,
name,
type,
uri,
...(issuerName && { issuerName }),
...(description && { description }),
...(attributes && { attributes }),
}
async function setStorageDeposit(): Promise<void> {
try {
const outputData = buildNftOutputBuilderParams(irc27Metadata, $selectedAccount.depositAddress)
const client = await getClient()
const preparedOutput = await client.buildNftOutput(outputData)
storageDeposit = Number(preparedOutput.amount) ?? 0
} catch (err) {
handleError(err)
}
}
function onBackClick(): void {
closePopup()
openPopup({
id: PopupId.MintNftCollectionForm,
overflow: true,
confirmClickOutside: true,
})
}
async function onConfirmClick(): Promise<void> {
try {
await checkActiveProfileAuthAsync()
} catch (err) {
return
}
try {
await mintNftCollection(irc27Metadata)
closePopup()
} catch (err) {
handleError(err)
}
}
onMount(() => {
try {
void setStorageDeposit()
} catch (err) {
handleError(err)
}
})
</script>

<PopupTemplate
title={localize('popups.mintNftForm.title')}
backButton={{
text: localize('actions.back'),
disabled: $selectedAccount?.isTransferring,
onClick: onBackClick,
}}
continueButton={{
text: localize('actions.confirm'),
disabled: $selectedAccount?.isTransferring,
onClick: onConfirmClick,
}}
busy={$selectedAccount?.isTransferring}
>
<div class="max-h-100 scrollable-y flex-1">
<nft-details class="flex flex-col justify-center items-center space-y-5">
<Avatar size="lg" shape="square" surface={2}>
<MediaIcon {type} size="base" surface={2} />
</Avatar>
<activity-details class="w-full h-full space-y-2 flex flex-auto flex-col shrink-0">
<Tabs bind:selectedTab tabs={TABS} />
{#if selectedTab.key === PopupTab.Transaction}
<Table
items={[
{
key: localize('general.storageDeposit'),
value: formatTokenAmountPrecise(storageDeposit, getBaseToken()),
},
{
key: localize('general.immutableIssuer'),
value: $selectedAccount?.depositAddress,
truncate: true,
},
]}
/>
{:else if selectedTab.key === PopupTab.Nft}
<Table
items={[
{
key: localize('general.name'),
value: name,
},
{
key: localize('general.description'),
value: description ? description : undefined,
},
{
key: localize('general.uri'),
value: uri,
},
{
key: localize('general.issuerName'),
value: issuerName ? issuerName : undefined,
},
]}
/>
{:else if selectedTab.key === PopupTab.NftMetadata}
<Table
items={[
{
key: localize('general.metadata'),
value: irc27Metadata,
copyable: true,
},
]}
/>
{/if}
</activity-details>
</nft-details>
</div>
</PopupTemplate>
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<script lang="ts">
import { BaseError } from '@core/error/classes'
import { localize } from '@core/i18n'
import { NftStandard, composeUrlFromNftUri } from '@core/nfts'
import { MimeType } from '@core/nfts/enums'
import { fetchWithTimeout } from '@core/nfts/utils/fetchWithTimeout'
import { HttpHeader } from '@core/utils'
import { isValidUri } from '@core/utils/validation'
import { IMintNftCollectionDetails } from '@core/wallet'
import { mintNftCollectionDetails, setMintNftCollectionDetails } from '@core/wallet/stores'
import { PopupId, closePopup, openPopup } from '@desktop/auxiliary/popup'
import { OptionalInput } from '@ui'
import { Error, TextInput } from '@bloomwalletio/ui'
import PopupTemplate from '../PopupTemplate.svelte'
let { standard, version, type, uri, name, issuerName, description, attributes } = $mintNftCollectionDetails || {}
interface IOptionalInputs {
[key: string]: {
inputType: 'text' | 'number'
isInteger?: boolean
value: string
error: string
isOpen?: boolean
}
}
const optionalInputs: IOptionalInputs = {
issuerName: {
inputType: 'text',
value: issuerName,
error: '',
},
description: {
inputType: 'text',
value: description,
error: '',
},
attributes: {
inputType: 'text',
value: attributes ? JSON.stringify(attributes) : undefined,
error: '',
},
}
let uriError: string, nameError: string
const error: BaseError = null
function onCancelClick(): void {
closePopup()
}
async function onContinueClick(): Promise<void> {
resetErrors()
const valid = await validate()
if (valid) {
setMintNftCollectionDetails(convertInputsToMetadataType())
openPopup({
id: PopupId.MintNftCollectionConfirmation,
overflow: true,
confirmClickOutside: true,
})
}
}
async function validate(): Promise<boolean> {
if (name.length === 0) {
nameError = localize('popups.mintNftForm.errors.emptyName')
}
if (optionalInputs.quantity?.isOpen) {
if (Number(optionalInputs.quantity.value) < 1) {
optionalInputs.quantity.error = localize('popups.mintNftForm.errors.quantityTooSmall')
}
if (Number(optionalInputs.quantity.value) >= 64) {
optionalInputs.quantity.error = localize('popups.mintNftForm.errors.quantityTooLarge')
}
}
if (uri.length === 0 || !isValidUri(uri)) {
uriError = localize('popups.mintNftForm.errors.invalidURI')
} else {
try {
const response = await fetchWithTimeout(composeUrlFromNftUri(uri), 1, { method: 'HEAD' })
if (response.status === 200 || response.status === 304) {
type = response.headers.get(HttpHeader.ContentType)
} else {
uriError = localize('popups.mintNftForm.errors.notReachable')
}
} catch (err) {
uriError = localize('popups.mintNftForm.errors.notReachable')
}
}
if (optionalInputs.attributes.isOpen) {
validateAttributes()
}
const optionalInputsErrors = Object.values(optionalInputs).map((optionalInput) => optionalInput.error)
const hasErrors = Object.values({ ...optionalInputsErrors, nameError, uriError }).some((e) => e !== '')
return !hasErrors
}
function resetErrors(): void {
nameError = ''
uriError = ''
for (const key of Object.keys(optionalInputs)) {
optionalInputs[key].error = ''
}
}
function validateAttributes(): void {
let attributes: unknown
try {
attributes = JSON.parse(optionalInputs.attributes.value)
} catch (err) {
optionalInputs.attributes.error = localize('popups.mintNftForm.errors.attributesMustBeJSON')
return
}
if (!Array.isArray(attributes)) {
optionalInputs.attributes.error = localize('popups.mintNftForm.errors.attributesMustBeArrayOfObjects')
return
}
const isArrayOfObjects = attributes.every(
(attribute) => typeof attribute === 'object' && !Array.isArray(attribute) && attribute !== null
)
if (!isArrayOfObjects) {
optionalInputs.attributes.error = localize('popups.mintNftForm.errors.attributesMustBeArrayOfObjects')
return
}
const isKeysValid = attributes.every(
(attribute) =>
Object.keys(attribute).every((key) => key === 'trait_type' || key === 'value') &&
Object.keys(attribute).filter((key) => key === 'trait_type').length === 1 &&
Object.keys(attribute).filter((key) => key === 'value').length === 1
)
if (!isKeysValid) {
optionalInputs.attributes.error = localize('popups.mintNftForm.errors.attributesInvalidKeys')
return
}
const isValuesValid = attributes.every(
(attribute) =>
(typeof attribute.trait_type === 'string' &&
attribute.trait_type.length > 0 &&
typeof attribute.value === 'string' &&
attribute.value.length > 0) ||
typeof attribute.value === 'number'
)
if (!isValuesValid) {
optionalInputs.attributes.error = localize('popups.mintNftForm.errors.attributesInvalidValues')
return
}
}
function convertInputsToMetadataType(): IMintNftCollectionDetails {
return {
standard: standard ?? NftStandard.Irc27,
version,
issuerName: optionalInputs.issuerName?.value,
description: optionalInputs.description?.value,
uri,
name,
attributes: optionalInputs.attributes?.value ? JSON.parse(optionalInputs.attributes.value) : undefined,
type: type as MimeType,
}
}
</script>

<PopupTemplate
title={localize('actions.mintNftCollection')}
backButton={{
text: localize('actions.cancel'),
onClick: onCancelClick,
}}
continueButton={{
text: localize('actions.continue'),
onClick: onContinueClick,
}}
>
<popup-inputs class="block space-y-5 max-h-100 scrollable-y overflow-x-hidden flex-1">
<TextInput bind:value={uri} bind:error={uriError} label={localize('general.uri')} />
<TextInput bind:value={name} bind:error={nameError} label={localize('general.name')} />
<optional-inputs class="flex flex-row flex-wrap gap-4">
{#each Object.keys(optionalInputs) as key}
<OptionalInput
bind:value={optionalInputs[key].value}
bind:error={optionalInputs[key].error}
bind:isOpen={optionalInputs[key].isOpen}
inputType={optionalInputs[key].inputType}
isInteger={optionalInputs[key]?.isInteger}
label={localize(`general.${key}`)}
description={localize(`tooltips.mintNftForm.${key}`)}
fontSize={14}
/>
{/each}
</optional-inputs>
{#if error}
<Error error={error?.message} />
{/if}
</popup-inputs>
</PopupTemplate>
Loading

0 comments on commit 9226fa6

Please sign in to comment.