Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.
68 changes: 56 additions & 12 deletions dashboard/src/components/rmrk/Gallery/AvailableActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
</option>
</b-select>
</b-field>
<b-field v-if="showMeta" label="Meta">
<b-input v-model="meta"></b-input>
</b-field>
<component v-if="showMeta" :is="showMeta" @input="updateMeta" />
<b-button
v-if="showSubmit"
type="is-primary"
Expand All @@ -32,24 +30,34 @@ import Connector from '@vue-polkadot/vue-api';
import exec, { execResultValue } from '@/utils/transactionExecutor';
import { notificationTypes, showNotification } from '@/utils/notification';
import { getInstance, RmrkType } from '../service/RmrkService';
import { unpin } from '@/pinata';
import Consolidator from '../service/Consolidator';
import RmrkVersionMixin from '@/utils/mixins/rmrkVersionMixin';

const ownerActions = ['SEND', 'CONSUME', 'LIST'];
const buyActions = ['BUY'];

const needMeta: Record<string, boolean> = {
SEND: true,
LIST: true
const needMeta: Record<string, string> = {
SEND: 'AddressInput',
LIST: 'BalanceInput'
};

@Component({})
const components = {
BalanceInput: () => import('@/components/shared/BalanceInput.vue'),
AddressInput: () => import('@/components/shared/AddressInput.vue')
}

@Component({ components })
export default class AvailableActions extends Mixins(RmrkVersionMixin) {
@Prop() public currentOwnerId!: string;
@Prop() public accountId!: string;
@Prop() public price!: string;
@Prop() public nftId!: string;
@Prop() public imageHash!: string;
@Prop() public metadataHash!: string;
@Prop() public animationHash!: string;
private selectedAction: string = '';
private meta: string = '';
private meta: string | number = '';
private isLoading: boolean = false;

get actions() {
Expand Down Expand Up @@ -90,20 +98,27 @@ export default class AvailableActions extends Mixins(RmrkVersionMixin) {
}`;
}

private async submit() {
protected updateMeta(value: string | number) {
this.meta = value;
}

protected async submit() {
const { api } = Connector.getInstance();
const rmrkService = getInstance();
const rmrk = this.constructRmrk();
try {
showNotification(rmrk)
console.log('submit', rmrk);
const isSend = this.selectedAction === 'SEND';
const cb = isSend ? api.tx.utility.batch : api.tx.system.remark
const arg = isSend ? [api.tx.system.remark(rmrk), api.tx.balances.transfer(this.currentOwnerId, this.price)] : rmrk
const isBuy = this.selectedAction === 'BUY';
const cb = isBuy ? api.tx.utility.batch : api.tx.system.remark
const arg = isBuy ? [api.tx.system.remark(rmrk), api.tx.balances.transfer(this.currentOwnerId, this.price)] : rmrk
const tx = await exec(this.accountId, '', cb, [arg]);
showNotification(execResultValue(tx), notificationTypes.success)
console.warn('TX IN', tx);
const persisted = await rmrkService?.resolve(rmrk, this.accountId);
if (this.selectedAction === 'CONSUME') {
this.unpinNFT()
}
console.log(persisted)
console.log('SAVED', persisted?._id);
showNotification(`[TEXTILE] ${persisted?._id}`, notificationTypes.success)
Expand All @@ -112,5 +127,34 @@ export default class AvailableActions extends Mixins(RmrkVersionMixin) {
console.error(e);
}
}

protected unpinNFT() {
[this.imageHash, this.metadataHash, this.animationHash]
.forEach(async hash => {
if (hash) {
try {
await unpin(hash)
} catch (e) {
console.warn(`[ACTIONS] Cannot Unpin ${hash} because: ${e}`)
}
}
})

}

protected async consolidate(): Promise<boolean> {
const rmrkService = getInstance();
await rmrkService?.checkExpiredOrElseRefresh()

if (!rmrkService) {
console.warn('NO RMRK SERVICE, Live your life on the edge')
return true;
}

const nft = await rmrkService?.getNFT(this.nftId)
return Consolidator.consolidate(this.selectedAction, nft, this.currentOwnerId, this.accountId)
}


}
</script>
88 changes: 45 additions & 43 deletions dashboard/src/components/rmrk/Gallery/GalleryItem.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,23 @@
<template>
<div class="wrapper">
<div class="tile is-ancestor">
<div class="tile is-8 is-vertical is-parent">
<div class="tile is-child box">
<b-image
v-if="!isLoading && imageVisible"
:src="nft.image || require('@/assets/kodadot_logo_v1_transparent_400px.png')"
:src-fallback="require('@/assets/kodadot_logo_v1_transparent_400px.png')"
alt="NFT minted image"
ratio="1by1"
></b-image>
<b-skeleton height="524px" size="is-large" :active="isLoading"></b-skeleton>
<div class="columns">
<div class="column is-8 is-offset-2">
<div class="box">
<b-image
v-if="!isLoading && imageVisible"
:src="nft.image || require('@/assets/kodadot_logo_v1_transparent_400px.png')"
:src-fallback="require('@/assets/kodadot_logo_v1_transparent_400px.png')"
alt="NFT minted image"
ratio="1by1"
></b-image>
<b-skeleton height="524px" size="is-large" :active="isLoading"></b-skeleton>

<MediaResolver v-if="nft.animation_url" :class="{ withPicture: imageVisible }" :src="nft.animation_url" :mimeType="mimeType" />
<Appreciation :accountId="accountId" :currentOwnerId="nft.currentOwner" :nftId="nft.id" />
<PackSaver v-if="accountId" :accountId="accountId" :currentOwnerId="nft.currentOwner" :nftId="nft.id" />
<div class="card">
<div class="card-content">
<p class="title is-size-2">
{{ $t('legend')}}
</p>
<p class="subtitle is-size-7">
<b v-if="!isLoading"># {{ nftId }}</b>
<b-skeleton size="is-large" :active="isLoading"></b-skeleton>
<b-tag v-if="nft.price" type="is-dark" size="is-medium">
<Money :value="nft.price" :inline="true" />
</b-tag>
<p v-if="!isLoading"
class="subtitle is-size-5">
{{ nft.description }}
</p>
<b-skeleton :count="3" size="is-large" :active="isLoading"></b-skeleton>
</p>
</div>
</div>
</div>
</div>

<div class="tile is-vertical is-parent">
<Name :nft="nft" :isLoading="isLoading" />
<br>
<b-collapse class="card" animation="slide"
aria-id="contentIdForA11y3" :open="false">

<b-collapse class="card" animation="slide"
aria-id="contentIdForA11y3" :open="false">
<template #trigger="props">
<div
class="card-header"
Expand All @@ -65,9 +42,29 @@
</div>
</div>
</b-collapse>
<Sharing />
<br>
<Facts :nft="nft" />
<Name :nft="nft" :isLoading="isLoading" />
<div class="card">
<div class="card-content">
<p class="title is-size-2">
{{ $t('legend')}}
</p>
<p class="subtitle is-size-7">
<b-tag v-if="nft.price" type="is-dark" size="is-medium">
<Money :value="nft.price" :inline="true" />
</b-tag>
<p v-if="!isLoading"
class="subtitle is-size-5">
<!-- <markdown-it-vue class="md-body" :content="nft.description" /> -->
<!-- <markdown-it-vue-light class="md-body" :content="nft.description" /> -->
{{ nft.description }}
</p>
<b-skeleton :count="3" size="is-large" :active="isLoading"></b-skeleton>
</p>
</div>
</div>
<Sharing />
<Facts :nft="nft" />
</div>
</div>
</div>
</div>
Expand All @@ -76,6 +73,9 @@
<script lang="ts" >
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { getInstance } from '@/components/rmrk/service/RmrkService';
// import MarkdownItVue from 'markdown-it-vue';
// import MarkdownItVueLight from 'markdown-it-vue/dist/markdown-it-vue-light.umd.min.js';
import 'markdown-it-vue/dist/markdown-it-vue.css'
import { NFTWithMeta, NFT } from '../service/scheme';
import { fetchNFTMetadata, sanitizeIpfsUrl } from '../utils';
import { emptyObject } from '@/utils/empty';
Expand All @@ -92,6 +92,7 @@ import api from '@/fetch';
import { resolveMedia } from '../utils';
import { MediaType } from '../types';
import { MetaInfo } from 'vue-meta';
// import { VueConstructor } from 'vue';

type NFTType = NFTWithMeta;

Expand Down Expand Up @@ -120,10 +121,11 @@ type NFTType = NFTWithMeta;
components: {
AccountSelect,
AvailableActions,
Money,
Sharing,
Facts,
// MarkdownItVue: MarkdownItVue as VueConstructor<Vue>,
Money,
Name,
Sharing,
Appreciation: () => import('./Appreciation.vue'),
MediaResolver: () => import('../Media/MediaResolver.vue'),
PackSaver: () => import('../Pack/PackSaver.vue')
Expand Down
27 changes: 24 additions & 3 deletions dashboard/src/components/rmrk/service/Consolidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RmrkType } from './RmrkService';
import { RmrkInteraction } from '../types';
import { RmrkEvent, RmrkInteraction } from '../types';
import { Collection, NFT } from './scheme';
import { u8aToHex } from '@polkadot/util';
import { decodeAddress } from '@polkadot/keyring';
Expand All @@ -26,9 +26,30 @@ export default class Consolidator {
}
}

// private static canBuy() {
public static isAvailableForSale(nft: NFT, previousOwner: string) {
return Consolidator.callerEquals(nft.currentOwner, previousOwner)
}

// }
public static consolidate(action: RmrkEvent | string, nft: NFT, previousOwner: string, caller: string): boolean {
switch(action) {
case RmrkEvent.BUY:
return Consolidator.canBuy(nft, previousOwner)
case RmrkEvent.CONSUME:
return Consolidator.canConsume(nft, caller)
default:
console.warn(`[CONSOLIDATOR] NO consolidation for interaction ${action}`)
}

return true
}

private static canBuy(nft: NFT, previousOwner: string) {
return Consolidator.isAvailableForSale(nft, previousOwner);
}

private static canConsume(nft: NFT, caller: string) {
return Consolidator.callerEquals(nft.currentOwner, caller)
}

// private static canMintNft() {

Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/components/rmrk/service/RmrkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class RmrkService extends TextileService<RmrkType> implements State {
return this._client.context.withKeyInfo(keysToTheKingdom)
}

protected async checkExpiredOrElseRefresh() {
public async checkExpiredOrElseRefresh() {
console.log('checkExpiredOrElseRefresh', this.isAuthExpired)
if (this.isAuthExpired) {
try {
Expand Down
38 changes: 38 additions & 0 deletions dashboard/src/components/shared/AddressInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<div>
<b-field :label="$t(label)">
<b-input type="is-danger" v-model="value" @input="handleInput" :message="err"></b-input>
</b-field>
</div>
</template>

<script lang="ts">
import { checkAddress } from '@polkadot/util-crypto';
import { Debounce } from 'vue-debounce-decorator';
import { Component, Emit, Prop, Vue } from 'vue-property-decorator';

@Component({})
export default class AddressInput extends Vue {

private value: string = '';
private err: string | null = '';
@Prop({ default: 'insert address' }) public label!: string;

@Debounce(500)
@Emit('input')
protected handleInput(value: string) {
const [valid, err] = checkAddress(value, this.ss58Format);
this.err = err;

if (valid) {
return value
}

return ''
}

get ss58Format(): number {
return this.$store.getters.getChainProperties?.ss58Format
}
}
</script>
Loading