-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Tokens tab * Fix code smells * Custom dipdup * Add thumbnail * Loading thumbnail * Deduplication code * Deduplication code - 2
1 parent
2b267a0
commit ba7d6ac
Showing
9 changed files
with
292 additions
and
553 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
NODE_ENV=development | ||
VUE_APP_API_URI=https://sharm.better-call.dev/v1 | ||
VUE_APP_API_URI=https://sharm.better-call.dev/v1 | ||
IPFS_NODE=https://ipfs.io |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
const axios = require('axios').default; | ||
|
||
export class RequestFailedError extends Error { } | ||
|
||
function createAxios(baseURL, timeout = 10000) { | ||
return axios.create({ | ||
baseURL: baseURL, | ||
timeout: timeout, | ||
responseType: 'json' | ||
}) | ||
} | ||
|
||
function get(api, query) { | ||
return api.post('/v1/graphql', query) | ||
.then(res => { | ||
if (res.status !== 200 || res.data.errors) { | ||
throw new RequestFailedError(JSON.stringify(res)); | ||
} | ||
return res.data.data; | ||
}) | ||
} | ||
|
||
function buildQuery(query, address, network, limit, offset) { | ||
return { | ||
query: query, | ||
variables: { | ||
address: address, | ||
network: network, | ||
limit: limit, | ||
offset: offset | ||
}, | ||
operationName: "GetTokenMetadata" | ||
} | ||
} | ||
|
||
export class TokenMetadataApi { | ||
constructor(baseURL) { | ||
this.api = createAxios(baseURL); | ||
} | ||
|
||
get(network, address, limit=10, offset=0) { | ||
let query = buildQuery(`query GetTokenMetadata($address: String, $network: String, $limit: Int, $offset: Int) { | ||
token_metadata( | ||
where: { | ||
contract: {_eq: $address}, | ||
network: {_eq: $network}} | ||
limit: $limit | ||
offset: $offset | ||
order_by: {update_id: desc} | ||
) { | ||
network | ||
contract | ||
error | ||
link | ||
metadata | ||
token_id | ||
} | ||
}`, address, network, limit, offset) | ||
|
||
return get(this.api, query) | ||
.then(res => { | ||
return res.token_metadata; | ||
}) | ||
} | ||
} | ||
|
||
export class DipDupTokenMetadataApi { | ||
constructor(baseURL) { | ||
this.api = createAxios(baseURL); | ||
} | ||
|
||
get(network, address, limit=10, offset=0) { | ||
let query = buildQuery(`query GetTokenMetadata($address: String, $network: String, $limit: Int, $offset: Int) { | ||
dipdup_token_metadata( | ||
where: { | ||
contract: {_eq: $address}, | ||
network: {_eq: $network}} | ||
limit: $limit | ||
offset: $offset | ||
order_by: {update_id: desc} | ||
) { | ||
network | ||
contract | ||
metadata | ||
token_id | ||
} | ||
}`, address, network, limit, offset) | ||
|
||
return get(this.api, query) | ||
.then(res => { | ||
return res.dipdup_token_metadata; | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,57 +1,210 @@ | ||
<template> | ||
<v-container class="canvas fill-canvas pa-8 ma-0" fluid> | ||
<v-row no-gutters> | ||
<v-col cols="8" class="pa-2"> | ||
<div | ||
v-if="selectedToken" | ||
> | ||
<TokenMetadata :token="selectedToken"/> | ||
<TokenHolders class="mt-3" :token="selectedToken" /> | ||
</div> | ||
<v-skeleton-loader | ||
v-else | ||
:loading="typeof selectedToken !== 'number'" | ||
type="image" | ||
> | ||
</v-skeleton-loader> | ||
</v-col> | ||
<v-col cols="4" class="pa-2"> | ||
<TokensList | ||
:network="network" | ||
:address="address" | ||
:tokensTotal="tokensTotal" | ||
@selectedToken="updateSelectedToken" | ||
/> | ||
</v-col> | ||
</v-row> | ||
<EmptyState | ||
v-if="tokens.length === 0 && !loading" | ||
icon="mdi-code-brackets" | ||
title="Nothing found" | ||
/> | ||
<v-expansion-panels v-show="tokens.length > 0" multiple hover flat class="bb-1"> | ||
<v-expansion-panel v-for="(token) in tokens" | ||
:key="token.token_id" | ||
class="bl-1 br-1 bt-1 token-panel" | ||
active-class="token-active-panel"> | ||
<v-expansion-panel-header class="py-0 px-4" :class="token.metadata === null && token.error !== null ? 'item-header-failed' : 'item-header-applied' " ripple :title="title(token)"> | ||
<template> | ||
<v-list-item class="fill-height pa-0"> | ||
<v-list-item-content> | ||
<v-list-item-title>{{ title(token) }}</v-list-item-title> | ||
<v-list-item-subtitle class="font-weight-light hash text--secondary"> | ||
Token ID: {{ token.token_id }} | ||
</v-list-item-subtitle> | ||
</v-list-item-content> | ||
</v-list-item> | ||
</template> | ||
</v-expansion-panel-header> | ||
<v-expansion-panel-content class="token-content py-4"> | ||
<v-list-item class="pl-0" v-if="token.link"> | ||
<v-list-item-content> | ||
<v-list-item-subtitle class="overline">Token metadata link</v-list-item-subtitle> | ||
<v-list-item-title class="d-flex align-center"> | ||
<span>{{ token.link }}</span> | ||
<v-tooltip bottom> | ||
<template v-slot:activator="{ on }"> | ||
<v-btn | ||
small | ||
v-on="on" | ||
icon | ||
class="mr-2" | ||
@click=" | ||
() => { | ||
$clipboard(token.link); | ||
showClipboardOK(); | ||
} | ||
" | ||
> | ||
<v-icon small class="text--secondary">mdi-content-copy</v-icon> | ||
</v-btn> | ||
</template> | ||
Copy token link | ||
</v-tooltip> | ||
</v-list-item-title> | ||
</v-list-item-content> | ||
</v-list-item> | ||
<v-alert v-if="token.error && token.metadata === null" type="error" prominent text class="ma-0 align-center"> | ||
<div class="overline">Resolving token metadata error</div> | ||
<div class="text--primary"> {{ token.error }}</div> | ||
</v-alert> | ||
<v-row v-if="token.metadata"> | ||
<v-col :cols="token.metadata.thumbnailUri ? 10 : 12"> | ||
<vue-json-pretty | ||
|
||
class="raw-json-viewer py-3" | ||
:data="token.metadata" | ||
:highlightMouseoverNode="true" | ||
:customValueFormatter="formatValue" | ||
></vue-json-pretty> | ||
</v-col> | ||
<v-col cols="2" v-if="token.metadata.thumbnailUri" class="d-flex flex-column align-center justify-start"> | ||
<span class="overline mt-10" v-if="!token.loaded">Loading thumbnail...</span> | ||
<div class="d-flex flex-column align-center justify-start"> | ||
<v-img @load="loadedImage(token)" :src="getIPFS(token.metadata.thumbnailUri)" sizes="200" alt="Thumbnail" contain eager max-height="200"> | ||
</v-img> | ||
<div class="mt-4" v-if="token.loaded"> | ||
<p class="overline">Image from thumbnail URL</p> | ||
</div> | ||
</div> | ||
</v-col> | ||
</v-row> | ||
</v-expansion-panel-content> | ||
</v-expansion-panel> | ||
</v-expansion-panels> | ||
<v-skeleton-loader | ||
v-show="loading" | ||
:loading="loading" | ||
type="list-item-two-line, list-item-two-line, list-item-two-line" | ||
> | ||
</v-skeleton-loader> | ||
<span | ||
v-intersect="onDownloadPage" | ||
v-if="!loading && !downloaded" | ||
></span> | ||
</v-container> | ||
</template> | ||
|
||
<script> | ||
import TokenMetadata from "@/views/contract/TokensTab/TokenMetadata"; | ||
import TokenHolders from "@/views/contract/TokensTab/TokenHolders"; | ||
import TokensList from "@/views/contract/TokensTab/TokensList"; | ||
import { mapActions } from "vuex"; | ||
import EmptyState from "@/components/Cards/EmptyState.vue"; | ||
import VueJsonPretty from "vue-json-pretty"; | ||
import { DipDupTokenMetadataApi } from "@/api/token_metadata.js"; | ||
export default { | ||
name: "ContractTokensTab", | ||
props: { | ||
network: String, | ||
address: String, | ||
tokensTotal: Number | ||
address: String | ||
}, | ||
components: { | ||
TokensList, | ||
TokenMetadata, | ||
TokenHolders | ||
EmptyState, | ||
VueJsonPretty | ||
}, | ||
data: () => ({ | ||
holders: {}, | ||
selectedToken: null | ||
tokens: [], | ||
loading: false, | ||
downloaded: false, | ||
ubisoft: {}, | ||
domains: {}, | ||
}), | ||
created() { | ||
this.ubisoft = new DipDupTokenMetadataApi('https://quartz.dipdup.net'); | ||
this.domains = new DipDupTokenMetadataApi('https://domains.dipdup.net/'); | ||
}, | ||
methods: { | ||
async updateSelectedToken(newVal) { | ||
this.selectedToken = newVal; | ||
...mapActions(["showError", "hideError"]), | ||
title(token) { | ||
if (token.metadata !== null) { | ||
return token.metadata.name; | ||
} | ||
return `${token.token_id}`; | ||
}, | ||
getIPFS(url) { | ||
if (!url) { | ||
return ''; | ||
} | ||
return `${this.config.IPFS_NODE}/ipfs/${url.replace('ipfs://', '')}` | ||
}, | ||
loadedImage(token) { | ||
token.loaded = true; | ||
}, | ||
async onDownloadPage(_entries, _observer, isIntersecting) { | ||
if (isIntersecting) { | ||
await this.getTokens(); | ||
} | ||
}, | ||
async getTokens() { | ||
if (this.loading || this.downloaded) return; | ||
this.loading = true; | ||
let api = this.tokenMetadata; | ||
if (this.address === 'KT1TnVQhjxeNvLutGvzwZvYtC7vKRpwPWhc6') { | ||
api = this.ubisoft; | ||
} else if (this.address === 'KT1GBZmSxmnKJXGMdMLbugPfLyUPmuLSMwKS') { | ||
api = this.domains; | ||
} | ||
await api.get( this.network, this.address, 20, this.tokens.length) | ||
.then(res => { | ||
if (!res) { | ||
this.downloaded = true; // prevent endless polling | ||
} else { | ||
this.downloaded = res.length === 0; | ||
res.forEach(x => x.loaded = false); | ||
this.tokens.push(...res); | ||
} | ||
}) | ||
.catch((err) => { | ||
this.showError(err); | ||
this.downloaded = true; | ||
}) | ||
.finally(() => { | ||
this.loading = false; | ||
}); | ||
}, | ||
formatValue(_data, _key, _path, defaultFormatResult) { | ||
return defaultFormatResult | ||
.replace(/&/g, "&") | ||
.replace(/</g, "<") | ||
.replace(/>/g, ">") | ||
.replace(/"/g, """) | ||
.replace(/'/g, "'"); | ||
}, | ||
} | ||
}, | ||
watch: { | ||
address: "getTokens", | ||
}, | ||
}; | ||
</script> | ||
|
||
<style scoped> | ||
.token-content > .v-expansion-panel-content__wrap { | ||
padding: 0; | ||
} | ||
.token-panel { | ||
background-color: var(--v-data-base) !important; | ||
} | ||
.token-active-panel > .v-expansion-panel-header { | ||
opacity: 0.8; | ||
background-color: var(--v-border-base) !important; | ||
} | ||
.token-active-panel, | ||
.token-panel.v-expansion-panel--next-active { | ||
border-bottom: 1px solid var(--v-border-base); | ||
} | ||
</style> | ||
|
||
<style lang="scss"> | ||
@import '../../../styles/vue-json-pretty.css'; | ||
</style> |