chainInfo from server, routes, wallet rework

This commit is contained in:
Tim Olson
2023-10-04 03:40:47 -04:00
parent e7a9600c54
commit 7239987867
26 changed files with 603 additions and 297 deletions

View File

@@ -11,7 +11,6 @@
<body>
<div id="app" class="app"></div>
<script src="/version.js"></script>
<script src="/generated.js"></script>
<script type="module" src="/src/main.js"></script>
</body>

View File

@@ -1,91 +0,0 @@
known_chains = [
{
name: 'Arbitrum One',
id: 42161,
icon: null,
}
]
_known_tokens = {
42161: [ // Arbitrum
{
name: 'Wrapped Ether',
symbol: 'WETH',
decimals: 18,
icon: null,
address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
},
{
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
icon: null,
address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
},
{
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
icon: null,
address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
},
{
name: 'Wrapped Bitcoin',
symbol: 'WBTC',
decimals: 8,
icon: null,
address: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
},
{
name: 'DAI Stablecoin',
symbol: 'DAI',
decimals: 18,
icon: null,
address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
},
{
name: 'Uniswap',
symbol: 'UNI',
decimals: 18,
icon: null,
address: '0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0',
},
{
name: 'Chainlink Token',
symbol: 'LINK',
decimals: 18,
icon: null,
address: '0xf97f4df75117a78c1A5a0DBb814Af92458539FB4',
},
{
name: 'TrueUSD',
symbol: 'TUSD',
decimals: 18,
icon: null,
address: '0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07',
},
{
name: 'Lido DAO Token',
symbol: 'LDO',
decimals: 18,
icon: null,
address: '0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60',
},
{
name: 'Arbitrum',
symbol: 'ARB',
decimals: 18,
icon: null,
address: '0x912CE59144191C1204E64559FE8253a0e49E6548',
},
]
};
known_tokens = {}
for( const chainId in _known_tokens ) {
known_tokens[chainId] = {}
for( const info of _known_tokens[chainId] )
known_tokens[chainId][info.address] = info
}

View File

@@ -1,3 +1,34 @@
export const factoryAbi = [
]
export const queryHelperAbi = [
'function getRoutes(address tokenA,address tokenB) view returns((uint8,uint24,address)[])',
]
export const poolAbi = [
// {
// // the current price
// uint160 sqrtPriceX96;
// // the current tick
// int24 tick;
// // the most-recently updated index of the observations array
// uint16 observationIndex;
// // the current maximum number of observations that are being stored
// uint16 observationCardinality;
// // the next maximum number of observations to store, triggered in observations.write
// uint16 observationCardinalityNext;
// // the current protocol fee as a percentage of the swap fee taken on withdrawal
// // represented as an integer denominator (1/x)%
// uint8 feeProtocol;
// // whether the pool is locked
// bool unlocked;
// }
'function slot0() view returns(uint160,int24,uint16,uint16,uint16,uint8,bool)',
'function token0() view returns(address)',
'function token1() view returns(address)',
]
export const erc20Abi = [
'function name() view returns (string)',
'function symbol() view returns (string)',
@@ -38,5 +69,6 @@ export const timedOrderAbi = [
export const abi = {
'ERC20': erc20Abi,
'TimedOrder': timedOrderAbi,
'QueryHelper': queryHelperAbi,
}

View File

@@ -0,0 +1,20 @@
import {ethers} from "ethers";
import {factoryAbi, queryHelperAbi} from "@/blockchain/abi.js";
import {useStore} from "@/store/store.js";
import {provider} from "@/blockchain/wallet.js";
export async function factoryContract() {
const s = useStore()
return new ethers.Contract(s.helper, factoryAbi, provider)
}
export async function queryHelperContract() {
const s = useStore()
return new ethers.Contract(s.helper, queryHelperAbi, provider)
}
export async function poolContract(addr) {
const s = useStore()
return new ethers.Contract(addr, poolAbi, provider)
}

View File

@@ -1,86 +1,36 @@
import {ethers} from "ethers";
import {useStore} from "@/store/store";
const store = useStore()
export let provider = null
function onChainChanged(chainId) {
chainId = Number(chainId)
console.log('chain changed', chainId)
const store = useStore()
store.chainId = chainId
provider = new ethers.BrowserProvider(window.ethereum, chainId)
}
class Wallet {
connected = false
_provider = null
_signer = null
_onSignerInits = {}
_enabled = true // prevents tight loops on errors
async connect() {
this._enabled = true
if( !this.connected )
await this.signer()
}
async provider() {
if (this._provider === null && this._enabled) {
console.log('creating provider')
const provider = new ethers.BrowserProvider(window.ethereum, store.chain.id)
await provider.getNetwork() // this invokes a check on having the correct network connected
this._provider = provider
console.log('wallet connected')
}
return this._provider
}
async signer() {
/*
if( !store.geo.approved ) {
console.log('not approved')
this._connected(false)
return null
}
*/
const provider = await this.provider()
if( provider === null ) {
console.log('provider null')
this._connected(false)
return null
}
const signer = await provider.getSigner()
if( this._signer?.address !== signer.address ) {
console.log('new signer', signer.address)
this._signer = signer
for (const key in this._onSignerInits) {
try {
await this._onSignerInits[key](signer)
}
catch (e) {
console.log('during onSignerInit', e)
}
}
console.log('wallet connected')
}
this._connected(true)
return signer
}
async address() {
const signer = await this.signer()
if( signer === null )
return null
return await signer.getAddress()
}
_connected(value) {
this.connected = value
store.$patch({wallet:{connected:value}})
}
onSignerInit(id, cb) {
this._onSignerInits[id] = cb
}
function onAccountsChanged(accounts) {
console.log('accounts changed', accounts)
const store = useStore()
if( accounts.length === 0 ) {
store.account = null
}
else if (accounts[0] !== store.account) {
store.account = accounts[0]
}
}
export async function watchWallet() {
const chainId = (await new ethers.BrowserProvider(window.ethereum).getNetwork()).chainId
onChainChanged(chainId)
window.ethereum.on('chainChanged', onChainChanged);
window.ethereum.on('accountsChanged', onAccountsChanged);
}
const handler = {
const errorHandlingProxy = {
get(target, prop, proxy) {
const got = Reflect.get(target, prop, proxy);
if( typeof got !== 'function' ) {
@@ -95,8 +45,11 @@ const handler = {
target._connected(false)
target._enabled = false
if( x.code === 'NETWORK_ERROR' ) {
store.error('Wrong Blockchain', 'Your wallet is connected to a different blockchain. Please select '+import.meta.env.VITE_CHAIN_NAME+' in your wallet.')
console.log('wallet network error', x)
// todo available chain names
// store.error('Wrong Blockchain', 'Your wallet is connected to a different blockchain. Please select Arbitrum in your wallet.') // todo hardcoded arb
console.log('wallet chain error', x)
// store.chainId = store.chainInfo[]
throw x
}
else {
console.log('wallet error')
@@ -109,4 +62,4 @@ const handler = {
}
export default new Proxy(new Wallet(), handler)
// const wallet = new Proxy(new Wallet(), errorHandlingProxy);

13
src/components/Alerts.vue Normal file
View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -1,9 +1,28 @@
<template>
<v-card title="DCA / TWAP" subtitle="Split order across time" class="order-card" elevation="4">
<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>
<pair-entry v-model="pair"/>
<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]" class="amount">
: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 }}
@@ -12,18 +31,16 @@
@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>
<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:append>APART</template>-->
<template v-slot:prepend-inner>
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within mr-2"
@click="intervalIsTotal=!intervalIsTotal"/>
@@ -32,15 +49,18 @@
<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"
<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 }}
{{
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>
@@ -57,24 +77,48 @@
</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">Place Order</v-btn>
<v-btn variant="flat" color="green" :disabled="!routes.length">Place Order</v-btn>
</v-card-actions>
</v-card>
</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,13 +212,13 @@ function validateAmount(v) {
}
function validateMax(v) {
if( !isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value) )
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) )
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;
}

View File

@@ -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>

View File

@@ -24,7 +24,6 @@
<script setup>
import {useStore} from "@/store/store.js";
import wallet from "@/blockchain/wallet.js";
const s = useStore()

View File

@@ -1,8 +1,7 @@
<template>
<v-app-bar flat>
<v-app-bar-title class="sc">
<v-icon icon="mdi-arrow-up-bold" size="x-small" class="arrow" color="green"/>
<span class="logo">dexorder</span>
<v-app-bar-title class="">
<span class="logo"><v-icon icon="mdi-arrow-up-bold" size="x-small" class="arrow" color="green"/>dexorder</span>
<v-chip text="ALPHA" size="x-small" color="red" class="mx-1"/>
</v-app-bar-title>
</v-app-bar>
@@ -14,10 +13,16 @@
<style lang="scss">
@use "src/styles/vars" as v;
.arrow{
.arrow {
color: v.$green;
vertical-align: text-top;
}
.logo {
font-family: v.$heading-font-family;
font-weight: 500;
font-size: 32px;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<v-app>
<default-bar />
<default-view />
<default-view/>
</v-app>
</template>

View File

@@ -1,10 +1,26 @@
<template>
<v-main>
<v-container>
<router-view />
<div class="maxw">
<Alerts/>
<router-view/>
<v-skeleton-loader v-if="!store.chainInfo" type="card" class="order-card"/>
</div>
</v-container>
</v-main>
</template>
<script setup>
import Alerts from "@/components/Alerts.vue";
import {VSkeletonLoader} from "vuetify/labs/VSkeletonLoader";
import {useStore} from "@/store/store.js";
const store = useStore()
</script>
<style scoped lang="scss">
@use 'src/styles/vars' as *;
.maxw {
max-width: $card-maxw;
}
</style>

View File

@@ -13,14 +13,11 @@ import { createApp } from 'vue'
// Plugins
import { registerPlugins } from '@/plugins'
import '@/styles/style.scss'
import {useStore} from "@/store/store.js";
import {watchWallet} from "@/blockchain/wallet.js";
import "./socket.js"
const app = createApp(App)
registerPlugins(app)
const s = useStore()
s.chains = known_chains
s.chain = known_chains[0]
app.mount('#app')
await watchWallet()

35
src/misc.js Normal file
View File

@@ -0,0 +1,35 @@
export class SingletonCoroutine {
constructor(f, delay=10, retry=true) {
this.f = f
this.delay = delay
this.timeout = null
this.args = null
}
pending() {
return this.timeout !== null
}
invoke(/*arguments*/) {
this.args = arguments
if( this.timeout !== null )
clearTimeout(this.timeout)
this.timeout = setTimeout(this.onTimeout, this.delay, this)
}
async onTimeout(self) {
self.timeout = null
console.log('invoking', self.f, self.args)
try {
await self.f(...self.args)
}
catch (e) {
if( self.retry )
self.invoke(self.args)
else
console.error(e)
}
}
}

View File

@@ -5,8 +5,8 @@
*/
// todo choose
const displayFonts = ['Tektur', 'Turret Road', 'Chakra Petch', 'Orbitron', 'Quantico', 'Geo']
const bodyFonts = ['Victor Mono', 'Nunito', 'Manrope', 'Sofia Sans']
const displayFonts = ['Orbitron', 'Tektur', 'Turret Road', 'Chakra Petch', 'Quantico', 'Geo']
const bodyFonts = ['Exo 2', 'Victor Mono', 'Nunito', 'Manrope', 'Sofia Sans', 'Dosis', 'PT Sans', 'Space Grotesk', 'Rajdhani', 'Saira Semi Condensed', 'Jura']
export async function loadFonts () {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')

View File

@@ -1,4 +1,5 @@
import {io} from "socket.io-client";
import {useStore} from "@/store/store.js";
export const socket = io(import.meta.env.VITE_WS_URL || undefined, { transports: ["websocket"] })
@@ -14,3 +15,7 @@ socket.on('welcome', (data)=>{
console.log('welcome',data)
})
socket.on('chainInfo', async (chainInfo)=>{
const s = useStore()
s.chainInfo = chainInfo
})

View File

@@ -1,33 +1,44 @@
// Utilities
import { defineStore } from 'pinia'
import {knownTokens} from "@/tokens.js";
export const useStore = defineStore('app', {
state: () => ({
chain: {name:'Not Connected', id:0, icon:''},
wallet: {
connected: false,
},
errors: [],
extraTokens: {},
chainInfo: null,
chainId: null,
account: null,
vault: null,
errors: [{
title: 'DANGER!',
text: 'This is early development (alpha) software, which could have severe bugs that lose all your money. Thank you for testing a SMALL amount!',
closeable: false
}],
extraTokens: {},
}),
getters: {
chain: (s)=> !s.chainInfo ? null : (s.chainInfo[s.chainId] || null),
tokens: (s)=>{
const extras = s.extraTokens[s.chain.id]
return extras === undefined ? Object.values(known_tokens[s.chain.id])
: [...Object.values(known_tokens[s.chain.id]), ...Object.values(extras)]
console.log('tokensget',s)
let known = knownTokens[s.chainId]
known = known ? Object.values(known) : []
let extras = s.extraTokens[s.chainId]
extras = extras ? Object.values(extras) : []
console.log('tokens for chainId',s.chainId, known, extras)
return [...known, ...extras]
},
factory: (s)=>!s.chain?null:s.chain.factory,
helper: (s)=>!s.chain?null:s.chain.helper,
},
actions: {
error(title, message) {
this.errors.push({title:title, message:message})
error(title, text, closeable=true) {
this.errors.push({title, text, closeable})
},
closeError(title, message) {
console.log('closing error', title, message)
closeError(title, text) {
console.log('closing error', title, text)
const result = []
this.errors.forEach((i)=>{if(i.title!==title && i.message!==message) result.push(i)})
this.errors.forEach((i)=>{if(i.title!==title && i.text!==text) result.push(i)})
this.errors = result
},
}
},
})

View File

@@ -11,4 +11,6 @@
//$variable: false,
$body-font-family: v.$body-font-family,
$heading-font-family: v.$heading-font-family,
$card-title-font-size: 24px,
$app-bar-title-font-size: 32px,
);

View File

@@ -18,9 +18,17 @@
.v-list-subheader {
font-family: v.$heading-font-family;
}
input {
text-align: center;
}
.v-field__input {
justify-content: center;
}
}
.sc {
font-family: v.$heading-font-family;
.uniswap-pink {
color: v.$uniswap-pink;
}

View File

@@ -7,6 +7,8 @@ $blue: #0033CC;
$white: #fffefd; // just a touch green
$black: #000102;
$uniswap-pink: #ff007a;
$primary: $blue;
$primary-50: transparentize($primary,0.5);
$primary-25: transparentize($primary,0.75);
@@ -29,10 +31,19 @@ $all-colors: (
green: $green,
yellow: $yellow,
red: $red,
uniswap: $uniswap-pink,
);
$body-font-family: 'Saira Semi Condensed', monospace, sans-serif; // fairly geometric, horizontal s's, clean sans, readable
//$body-font-family: 'Exo 2', monospace, sans-serif; // good, works with Orbitron
//$body-font-family: 'Jura', monospace, sans-serif; // interesting numbers, zero dot. l-foot but the s is too twisty with light serifs
//$body-font-family: 'Manrope', monospace, sans-serif; // readable, clean, works with Orbitron
//$body-font-family: 'Victor Mono', monospace, sans-serif;
$body-font-family: 'Manrope', monospace, sans-serif;
//$body-font-family: 'Space Grotesk', monospace, sans-serif; // silly serifs on caps
//$body-font-family: 'Rajdhani', monospace, sans-serif; // very thin a little hard to read
//$body-font-family: 'Dosis', monospace, sans-serif; // a little informal, to Dilbert-y
//$body-font-family: 'PT Sans', monospace, sans-serif; // not bad but numbers are too seriffy
//$body-font-family: 'Cairo', monospace, sans-serif; // number baseline is jagged
//$body-font-family: 'Nunito', monospace, sans-serif;
//$body-font-family: 'Sofia Sans', monospace, sans-serif;
//$body-font-family: 'Quantico', sans-serif;
@@ -43,4 +54,4 @@ $heading-font-family: 'Orbitron', sans-serif;
//$heading-font-family: 'Chakra Petch', sans-serif;
$sm-breakpoint: 600px;
$card-maxw: 30rem;
$card-maxw: 40em;

94
src/tokens.js Normal file
View File

@@ -0,0 +1,94 @@
export const knownTokens = {};
const known_chains = [
{
name: 'Arbitrum One',
id: 42161,
icon: null,
}
]
const arbitrumTokens = [
{
name: 'Wrapped Ether',
symbol: 'WETH',
decimals: 18,
icon: null,
address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
},
{
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
icon: null,
address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
},
{
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
icon: null,
address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
},
{
name: 'Wrapped Bitcoin',
symbol: 'WBTC',
decimals: 8,
icon: null,
address: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
},
{
name: 'DAI Stablecoin',
symbol: 'DAI',
decimals: 18,
icon: null,
address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
},
{
name: 'Uniswap',
symbol: 'UNI',
decimals: 18,
icon: null,
address: '0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0',
},
{
name: 'Chainlink Token',
symbol: 'LINK',
decimals: 18,
icon: null,
address: '0xf97f4df75117a78c1A5a0DBb814Af92458539FB4',
},
{
name: 'TrueUSD',
symbol: 'TUSD',
decimals: 18,
icon: null,
address: '0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07',
},
{
name: 'Lido DAO Token',
symbol: 'LDO',
decimals: 18,
icon: null,
address: '0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60',
},
{
name: 'Arbitrum',
symbol: 'ARB',
decimals: 18,
icon: null,
address: '0x912CE59144191C1204E64559FE8253a0e49E6548',
},
];
const _knownTokens = {
42161: arbitrumTokens,
1338: arbitrumTokens,
};
for( const chainId in _knownTokens ) {
knownTokens[chainId] = {}
for( const info of _knownTokens[chainId] )
knownTokens[chainId][info.address] = info
}