chainInfo from server, routes, wallet rework
This commit is contained in:
13
src/components/Alerts.vue
Normal file
13
src/components/Alerts.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<v-alert v-for="e in s.errors" icon="mdi-alert" color="error" :title="e.title" :text="e.text" class="mb-3" :closable="e.closeable"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
const s = useStore()
|
||||
console.log('errors', s.errors)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
</style>
|
||||
24
src/components/ConnectWallet.vue
Normal file
24
src/components/ConnectWallet.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<v-btn prepend-icon="mdi-lightbulb-on" text="Connect Wallet" @click="connectWallet" :disabled="disabled"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {ref} from "vue";
|
||||
import {provider} from "@/blockchain/wallet.js";
|
||||
|
||||
const s = useStore()
|
||||
const disabled = ref(false)
|
||||
|
||||
async function connectWallet() {
|
||||
disabled.value = true
|
||||
await provider.getSigner()
|
||||
disabled.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
18
src/components/NeedsQueryHelper.vue
Normal file
18
src/components/NeedsQueryHelper.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<slot v-if="s.helper"/>
|
||||
<v-card v-if="!s.helper" prepend-icon='mdi-reload-alert' title="Change Blockchain" text="Dexorder works only with Arbitrum. Please choose the Arbitrum blockchain in your wallet."/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
async function connect() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
15
src/components/NeedsWallet.vue
Normal file
15
src/components/NeedsWallet.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<token-choice v-model="modelValue.tokenA" class="token-choice mb-1">
|
||||
<token-choice v-model="tokenA" class="token-choice mb-1">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="modelValue.buy ? 'Buy' : 'Sell'" :color="modelValue.buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="modelValue.buy=!modelValue.buy" class="bs-button"/>
|
||||
<v-btn :text="buy ? 'Buy' : 'Sell'" :color="buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="buy=!buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
<token-choice v-model="modelValue.tokenB" class="token-choice">
|
||||
<token-choice v-model="tokenB" class="token-choice">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="!modelValue.buy ? 'Buy' : 'Sell'" :color="!modelValue.buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="modelValue.buy=!modelValue.buy" class="bs-button"/>
|
||||
<v-btn :text="!buy ? 'Buy' : 'Sell'" :color="!buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="buy=!buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
</template>
|
||||
@@ -16,15 +16,52 @@
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import TokenChoice from "@/components/TokenChoice.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
// {
|
||||
// tokenA, tokenB, buy
|
||||
// }
|
||||
|
||||
// { tokenA, tokenB, buy }
|
||||
// tokens may be undefined if not selected
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
console.log('PairEntry setup', props.modelValue)
|
||||
|
||||
if((!props.modelValue || !props.modelValue.tokenA) && s.tokens && s.tokens.length >= 1)
|
||||
set('tokenA', s.tokens[0])
|
||||
if((!props.modelValue || !props.modelValue.tokenB) && s.tokens && s.tokens.length >= 2)
|
||||
set('tokenB', s.tokens[1])
|
||||
|
||||
function set(k,v) {
|
||||
const newModel = {}
|
||||
if(props.modelValue)
|
||||
Object.assign(newModel, props.modelValue);
|
||||
newModel[k] = v
|
||||
console.log('set', k, v, props.modelValue, newModel)
|
||||
emit('update:modelValue', newModel)
|
||||
}
|
||||
|
||||
function tokenComputer(attr, index) {
|
||||
return {
|
||||
get() {
|
||||
return !props.modelValue || !props.modelValue[attr] ? null : props.modelValue[attr]
|
||||
},
|
||||
set(value) {
|
||||
set(attr,value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tokenA = computed(tokenComputer('tokenA', 0))
|
||||
const tokenB = computed(tokenComputer('tokenB', 1))
|
||||
const buy = computed({
|
||||
get() {
|
||||
!props.modelValue ? true : props.modelValue.buy
|
||||
},
|
||||
set(value) {
|
||||
set('buy',value)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
19
src/components/PhoneCard.vue
Normal file
19
src/components/PhoneCard.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<v-card class="d-none d-md-block" :elevation="4">
|
||||
<slot/>
|
||||
</v-card>
|
||||
<v-container class="d-md-none">
|
||||
<slot/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
</style>
|
||||
@@ -1,80 +1,124 @@
|
||||
<template>
|
||||
<v-card title="DCA / TWAP" subtitle="Split order across time" class="order-card" elevation="4">
|
||||
<v-card-text>
|
||||
<pair-entry v-model="pair"/>
|
||||
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
|
||||
:model-value="amount" :rules="[validateRequired,validateAmount]" class="amount">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn @click="amountIsBase=!amountIsBase" variant="outlined" class="mr-2">
|
||||
{{ amountIsBase ? pair.tokenA.symbol : pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
<v-btn :text="amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
|
||||
@click="amountIsTotal=!amountIsTotal" class="total"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1"
|
||||
:model-value="tranches" :rules="[validateRequired]">
|
||||
<template v-slot:prepend-inner>
|
||||
<v-btn class="split-into mr-2" variant="outlined" @click="amountIsTotal=!amountIsTotal">
|
||||
{{ amountIsTotal ? 'Split into' : 'Times' }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:append-inner>tranches</template>
|
||||
</v-text-field>
|
||||
<v-text-field type="number" variant="outlined" :min="1" v-model="interval" class="interval"
|
||||
:label="intervalIsTotal ? 'Completion time' : 'Time between tranches'">
|
||||
<!-- <template v-slot:append>APART</template>-->
|
||||
<template v-slot:prepend-inner>
|
||||
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within mr-2"
|
||||
@click="intervalIsTotal=!intervalIsTotal"/>
|
||||
</template>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" :text="timeUnits[timeUnitIndex]" @click="toggleTimeUnits" class="time-units"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field v-model="limitPrice" :label="(limitIsMinimum?'Minimum':'Maximum')+' Price'" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMin]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<!--
|
||||
<v-text-field v-model="minPrice" label="Minimum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMin]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field v-model="maxPrice" label="Maximum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMax]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
-->
|
||||
<NeedsQueryHelper>
|
||||
<PhoneCard>
|
||||
<v-card-title class="big">DCA / TWAP</v-card-title>
|
||||
<v-card-subtitle>Split order across time</v-card-subtitle>
|
||||
<v-card-text>
|
||||
|
||||
</v-card-text>
|
||||
<pair-entry v-model="pair"/>
|
||||
|
||||
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
||||
<v-btn variant="outlined" color="red">Cancel</v-btn>
|
||||
<v-btn variant="flat" color="green">Place Order</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-chip v-for="r in routes" variant="text">
|
||||
{{ s.chain.name }}
|
||||
<v-img src="https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg" width="1.5em"/>
|
||||
<span class="uniswap-pink ml-0 mr-1">v3</span>
|
||||
{{pairSymbol}} {{r.fee/10000}}%
|
||||
</v-chip>
|
||||
|
||||
<div v-if="routeFinder.pending()">
|
||||
<v-progress-circular indeterminate/> Searching for {{pairSymbol}} pools...
|
||||
</div>
|
||||
|
||||
<v-alert v-if="routes.length===0 && !routeFinder.pending()" text="No pool found!" type=""/>
|
||||
|
||||
<div v-if="routes.length">
|
||||
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
|
||||
:model-value="amount" :rules="[validateRequired,validateAmount]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn @click="amountIsBase=!amountIsBase" variant="outlined" class="mr-2">
|
||||
{{ amountIsBase ? pair.tokenA.symbol : pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
<v-btn :text="amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
|
||||
@click="amountIsTotal=!amountIsTotal" class="total"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255"
|
||||
:model-value="tranches" :rules="[validateRequired,validateTranches]">
|
||||
<!-- <template v-slot:prepend-inner>-->
|
||||
<!-- <div>{{ amountIsTotal ? 'Split into' : 'Times' }}</div>-->
|
||||
<!-- </template>-->
|
||||
<template v-slot:append-inner>tranches</template>
|
||||
</v-text-field>
|
||||
<v-text-field type="number" variant="outlined" :min="1" v-model="interval" class="interval"
|
||||
:label="intervalIsTotal ? 'Completion time' : 'Time between tranches'">
|
||||
<!-- <template v-slot:append>APART</template>-->
|
||||
<template v-slot:prepend-inner>
|
||||
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within mr-2"
|
||||
@click="intervalIsTotal=!intervalIsTotal"/>
|
||||
</template>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" :text="timeUnits[timeUnitIndex]" @click="toggleTimeUnits" class="time-units"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field v-model="limitPrice" :label="(limitIsMinimum?'Minimum':'Maximum')+' Price'" type="number"
|
||||
variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMin]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{
|
||||
inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol
|
||||
}}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<!--
|
||||
<v-text-field v-model="minPrice" label="Minimum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMin]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field v-model="maxPrice" label="Maximum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMax]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
-->
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
||||
<v-btn variant="outlined" color="red">Cancel</v-btn>
|
||||
<v-btn variant="flat" color="green" :disabled="!routes.length">Place Order</v-btn>
|
||||
</v-card-actions>
|
||||
</PhoneCard>
|
||||
</NeedsQueryHelper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {computed, ref} from "vue";
|
||||
import PairEntry from "@/components/PairEntry.vue";
|
||||
import PhoneCard from "@/components/PhoneCard.vue";
|
||||
import {queryHelperContract} from "@/blockchain/contract.js";
|
||||
import {SingletonCoroutine} from "@/misc.js";
|
||||
import NeedsQueryHelper from "@/components/NeedsQueryHelper.vue";
|
||||
|
||||
const s = useStore()
|
||||
const pair = ref({tokenA: s.tokens[0], tokenB: s.tokens[1], buy: true})
|
||||
// pair is computed to allow for dynamic computation of a default value based on current chainId
|
||||
const _pair = ref({buy:true})
|
||||
const pair = computed({
|
||||
get() {
|
||||
console.log('te getpair',_pair.value)
|
||||
return _pair.value
|
||||
},
|
||||
set(value) {
|
||||
console.log('set pair',value)
|
||||
_pair.value = value
|
||||
routes.value = []
|
||||
routeFinder.invoke()
|
||||
}
|
||||
})
|
||||
const pairSelected = computed(()=>_pair.value.tokenA && _pair.value.tokenB && routes.value)
|
||||
const base = computed(()=>!pairSelected?{}:_pair.value.tokenA)
|
||||
const quote = computed(()=>!pairSelected?{}:_pair.value.tokenB)
|
||||
const pairSymbol = computed(()=>base.value?.symbol+'/'+quote.value?.symbol)
|
||||
const routes = ref([])
|
||||
const amount = ref(1)
|
||||
const amountIsBase = ref(false)
|
||||
const amountIsTotal = ref(true)
|
||||
@@ -88,12 +132,46 @@ const intervalIsTotal = ref(true)
|
||||
const timeUnits = ['minutes', 'hours', 'days']
|
||||
const timeUnitIndex = ref(1)
|
||||
|
||||
const limitIsMinimum = computed(()=>!(pair.value.buy ^ inverted.value))
|
||||
const limitIsMinimum = computed(() => !(pair.value.buy ^ inverted.value))
|
||||
|
||||
async function findRoute() {
|
||||
if( !pair.value || !pair.value.tokenA || !pair.value.tokenB )
|
||||
return null
|
||||
const helper = await queryHelperContract()
|
||||
console.log('helper',helper)
|
||||
let rawRoutes
|
||||
try {
|
||||
rawRoutes = await helper.getRoutes(pair.value.tokenA.address, pair.value.tokenB.address)
|
||||
}
|
||||
catch (e) {
|
||||
routes.value = []
|
||||
console.log('routes exception', e)
|
||||
return
|
||||
}
|
||||
// todo expose all available pools
|
||||
let result = {} // we actually only find a single pool for now
|
||||
for (let [exchange, fee, pool] of rawRoutes) {
|
||||
exchange = Number(exchange)
|
||||
fee = Number(fee)
|
||||
if (result.fee === undefined || result.fee > fee) {
|
||||
switch (exchange) {
|
||||
case 0: // UniswapV2
|
||||
break
|
||||
case 1: // UniswapV3
|
||||
result = {exchange: 'UniswapV3', pool, fee,}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
routes.value = [result]
|
||||
}
|
||||
|
||||
const routeFinder = new SingletonCoroutine(findRoute,50)
|
||||
|
||||
|
||||
function toggleTimeUnits() {
|
||||
timeUnitIndex.value++
|
||||
if( timeUnitIndex.value >= timeUnits.length )
|
||||
if (timeUnitIndex.value >= timeUnits.length)
|
||||
timeUnitIndex.value = 0
|
||||
}
|
||||
|
||||
@@ -104,14 +182,26 @@ function isEmpty(v) {
|
||||
|
||||
|
||||
function validateRequired(v) {
|
||||
if( isEmpty(v) )
|
||||
if (isEmpty(v))
|
||||
return 'Required'
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
function validateTranches(v) {
|
||||
const i = parseInt(v)
|
||||
if (parseFloat(v) !== i)
|
||||
return 'Whole numbers only'
|
||||
if (i < 1)
|
||||
return 'Must have at least one tranche'
|
||||
if (i > 255)
|
||||
return 'Maximum 255 tranches'
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
function validateAmount(v) {
|
||||
if( isEmpty(v) )
|
||||
if (isEmpty(v))
|
||||
return true
|
||||
const floatRegex = /^-?\d*(?:[.,]\d*?)?$/
|
||||
if (!floatRegex.test(v))
|
||||
@@ -122,14 +212,14 @@ function validateAmount(v) {
|
||||
}
|
||||
|
||||
function validateMax(v) {
|
||||
if( !isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value) )
|
||||
return 'Must be greater than the minimum price'
|
||||
if (!isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value))
|
||||
return 'Must be greater than the minimum price'
|
||||
return true
|
||||
}
|
||||
|
||||
function validateMin(v) {
|
||||
if( !isEmpty(maxPrice.value) && !isEmpty(v) && parseFloat(v) > parseFloat(maxPrice.value) )
|
||||
return 'Must be less than the maximum price'
|
||||
if (!isEmpty(maxPrice.value) && !isEmpty(v) && parseFloat(v) > parseFloat(maxPrice.value))
|
||||
return 'Must be less than the maximum price'
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -139,10 +229,6 @@ function validateMin(v) {
|
||||
<style scoped lang="scss">
|
||||
@use "@/styles/vars" as *;
|
||||
|
||||
.order-card {
|
||||
width: 25em;
|
||||
}
|
||||
|
||||
.amount {
|
||||
width: 23em;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:label="label"
|
||||
v-bind="modelValue" @update:modelValue="updateValue"
|
||||
variant="outlined"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
>
|
||||
<template v-slot:prepend><slot name="prepend"/></template>
|
||||
<template v-slot:item="{props,item}">
|
||||
@@ -76,8 +77,6 @@ function updateValue(v) {
|
||||
|
||||
<script>
|
||||
import {ethers} from "ethers";
|
||||
import wallet from "@/blockchain/wallet.js";
|
||||
import {erc20Abi} from "@/blockchain/abi.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
import {socket} from "@/socket.js";
|
||||
|
||||
@@ -85,14 +84,14 @@ const s = useStore()
|
||||
|
||||
async function addExtraToken(addr) {
|
||||
const prom = new Promise((resolve)=>{
|
||||
socket.emit('lookupToken', s.chain.id, addr, (info) => {
|
||||
socket.emit('lookupToken', s.chainId, addr, (info) => {
|
||||
if( info === null )
|
||||
return resolve(null)
|
||||
s.$patch((state)=>{
|
||||
let extras = state.extraTokens[state.chain.id]
|
||||
let extras = state.extraTokens[state.chainId]
|
||||
if( extras === undefined ) {
|
||||
extras = {}
|
||||
state.extraTokens[state.chain.id] = extras
|
||||
state.extraTokens[state.chainId] = extras
|
||||
}
|
||||
extras[info.address] = info
|
||||
})
|
||||
@@ -106,5 +105,4 @@ async function addExtraToken(addr) {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store.js";
|
||||
import wallet from "@/blockchain/wallet.js";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user