Skip to content

Commit

Permalink
chore: refactor ipfs config and metadata retrieval (#1517)
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Viénot <simon.vienot@icloud.com>
  • Loading branch information
svienot authored Nov 25, 2024
1 parent 5c92c29 commit a233864
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 101 deletions.
6 changes: 3 additions & 3 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The following configuration parameters can be set in `core-config.json`:
| `estimatorNotice` | `string` | No estimator notice is displayed | |
| `walletChooserDisclaimerPopup` | `string` | No wallet chooser disclaimer popup is displayed | |
| `googleTagID` | `string` | Google Tag is disabled | |
| `ipfsGatewayUrlPrefix` | `string` | Gateway `https://gateway.pinata.cloud/ipfs/` is used | |
| `ipfsGatewayURL` | `string` | Gateway `https://gateway.pinata.cloud/ipfs/` is used | |
| `cryptoName` | `string` | `HBAR` is displayed | |
| `cryptoSymbol` | `string` | `<span style="color: darkgrey">ℏ</span>` is displayed | |

Expand Down Expand Up @@ -100,8 +100,8 @@ This provides the global site tag ID to be used by Google Analytics. When specif
dialog asking the user to agree to the use of cookies before proceeding with the application. The google tag ID will
be actually used only if the user has agreed.

### `ipfsGatewayUrlPrefix`
This provides the URL prefix of the public IPFS gateway to use to resolve the IPFS URLs used in the token metadata.
### `ipfsGatewayURL`
This provides the URL of the public IPFS gateway to use to resolve the IPFS URIs (or CIDs) used in the token metadata.
By default the Pinata public IPFS gateway is used.

### `cryptoName`
Expand Down
2 changes: 1 addition & 1 deletion public/core-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"estimatorNotice": null,
"walletChooserDisclaimerPopup": null,
"googleTagID": null,
"ipfsGatewayUrlPrefix": null,
"ipfsGatewayURL": null,
"cryptoName": null,
"cryptoSymbol": null
}
2 changes: 1 addition & 1 deletion src/components/token/NftCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default defineComponent({
onMounted(() => nftLookup.mount())
onBeforeUnmount(() => nftLookup.unmount())

const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayUrlPrefix
const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayURL
const metadata = computed(() => nftLookup.entity.value?.metadata ?? '')
const metadataAnalyzer = new TokenMetadataAnalyzer(metadata, ipfsGatewayPrefix)
onMounted(() => metadataAnalyzer.mount())
Expand Down
54 changes: 16 additions & 38 deletions src/components/token/TokenMetadataAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@
*/

import {computed, ref, Ref, watch, WatchStopHandle} from "vue";
import {EntityID} from "@/utils/EntityID";
import axios from "axios";
import {Timestamp} from "@/utils/Timestamp";
import {TopicMessageCache} from "@/utils/cache/TopicMessageCache";
import {CID} from "multiformats";
import {AssetCache} from "@/utils/cache/AssetCache.ts";
import {LastTopicMessageByIdCache} from "@/utils/cache/LastTopicMessageByIdCache.ts";
import {blob2Topic, blob2URL} from "@/utils/URLUtils.ts";

export interface NftAttribute {
trait_type: string
Expand Down Expand Up @@ -54,9 +53,9 @@ export class TokenMetadataAnalyzer {
//

public readonly rawMetadata: Ref<string>
public readonly ipfsGatewayPrefix: string
public readonly ipfsGatewayPrefix: string | null

public constructor(rawMetadata: Ref<string>, ipfsGatewayPrefix: string) {
public constructor(rawMetadata: Ref<string>, ipfsGatewayPrefix: string | null) {
this.rawMetadata = rawMetadata
this.ipfsGatewayPrefix = ipfsGatewayPrefix
}
Expand Down Expand Up @@ -153,14 +152,8 @@ export class TokenMetadataAnalyzer {
const files = this.getProperty('files')
if (Array.isArray(files)) {
for (const file of files) {
if (file.uri != undefined && file.type != undefined) {
let url
if (file.uri.startsWith("ipfs://") && file.uri.length > 7) {
url = `${this.ipfsGatewayPrefix}${file.uri.substring(7)}`
} else {
url = file.uri
}

if (file.uri && file.type) { // both required by HIP-412
const url = blob2URL(file.uri, this.ipfsGatewayPrefix) ?? file.uri
result.push({
uri: file.uri,
url: url,
Expand Down Expand Up @@ -196,12 +189,8 @@ export class TokenMetadataAnalyzer {

public imageUrl = computed<string | null>(
() => {
let result = this.getProperty('image') ?? this.getProperty(('picture'))

if (result != null && result.startsWith("ipfs://") && result.length > 7) {
result = `${this.ipfsGatewayPrefix}${result.substring(7)}`
}
return result
const uri = this.getProperty('image') ?? this.getProperty(('picture'))
return blob2URL(uri, this.ipfsGatewayPrefix) ?? uri
})

//
Expand Down Expand Up @@ -245,27 +234,16 @@ export class TokenMetadataAnalyzer {
}

if (metadata.value !== null) {
if (metadata.value.startsWith('ipfs://')) {
content.value = await this.readMetadataFromUrl(`${this.ipfsGatewayPrefix}${metadata.value.substring(7)}`)
} else if (metadata.value.startsWith('hcs://')) {
const i = metadata.value.lastIndexOf('/');
const id = metadata.value.substring(i + 1);
if (EntityID.parse(id) !== null) {
content.value = await this.readMetadataFromTopic(id)
} else {
content.value = null
}
} else if (metadata.value.startsWith('https://')) {
content.value = await this.readMetadataFromUrl(metadata.value)
} else if (EntityID.parse(metadata.value) !== null) {
content.value = await this.readMetadataFromTopic(metadata.value)
} else if (Timestamp.parse(metadata.value) !== null) {
content.value = await this.readMetadataFromTimestamp(metadata.value)
const url = blob2URL(metadata.value, this.ipfsGatewayPrefix)
if (url !== null) {
content.value = await this.readMetadataFromUrl(url)
} else {
try {
CID.parse(metadata.value)
content.value = await this.readMetadataFromUrl(`${this.ipfsGatewayPrefix}${metadata.value}`)
} catch {
const topic = blob2Topic(metadata.value)
if (topic !== null) {
content.value = await this.readMetadataFromTopic(topic)
} else if (Timestamp.parse(metadata.value) !== null) {
content.value = await this.readMetadataFromTimestamp(metadata.value)
} else {
content.value = null
}
}
Expand Down
56 changes: 7 additions & 49 deletions src/components/values/BlobValue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,11 @@
<template>
<div class="should-wrap">

<template v-if="blobValue">
<template v-if="decodedValue">

<template v-if="isURL">
<span v-if="noAnchor">{{ blobValue }}</span>
<a v-else :href="blobValue">{{ blobValue }}</a>
</template>

<template v-else-if="decodedURL">
<span v-if="noAnchor">{{ decodedURL }}</span>
<a v-else :href="decodedURL.toString()">{{ decodedURL }}</a>
</template>

<template v-else-if="ipfsAddress">
<template v-if="decodedURL">
<span v-if="noAnchor">{{ decodedValue }}</span>
<a v-else :href="ipfsAddress">{{ decodedValue }}</a>
<a v-else :href="decodedURL">{{ decodedValue }}</a>
</template>

<template v-else-if="jsonValue && isNaN(jsonValue)">
Expand Down Expand Up @@ -86,6 +76,7 @@
import {computed, defineComponent, inject, PropType, ref} from "vue";
import {initialLoadingKey} from "@/AppKeys";
import {CoreConfig} from "@/config/CoreConfig";
import {blob2URL} from "@/utils/URLUtils.ts";

export default defineComponent({
name: "BlobValue",
Expand Down Expand Up @@ -122,31 +113,9 @@ export default defineComponent({
const windowWidth = inject('windowWidth', 1280)
const initialLoading = inject(initialLoadingKey, ref(false))

const isURL = computed(() => {
let result: boolean
if (props.blobValue) {
try {
const url = new URL(props.blobValue)
result = url.protocol == "http:" || url.protocol == "https:"
} catch {
result = false
}
} else {
result = false
}
return result
})
const ipfsGateway = CoreConfig.inject().ipfsGatewayURL

const decodedURL = computed(() => {
if (decodedValue.value.startsWith("http://") || decodedValue.value.startsWith("https://")) {
try {
return new URL(decodedValue.value)
} catch {
return null
}
}
return null
})
const decodedURL = computed(() => blob2URL(decodedValue.value, ipfsGateway))

const jsonValue = computed(() => {
let result
Expand All @@ -165,7 +134,7 @@ export default defineComponent({
const b64EncodingFound = computed(() => b64DecodedValue.value !== null)

const b64DecodedValue = computed(() => {
let result: string|null
let result: string | null
const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
if (props.blobValue && props.base64 && base64regex.test(props.blobValue)) {
try {
Expand Down Expand Up @@ -195,24 +164,13 @@ export default defineComponent({
return result
})

const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayUrlPrefix

const ipfsAddress = computed(() => {
if (decodedValue.value.startsWith("ipfs://") && decodedValue.value.length > 7) {
return `${ipfsGatewayPrefix}${decodedValue.value.substring(7)}`
}
return null
})

return {
isMediumScreen,
windowWidth,
isURL,
jsonValue,
b64EncodingFound,
decodedValue,
initialLoading,
ipfsAddress,
decodedURL
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/config/CoreConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ export class CoreConfig {
// Global site tag ID for Google Analytics
public readonly googleTagID: string|null,

// The URL prefix of the IPFS gateway
public readonly ipfsGatewayUrlPrefix: string,
// The URL of the IPFS gateway
public readonly ipfsGatewayURL: string|null,

// The HTML content used as crypto unit symbol
public readonly cryptoName: string,
Expand All @@ -126,7 +126,7 @@ export class CoreConfig {
fetchString(obj, "estimatorNotice"),
fetchString(obj, "walletChooserDisclaimerPopup"),
fetchString(obj, "googleTagID"),
fetchURL(obj, "ipfsGatewayUrlPrefix") ?? "https://gateway.pinata.cloud/ipfs/",
fetchURL(obj, "ipfsGatewayURL") ?? "https://gateway.pinata.cloud/ipfs/",
fetchString(obj, "cryptoName") ?? "HBAR",
fetchString(obj, "cryptoSymbol")
)
Expand Down
2 changes: 1 addition & 1 deletion src/pages/NftDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export default defineComponent({
onMounted(() => nftLookup.mount())
onBeforeUnmount(() => nftLookup.unmount())

const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayUrlPrefix
const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayURL
const metadata = computed(() => nftLookup.entity.value?.metadata ?? '')
const metadataAnalyzer = new TokenMetadataAnalyzer(metadata, ipfsGatewayPrefix)
onMounted(() => metadataAnalyzer.mount())
Expand Down
2 changes: 1 addition & 1 deletion src/pages/TokenDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ export default defineComponent({
onMounted(() => tokenAnalyzer.mount())
onBeforeUnmount(() => tokenAnalyzer.unmount())

const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayUrlPrefix
const ipfsGatewayPrefix = CoreConfig.inject().ipfsGatewayURL
const metadata = computed(() => tokenLookup.entity.value?.metadata ?? '')
const metadataAnalyzer = new TokenMetadataAnalyzer(metadata, ipfsGatewayPrefix)
onMounted(() => metadataAnalyzer.mount())
Expand Down
86 changes: 86 additions & 0 deletions src/utils/URLUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*-
*
* Hedera Mirror Node Explorer
*
* Copyright (C) 2021 - 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import {EntityID} from "@/utils/EntityID";
import {CID} from "multiformats";

export function blob2URL(blob: string | null, ipfsGateway: string | null): string | null {

let result: string | null

if (blob !== null) {
if (isSecureURL(blob)) {
result = blob
} else if (ipfsGateway && blob.startsWith('ipfs://') && blob.length > 7) {
result = `${ipfsGateway}${blob.substring(7)}`
} else if (ipfsGateway && isIPFSHash(blob)) {
result = `${ipfsGateway}${blob}`
} else {
result = null
}
} else {
result = null
}
return result
}

export function blob2Topic(blob: string | null): string | null {
let result: string | null
let id: string

if (blob !== null) {
if (blob.startsWith('hcs://') && blob.length > 6) {
const i = blob.lastIndexOf('/');
id = blob.substring(i + 1);
} else {
id = blob
}
if (EntityID.parse(id) !== null) {
result = id
} else {
result = null
}
} else {
result = null
}
return result
}

export function isSecureURL(blob: string): boolean {
let isValid: boolean
try {
const url = new URL(blob)
isValid = url.protocol == "https:"
} catch {
isValid = false
}
return isValid
}

export function isIPFSHash(blob: string): boolean {
let isValid: boolean
try {
CID.parse(blob)
isValid = true
} catch {
isValid = false
}
return isValid
}
7 changes: 3 additions & 4 deletions tests/unit/values/BlobValue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,14 @@ describe("BlobValue.vue", () => {
expect(wrapper.find("a").attributes("href")).toBe(BLOB_URL)

const encodedUrl = btoa(BLOB_URL)
const resultingUrl = (new URL(BLOB_URL)).toString()

await wrapper.setProps({
blobValue: encodedUrl,
base64: true
})

expect(wrapper.find("a").text()).toBe(resultingUrl)
expect(wrapper.find("a").attributes("href")).toBe(resultingUrl)
expect(wrapper.find("a").text()).toBe(BLOB_URL)
expect(wrapper.find("a").attributes("href")).toBe(BLOB_URL)

wrapper.unmount()
await flushPromises()
Expand All @@ -266,7 +265,7 @@ describe("BlobValue.vue", () => {
const wrapper = mount(BlobValue, {
global: {
plugins: [router],
provide: { [coreConfigKey]: CoreConfig.FALLBACK }
provide: {[coreConfigKey]: CoreConfig.FALLBACK}
},
props: {
blobValue: encodedUrl,
Expand Down

0 comments on commit a233864

Please sign in to comment.