Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix IPFS issue related to NFTs #1134

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/components/AssetListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@
import { Component, Mixins, Prop } from 'vue-property-decorator';

import TranslationMixin from './mixins/TranslationMixin';
import NftTokenLogo from './NftTokenLogo.vue';
import TokenAddress from './TokenAddress.vue';
import TokenLogo from './TokenLogo.vue';

import type { Asset } from '@sora-substrate/util/build/assets/types';

@Component({
components: {
NftTokenLogo,
TokenLogo,
TokenAddress,
},
Expand Down
31 changes: 26 additions & 5 deletions src/components/NftDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@
<script lang="ts">
import { Prop, Component, Mixins } from 'vue-property-decorator';

import { IpfsStorage } from '@/util/ipfsStorage';

import TranslationMixin from './mixins/TranslationMixin';

const UrlCreator = window.URL || window.webkitURL;

@Component
export default class NftDetails extends Mixins(TranslationMixin) {
@Prop({ default: '', type: String }) readonly contentLink!: string;
@Prop({ default: '', type: String }) readonly tokenContent!: string;
@Prop({ default: '', type: String }) readonly tokenName!: string;
@Prop({ default: '', type: String }) readonly tokenSymbol!: string;
@Prop({ default: '', type: String }) readonly tokenDescription!: string;
Expand All @@ -68,11 +71,7 @@ export default class NftDetails extends Mixins(TranslationMixin) {
return [this.image];
}

private async checkImageAvailability(): Promise<void> {
if (!this.contentLink) {
return;
}

private async parseExternalLink(): Promise<void> {
try {
const response = await fetch(this.contentLink);
const buffer = await response.blob();
Expand All @@ -90,6 +89,28 @@ export default class NftDetails extends Mixins(TranslationMixin) {
}
}

private async parseIpfsLink(): Promise<void> {
const imageObj = await IpfsStorage.requestImage(this.tokenContent);
if (imageObj) {
this.image = imageObj.image;
this.$emit('update-link', imageObj.link);
} else {
this.badLink = true;
}
this.imageLoading = false;
}

private async checkImageAvailability(): Promise<void> {
if (!(this.contentLink || this.tokenContent)) {
return;
}
if (this.contentLink) {
await this.parseExternalLink();
} else {
await this.parseIpfsLink();
}
}

handleDetailsClick(): void {
this.nftDetailsClicked = !this.nftDetailsClicked;
this.$emit('click-details');
Expand Down
42 changes: 11 additions & 31 deletions src/components/NftTokenLogo.vue
Original file line number Diff line number Diff line change
@@ -1,48 +1,28 @@
<template>
<img
v-show="asset.content && showNftImage"
class="asset-logo nft-image"
:src="nftImageUrl"
ref="nftImage"
@load="handleNftImageLoad"
@error="hideNftImage"
/>
<img v-show="src" class="asset-logo nft-image" :alt="nftAlt" :src="src" />
</template>

<script lang="ts">
import { Component, Prop, Vue, Ref } from 'vue-property-decorator';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';

import { IpfsStorage } from '../util/ipfsStorage';

import type { Asset } from '@sora-substrate/util/build/assets/types';

@Component
export default class NftTokenLogo extends Vue {
@Prop({ default: () => {}, type: Object, required: true }) readonly asset!: Asset;
@Ref('nftImage') readonly nftImage!: HTMLImageElement;
@Prop({ default: '', type: String, required: true }) readonly link!: string;

showNftImage = false;
src = '';

get nftImageUrl(): string {
if (this.asset.content) {
return IpfsStorage.constructFullIpfsUrl(this.asset.content);
@Watch('link', { immediate: true })
private async handleLinkUpdate(link?: string): Promise<void> {
if (link) {
const obj = await IpfsStorage.requestImage(link);
this.src = (obj || {}).image || '';

Check warning on line 20 in src/components/NftTokenLogo.vue

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-wallet-web Sonarqube Results

src/components/NftTokenLogo.vue#L20

Prefer using an optional chain expression instead, as it's more concise and easier to read.

Check warning on line 20 in src/components/NftTokenLogo.vue

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-wallet-web Sonarqube Results

src/components/NftTokenLogo.vue#L20

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.

Check warning on line 20 in src/components/NftTokenLogo.vue

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-wallet-web Sonarqube Results

src/components/NftTokenLogo.vue#L20

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
}
return '';
}

handleNftImageLoad(): void {
const imgElement = this.nftImage as HTMLImageElement;
if (imgElement) {
this.showNftImage = imgElement.complete && imgElement.naturalHeight !== 0;
} else {
this.showNftImage = false;
}
}

hideNftImage(): void {
this.showNftImage = false;
get nftAlt(): string {
return `NFT icon: ${this.link}`;
}
}
</script>

<style></style>
8 changes: 6 additions & 2 deletions src/components/TokenLogo.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="logo">
<span :class="iconClasses" :style="iconStyles" />
<nft-token-logo v-if="isNft" class="asset-logo__nft-image" :class="iconClasses" :asset="token" />
<nft-token-logo v-if="isNft" class="asset-logo__nft-image" :class="iconClasses" :link="tokenObj.content" />
</div>
</template>

Expand Down Expand Up @@ -32,12 +32,16 @@
@Prop({ type: String, default: LogoSize.MEDIUM, required: false }) readonly size!: LogoSize;
@Prop({ default: false, type: Boolean }) readonly withClickableLogo!: boolean;

get tokenObj(): Asset {
return this.token || ({} as Asset);

Check warning on line 36 in src/components/TokenLogo.vue

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-wallet-web Sonarqube Results

src/components/TokenLogo.vue#L36

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
}

get isNft(): boolean {
return !!this.token && api.assets.isNft(this.token);
}

get assetAddress(): Nullable<string> {
return this.tokenSymbol ? this.whitelistIdsBySymbol[this.tokenSymbol] : (this.token || {}).address;
return this.tokenSymbol ? this.whitelistIdsBySymbol[this.tokenSymbol] : this.tokenObj.address;
}

get whitelistedItem(): Nullable<WhitelistItem> {
Expand Down
31 changes: 10 additions & 21 deletions src/components/WalletAssetDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
<nft-details
v-if="isNft"
is-asset-details
:content-link="nftContentLink"
:token-content="asset.content"
:token-name="asset.name"
:token-symbol="asset.symbol"
:token-description="asset.description"
@click-details="handleClickNftDetails"
@update-link="setNftContentLink"
/>
<template v-else>
<token-logo :token="asset" size="bigger" />
Expand Down Expand Up @@ -211,14 +212,11 @@ export default class WalletAssetDetails extends Mixins(
}

get displayedNftContentLink(): string {
const hostname = IpfsStorage.getStorageHostname(this.nftContentLink);
const path = IpfsStorage.getIpfsPath(this.nftContentLink);
return shortenValue(hostname + '/ipfs/' + path, 25);
return shortenValue(this.nftContentLink, 25);
}

private async setNftMeta(): Promise<void> {
const ipfsPath = this.asset.content as string;
this.nftContentLink = IpfsStorage.constructFullIpfsUrl(ipfsPath);
setNftContentLink(link: string): void {
this.nftContentLink = link;
}

handleClickNftDetails(): void {
Expand All @@ -228,15 +226,9 @@ export default class WalletAssetDetails extends Mixins(
async handleCopyNftLink(): Promise<void> {
await copyToClipboard(this.nftContentLink);
this.wasNftLinkCopied = true;
await delay(1000);
await delay(1_000);
this.wasNftLinkCopied = false;
}

mounted(): void {
if (this.isNft) {
this.setNftMeta();
}
}
// __________________________________________________________

formatBalance(value: CodecString): string {
Expand Down Expand Up @@ -341,13 +333,10 @@ export default class WalletAssetDetails extends Mixins(
}

handleOperation(operation: Operations): void {
switch (operation) {
case Operations.Send:
this.navigate({ name: RouteNames.WalletSend, params: { asset: this.asset } });
break;
default:
this.$emit(operation, this.asset);
break;
if (operation === Operations.Send) {
this.navigate({ name: RouteNames.WalletSend, params: { asset: this.asset } });
} else {
this.$emit(operation, this.asset);
}
}

Expand Down
68 changes: 63 additions & 5 deletions src/util/ipfsStorage.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,71 @@
enum IpfsProviders {
Pinata = 'pinata',
Dweb = 'dweb',
NFTStorage = 'nftstorage',
Default = 'ipfs.io',
}

type IpfsProvider = {
type: IpfsProviders;
fn: (link: string) => string;
};

export class IpfsStorage {
static IPFS_GATEWAY = 'https://ipfs.io/ipfs/';
static UCAN_TOKEN_HOST_PROVIDER = 'https://ucan.polkaswap2.io/ucan.json';
private static readonly IPFS_PREFIX = 'https://ipfs.io/ipfs/';
private static readonly PINATA_PREFIX = 'https://gateway.pinata.cloud/ipfs/';
private static readonly DWEB_SUFFIX = '.ipfs.dweb.link/';
private static readonly NFTSTORAGE_SUFFIX = '.ipfs.nftstorage.link/';

private static readonly UCAN_TOKEN_HOST_PROVIDER = 'https://ucan.polkaswap2.io/ucan.json';

private static readonly Providers: IpfsProvider[] = [
{ type: IpfsProviders.Pinata, fn: IpfsStorage.getPinataUrl },
{ type: IpfsProviders.Dweb, fn: IpfsStorage.getDwebUrl },
{ type: IpfsProviders.NFTStorage, fn: IpfsStorage.getNftstorageUrl },
{ type: IpfsProviders.Default, fn: IpfsStorage.getIpfsUrl },
];

static async getUcanTokens(): Promise<Record<string, string>> {
const response = await fetch(this.UCAN_TOKEN_HOST_PROVIDER, { cache: 'no-cache' });
const response = await fetch(IpfsStorage.UCAN_TOKEN_HOST_PROVIDER, { cache: 'no-cache' });
return response.json();
}

static constructFullIpfsUrl(path: string): string {
return this.IPFS_GATEWAY + path;
static async requestImage(link: string): Promise<{ image: string; link: string } | null> {
for (const { fn } of IpfsStorage.Providers) {
try {
const path = fn(link);
const response = await fetch(path);
if (response.ok) {
const blob = await response.blob();
return { image: URL.createObjectURL(blob), link: path };
}
} catch (error) {
// do nothing
// console.error(`IPFS: Failed to download "${link}" using "${type}"`, error);
}
}
return null;
}

private static getIpfsUrl(link: string): string {
return IpfsStorage.IPFS_PREFIX + link;
}

/** Alternative url if `getIpfsUrl` does't work */
private static getNftstorageUrl(link: string): string {
const [path, name] = link.split('/');
return `https://${path}${IpfsStorage.NFTSTORAGE_SUFFIX}${name}`;
}

/** Alternative url if `getIpfsUrl` does't work */
private static getDwebUrl(link: string): string {
const [path, name] = link.split('/');
return `https://${path}${IpfsStorage.DWEB_SUFFIX}${name}`;
}

/** Alternative url if `getIpfsUrl` does't work */
private static getPinataUrl(link: string): string {
return IpfsStorage.PINATA_PREFIX + link;
}

static getStorageHostname(link: string): string {
Expand All @@ -18,6 +75,7 @@

static getIpfsPath(url: string): string {
const path = new URL(url).pathname;
// TODO: !!! provider

Check notice on line 78 in src/util/ipfsStorage.ts

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-wallet-web Sonarqube Results

src/util/ipfsStorage.ts#L78

Complete the task associated to this "TODO" comment.
return path.replace(/\/ipfs\//, '');
}

Expand Down