withdrawls

This commit is contained in:
Tim Olson
2023-11-01 00:33:53 -04:00
parent ee61c96d38
commit 16e04b0f90
20 changed files with 438 additions and 189 deletions

View File

@@ -1,7 +1,6 @@
import {ethers} from "ethers"; import {ethers} from "ethers";
import {factoryAbi, queryHelperAbi, vaultAbi} from "@/blockchain/abi.js"; import {factoryAbi, queryHelperAbi, vaultAbi} from "@/blockchain/abi.js";
import {useStore} from "@/store/store.js"; import {useStore} from "@/store/store.js";
import {provider} from "@/blockchain/wallet.js";
export function vaultAddress( owner, num=0) { export function vaultAddress( owner, num=0) {
@@ -11,7 +10,7 @@ export function vaultAddress( owner, num=0) {
} }
function contractOrNull(addr,abi,provider) { export function contractOrNull(addr,abi,provider) {
try { try {
return new ethers.Contract(addr,abi,provider) return new ethers.Contract(addr,abi,provider)
} }
@@ -22,16 +21,17 @@ function contractOrNull(addr,abi,provider) {
export async function factoryContract() { export async function factoryContract() {
const s = useStore() const s = useStore()
return contractOrNull(s.factory, factoryAbi, provider) return contractOrNull(s.factory, factoryAbi, s.provider)
} }
export async function queryHelperContract() { export async function queryHelperContract() {
const s = useStore() const s = useStore()
return contractOrNull(s.helper, queryHelperAbi, provider) return contractOrNull(s.helper, queryHelperAbi, s.provider)
} }
export async function poolContract(addr) { export async function poolContract(addr) {
return contractOrNull(addr, poolAbi, provider) const s = useStore()
return contractOrNull(addr, poolAbi, s.provider)
} }
export async function vaultContract(num, signer) { export async function vaultContract(num, signer) {

44
src/blockchain/token.js Normal file
View File

@@ -0,0 +1,44 @@
import {socket} from "@/socket.js";
import {useStore} from "@/store/store.js";
import {erc20Abi} from "@/blockchain/abi.js";
import {ethers} from "ethers";
export async function getToken(addr) {
const s = useStore()
if (!(addr in s.tokens))
await addExtraToken(addr)
return s.tokens[addr]
}
export async function addExtraToken(addr) {
const prom = new Promise((resolve) => {
const s = useStore()
const chainId = s.chainId
console.log('querying token', addr)
socket.emit('lookupToken', chainId, addr, (info) => {
console.log('server token info', info)
if (info !== null) {
s.addToken(chainId, info)
resolve(info)
}
else {
if( s.provider===null ) {
console.log('warning: token lookup cancelled due to null provider', addr)
resolve(null)
return
}
const token = new ethers.Contract(addr, erc20Abi, s.provider)
Promise.all( [token.symbol(), token.decimals()] ).then((symbol,decimals)=>{
info = {
address: addr,
symbol: symbol,
decimals: decimals,
}
s.addToken(chainId, info)
resolve(info)
})
}
})
})
return await prom
}

View File

@@ -1,26 +1,26 @@
import {ethers} from "ethers"; import {ethers} from "ethers";
import {useStore} from "@/store/store"; import {setProvider, useStore} from "@/store/store";
import {socket} from "@/socket.js"; import {socket} from "@/socket.js";
import {vaultContract} from "@/blockchain/contract.js"; import {contractOrNull} from "@/blockchain/contract.js";
import {vaultAbi} from "@/blockchain/abi.js";
export let provider = null
export function onChainChanged(chainId) { export function onChainChanged(chainId) {
chainId = Number(chainId) chainId = Number(chainId)
console.log('chain changed', chainId) console.log('chain changed', chainId)
const store = useStore() const store = useStore()
if( chainId !== store.chainId ) { if( chainId !== store.chainId ) {
store.chainId = chainId const provider = new ethers.BrowserProvider(window.ethereum, chainId);
setProvider(provider, chainId)
store.account = null store.account = null
provider = new ethers.BrowserProvider(window.ethereum, chainId)
provider.listAccounts().then(changeAccounts) provider.listAccounts().then(changeAccounts)
new ethers.Interface([ store.chainId = chainId // touch the chainId last. will cause any clients of the store's provider getter to refresh
// 'event DexorderSwapCreated' // todo
])
} }
} }
function changeAccounts(accounts) { function changeAccounts(accounts) {
console.log('change accounts', accounts)
const store = useStore()
if( accounts.length === 0 ) { if( accounts.length === 0 ) {
store.account = null store.account = null
store.vaults = [] store.vaults = []
@@ -29,16 +29,15 @@ function changeAccounts(accounts) {
else { else {
const store = useStore() const store = useStore()
store.account = accounts[0].address store.account = accounts[0].address
flushOrders() flushTransactions()
socket.emit('address', store.chainId, accounts[0].address) socket.emit('address', store.chainId, accounts[0].address)
} }
} }
function onAccountsChanged(accounts) { function onAccountsChanged(accounts) {
// console.log('accounts changed', accounts)
const store = useStore() const store = useStore()
if (accounts.length === 0 || accounts[0] !== store.account) if (accounts.length === 0 || accounts[0] !== store.account)
changeAccounts(store, accounts); changeAccounts(accounts);
} }
export async function watchWallet() { export async function watchWallet() {
@@ -82,31 +81,56 @@ const errorHandlingProxy = {
export async function connectWallet() { export async function connectWallet() {
return provider.getSigner() console.log('TODO connect wallet')
// eth_getaccounts
} }
export async function pendOrder(order) { export function pendOrder(order) {
console.log('order', JSON.stringify(order)) console.log('order', JSON.stringify(order))
const s = useStore() const s = useStore()
s.pendingOrders.push(order) const vault = s.vault;
flushOrders() if(vault === null ) {
} console.error('vault was null during pendOrder')
export function flushOrders() {
// noinspection JSIgnoredPromiseFromCall
asyncFlushOrders()
}
export async function asyncFlushOrders() {
const s = useStore()
const orders = s.pendingOrders
if (!orders.length)
return return
}
pendTransaction(async (signer)=> {
const contract = contractOrNull(vault, vaultAbi, signer)
if( contract === null ) {
console.error('vault contract was null while sending order transaction', vault)
return null
}
return await contract.placeOrder(order)
})
}
export function pendTransaction(sender) {
const s = useStore()
s.transactionSenders.push(sender)
flushTransactions()
}
export function flushTransactions() {
// noinspection JSIgnoredPromiseFromCall
asyncFlushTransactions()
}
export async function asyncFlushTransactions() {
// todo rework into flushTransactions()
const s = useStore()
if( s.provider === null ) {
console.log('warning: asyncFlushOrders() cancelled due to null provider')
return
}
const senders = s.transactionSenders
if (!senders.length)
return
console.log(`flushing ${s.transactionSenders.length} transactions`)
let signer let signer
try { try {
signer = await provider.getSigner(); signer = await s.provider.getSigner();
} catch (e) { } catch (e) {
// { // {
// "code": -32002, // "code": -32002,
@@ -119,28 +143,27 @@ export async function asyncFlushOrders() {
socket.emit('ensureVault', s.chainId, await signer.getAddress(), 0) socket.emit('ensureVault', s.chainId, await signer.getAddress(), 0)
return return
} }
const contract = await vaultContract(0, signer) for (const sender of senders)
if (!contract) { doSendTransaction(sender, signer)
console.error(`no contract for vault 0 of ${signer.address}`)
return
}
for (const order of orders)
doPlaceOrder(s, contract, order)
} }
function doPlaceOrder(s, contract, order) { function doSendTransaction(sender, signer) {
contract.placeOrder(order).then((tx)=>{ const s = useStore();
console.log('placed order', tx) sender(signer).then((tx)=>{
s.removePendingOrder(order) console.log('sent transaction', tx)
s.removeTransactionSender(sender)
tx.wait().then((tr)=>console.log('tx receipt',tr)) tx.wait().then((tr)=>console.log('tx receipt',tr))
}).catch((e)=>{ }).catch((e)=>{
if( e.info.error.code === 4001 ) { if( e.info?.error?.code === 4001 ) {
console.log(`user rejected order`, order) console.log(`user rejected transaction`)
s.removePendingOrder(order) s.removeTransactionSender(sender)
} }
else { else {
console.error('error placing order', order, e.reason, e.info) if( e.reason && e.info )
s.removePendingOrder(order) console.error('error sending transaction', e.reason, e.info)
else
console.error('error sending transaction', e)
s.removeTransactionSender(sender)
// todo retry? // todo retry?
} }
}) })
@@ -150,5 +173,5 @@ socket.on('vaults', (vaults)=>{
const s = useStore() const s = useStore()
console.log('vaults', vaults) console.log('vaults', vaults)
s.vaults = vaults s.vaults = vaults
flushOrders() flushTransactions()
}) })

View File

@@ -0,0 +1,34 @@
<template>
<div :class="['chain-block','chain-'+chainId]"><v-avatar class='chain-avatar' v-if="media.img" :image="media.img" rounded="0" size="1em"/>&nbsp;{{media.name}}</div>
</template>
<script setup>
import {useStore} from "@/store/store";
import {computed} from "vue";
const unsupportedMedia = {}
const chainMedia = {
1: {name: 'Ethereum', img: null},
42161: { name: 'Arbitrum One', img: '/arbitrum-logo.svg'},
31331: { name: 'Mockchain', img: null},
}
const s = useStore()
const props = defineProps(['chainId'])
const media = computed(()=>chainMedia[props.chainId] || unsupportedMedia)
</script>
<style lang="scss"> // NOT 'scoped'
@use "src/styles/vars" as *;
//.chain-avatar {
// max-height: 1em;
// max-width: 1em;
//}
.chain-block {
display: inline;
}
.chain-42161 {
color: #12aaff;
}
</style>

View File

@@ -1,24 +0,0 @@
<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

@@ -1,7 +1,12 @@
<template> <template>
<v-btn v-if="permitted" rounded variant="text" size="small" density="compact" @click="copy()" <v-tooltip :model-value="!error&&copied" :open-on-hover="false">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" v-if="permitted" rounded variant="text" size="small" density="compact" @click="copy()"
:class="error?'error':copied?'success':''" :class="error?'error':copied?'success':''"
:icon="error?'mdi-close-box-outline':copied?'mdi-check-circle-outline':'mdi-content-copy'"/> :icon="error?'mdi-close-box-outline':copied?'mdi-check-circle-outline':'mdi-content-copy'"/>
</template>
<span>Copied!</span>
</v-tooltip>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -0,0 +1,52 @@
<template>
<slot v-if="ok"/>
<phone-card v-if="!walletOk">
<v-card-title>Install Wallet</v-card-title>
<v-card-text>
A cryptocurrency wallet such as <a href="https://metamask.io/download/">MetaMask</a> is required to use Dexorder.
Please install a crypto wallet into your browser to experience the power of Dexorder.
</v-card-text>
<v-card-actions>
<v-btn prepend-icon="mdi-reload" text="Reload After Installing Wallet"/>
</v-card-actions>
</phone-card>
<phone-card v-if="walletOk && !providerOk">
<v-card-text>
Please log in to your crypto wallet.
</v-card-text>
<v-card-actions v-if="walletOk && !providerOk">
<v-btn prepend-icon="mdi-power" text="Connect Wallet" @click="connectWallet"/>
</v-card-actions>
</phone-card>
<phone-card v-if="walletOk && providerOk && !chainOk">
<v-card-title><v-icon icon="mdi-reload-alert" color="warning"/> Change Blockchain</v-card-title>
<v-card-text>
Dexorder works only with <blockchain chain-id="42161"/>. Please switch to the
<blockchain chain-id="42161"/> blockchain in your wallet.
</v-card-text>
</phone-card>
</template>
<script setup>
import {useStore} from "@/store/store";
import PhoneCard from "@/components/PhoneCard.vue";
import {connectWallet} from "@/blockchain/wallet.js";
import {computed} from "vue";
import Blockchain from "@/components/Blockchain.vue";
const s = useStore()
const walletOk = typeof window.ethereum !== 'undefined'
const providerOk = computed(()=>s.provider!==null)
const chainOk = computed(()=>providerOk.value && s.helper!==null)
const ok = computed(()=>{
return walletOk && providerOk.value && chainOk.value
})
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;
.arbitrum {
color: $arbitrum-color;
}
</style>

View File

@@ -1,21 +0,0 @@
<template>
<!-- <v-card v-if="!s.account" prepend-icon="mdi-connection" title="Connect Wallet"-->
<!-- text="Please connect your wallet to an Arbitrum account.">-->
<!-- <v-card-actions><v-btn text="Connect Wallet" color="green" variant="elevated" @click="connectWallet" prepend-icon="mdi-power"/></v-card-actions>-->
<!-- </v-card>-->
<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()
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<v-btn prepend-icon="mdi-plus" text="New Order"/>
</template> </template>
<script setup> <script setup>

View File

@@ -1,8 +1,10 @@
<template> <template>
<v-card class="d-none d-md-block" :elevation="4"> <!-- tablets and desktops get a card outline -->
<v-card class="d-none d-sm-block phone-card" :elevation="4">
<slot/> <slot/>
</v-card> </v-card>
<v-container class="d-md-none"> <!-- phones use the entire screen -->
<v-container class="d-sm-none">
<slot/> <slot/>
</v-container> </v-container>
</template> </template>
@@ -16,4 +18,7 @@ const s = useStore()
<style scoped lang="scss"> <style scoped lang="scss">
@use "src/styles/vars" as *; @use "src/styles/vars" as *;
.phone-card {
max-width: $card-maxw;
}
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<NeedsQueryHelper> <needs-provider>
<PhoneCard> <phone-card>
<v-card-title class="big">DCA / TWAP</v-card-title> <v-card-title class="big">DCA / TWAP</v-card-title>
<v-card-subtitle>Multiple tranches over a time range</v-card-subtitle> <v-card-subtitle>Multiple tranches over a time range</v-card-subtitle>
<v-card-text> <v-card-text>
@@ -20,7 +20,7 @@
<v-chip v-for="r in routes" variant="text"> <v-chip v-for="r in routes" variant="text">
{{ s.chain.name }} {{ s.chain.name }}
<v-img src="https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg" width="1.5em"/> <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> <span class="uniswap-color ml-0 mr-1">v3</span>
{{pairSymbol}} {{r.fee/10000}}% {{pairSymbol}} {{r.fee/10000}}%
</v-chip> </v-chip>
@@ -78,8 +78,8 @@
<v-btn variant="outlined" color="red">Cancel</v-btn> <v-btn variant="outlined" color="red">Cancel</v-btn>
<v-btn variant="flat" color="green" :disabled="!validOrder" @click="placeOrder">Place Order</v-btn> <v-btn variant="flat" color="green" :disabled="!validOrder" @click="placeOrder">Place Order</v-btn>
</v-card-actions> </v-card-actions>
</PhoneCard> </phone-card>
</NeedsQueryHelper> </needs-provider>
</template> </template>
<script setup> <script setup>
@@ -90,15 +90,15 @@ import PhoneCard from "@/components/PhoneCard.vue";
import {queryHelperContract} from "@/blockchain/contract.js"; import {queryHelperContract} from "@/blockchain/contract.js";
// noinspection ES6UnusedImports // noinspection ES6UnusedImports
import {SingletonCoroutine, vAutoSelect} from "@/misc.js"; import {SingletonCoroutine, vAutoSelect} from "@/misc.js";
import NeedsQueryHelper from "@/components/NeedsQueryHelper.vue";
import {Exchange, newOrder, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js"; import {Exchange, newOrder, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js";
import {FixedNumber} from "ethers"; import {FixedNumber} from "ethers";
import {pendOrder} from "@/blockchain/wallet.js"; import {pendOrder} from "@/blockchain/wallet.js";
import NeedsProvider from "@/components/NeedsProvider.vue";
const s = useStore() const s = useStore()
const buy = ref(false) const buy = ref(false)
let _tokenA = ref(s.tokens !== undefined && s.tokens.length >= 1 ? s.tokens[0] : null) let _tokenA = ref(Object.values(s.tokens).length >= 1 ? Object.values(s.tokens)[0] : null)
let _tokenB = ref(s.tokens !== undefined && s.tokens.length >= 2 ? s.tokens[1] : null) let _tokenB = ref(Object.values(s.tokens).length >= 2 ? Object.values(s.tokens)[1] : null)
const tokenA = computed({ const tokenA = computed({
get() { get() {
return _tokenA.value return _tokenA.value
@@ -149,19 +149,22 @@ const limitIsMinimum = computed(() => !(buy.value ^ inverted.value))
const validOrder = computed(()=>amount.value > 0 && routes.value.length > 0 ) const validOrder = computed(()=>amount.value > 0 && routes.value.length > 0 )
async function findRoute() { async function findRoute() {
console.log('finding route', _tokenA.value, _tokenB.value)
routes.value = [] routes.value = []
if( !_tokenA.value || !_tokenB.value ) if( !_tokenA.value || !_tokenB.value )
return return
const helper = await queryHelperContract() const helper = await queryHelperContract()
if( !helper ) if( !helper ) {
console.log('no helper')
return return
}
routesPending.value = true routesPending.value = true
let rawRoutes let rawRoutes
try { try {
rawRoutes = await helper.getRoutes(tokenA.value.address, tokenB.value.address) rawRoutes = await helper.getRoutes(tokenA.value.address, tokenB.value.address)
} }
catch (e) { catch (e) {
// console.log('routes exception', e) console.log('routes exception', e)
routesPending.value = false routesPending.value = false
return return
} }
@@ -242,7 +245,7 @@ function validateMin(v) {
return true return true
} }
async function placeOrder() { function placeOrder() {
const ta = tokenA.value; const ta = tokenA.value;
const tb = tokenB.value; const tb = tokenB.value;
const tokenIn = buy.value ? tb.address : ta.address const tokenIn = buy.value ? tb.address : ta.address
@@ -278,7 +281,7 @@ async function placeOrder() {
ts.push([amtPerTranche, cs]) ts.push([amtPerTranche, cs])
} }
const order = newOrder(tokenIn, tokenOut, route.exchange, route.fee, amt, amountIsInput, ts) const order = newOrder(tokenIn, tokenOut, route.exchange, route.fee, amt, amountIsInput, ts)
await pendOrder(order) pendOrder(order)
} }
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<v-combobox :items="Object.values(s.tokens)" :auto-select-first="true" <v-combobox :items="tokens" :auto-select-first="true"
item-title="symbol" item-title="symbol"
:filter-keys="['raw.name','raw.symbol','raw.address']" :filter-keys="['raw.name','raw.symbol','raw.address']"
:model-value="modelValue" :model-value="modelValue"
@@ -21,7 +21,7 @@
<script setup> <script setup>
import {useStore as useStore2} from "@/store/store"; import {useStore as useStore2} from "@/store/store";
import {ref} from "vue"; import {computed, ref} from "vue";
import {ethers} from "ethers"; import {ethers} from "ethers";
// noinspection ES6UnusedImports // noinspection ES6UnusedImports
import {vAutoSelect} from "@/misc.js"; import {vAutoSelect} from "@/misc.js";
@@ -32,6 +32,8 @@ const props = defineProps(['modelValue', 'label'])
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const loading = ref(false) const loading = ref(false)
const errors = ref([]) const errors = ref([])
const tokens = computed(()=>Object.values(s.tokens))
function good() { function good() {
errors.value = [] errors.value = []

View File

@@ -1,10 +1,24 @@
<template> <template>
<tr> <tr>
<td><v-img v-if="imageSrc" :src="imageSrc"/></td> <td>
<td>{{token.symbol}}</td> <v-avatar v-if="imageSrc" :image="imageSrc"/>
<td class="d-none d-sm-table-cell">{{token.name||''}}</td> </td>
<td>{{fixed}}</td> <td class="d-none d-sm-table-cell">{{ token.name || '' }}</td>
<td><!-- todo actions --></td> <td class="text-right">{{ fixed }}</td>
<td class="text-left">{{ token.symbol }}</td>
<!-- todo price and value columns -->
<td>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="plain" v-bind="props" icon="mdi-menu"/> <!-- mdi-dots-vertical -->
</template>
<v-list>
<v-list-subheader :title="token.symbol"/>
<v-list-item title="Withdraw" key="withdraw" value="withdraw" prepend-icon="mdi-arrow-down-bold"
@click="()=>onWithdraw(token.address)"/>
</v-list>
</v-menu>
</td>
</tr> </tr>
</template> </template>
@@ -12,13 +26,17 @@
import {useStore} from "@/store/store"; import {useStore} from "@/store/store";
import {getToken} from "@/blockchain/token.js"; import {getToken} from "@/blockchain/token.js";
import {FixedNumber} from "ethers"; import {FixedNumber} from "ethers";
import {computed} from "vue"; import {computed, ref} from "vue";
const s = useStore() const s = useStore()
const props = defineProps(['addr','amount']) const props = defineProps(['addr', 'amount', 'onWithdraw'])
const token = await getToken(props.addr) const token = await getToken(props.addr)
console.log('token', props.addr, token) const fixed = computed(() => FixedNumber.fromValue(props.amount, token.decimals, {
const fixed = computed(()=>FixedNumber.fromValue(props.amount, token.decimals, {width:256, decimals: token.decimals})) width: 256,
const imageSrc = computed(()=>null ) decimals: token.decimals
}))
const imageSrc = computed(() => null)
const withdrawing = ref(false)
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -1,5 +1,6 @@
<template> <template>
<!-- todo we can use something like this for ethereum where the vault creation is too expensive to subsidize <needs-provider>
<!-- todo we can use something like this for ethereum where the vault creation is too expensive to subsidize
<PhoneCard v-if="s.vault===null || s.vault.length === 0"> <PhoneCard v-if="s.vault===null || s.vault.length === 0">
<v-card-title><v-icon color="warning" icon="mdi-alert"/>&nbsp;Setup&nbsp;Vault</v-card-title> <v-card-title><v-icon color="warning" icon="mdi-alert"/>&nbsp;Setup&nbsp;Vault</v-card-title>
<v-card-subtitle>Create Your Own Personal Dexorder Vault</v-card-subtitle> <v-card-subtitle>Create Your Own Personal Dexorder Vault</v-card-subtitle>
@@ -21,7 +22,7 @@
easy access. easy access.
</v-card-text> </v-card-text>
</PhoneCard> </PhoneCard>
--> -->
<v-card v-if="s.vaults.length<num"> <v-card v-if="s.vaults.length<num">
<v-card-title>No Vault Yet</v-card-title> <v-card-title>No Vault Yet</v-card-title>
<v-card-text v-if="num!==0"><!--todo-->Multiple vaults are not yet supported</v-card-text> <v-card-text v-if="num!==0"><!--todo-->Multiple vaults are not yet supported</v-card-text>
@@ -34,25 +35,36 @@
<p>There are no funds currently in your vault.</p> <p>There are no funds currently in your vault.</p>
<p>Send tokens to the address above to fund your vault.</p> <p>Send tokens to the address above to fund your vault.</p>
</v-card-text> </v-card-text>
<v-card-item> <v-card-item v-if="!empty">
<v-table v-if="!empty"> <v-table>
<tbody> <tbody>
<suspense v-for="(amount,addr) of balances"> <suspense v-for="(amount,addr) of balances">
<token-row :addr="addr" :amount="amount"/> <token-row :addr="addr" :amount="amount" :onWithdraw="onWithdraw"/>
</suspense> </suspense>
</tbody> </tbody>
</v-table> </v-table>
</v-card-item> </v-card-item>
</v-card> </v-card>
<withdraw :vault="addr" :token="withdrawToken" v-model="withdrawShow"/>
<!--
<div>
addr {{ addr }}<br/>
empty {{ empty }}<br/>
exists {{ exists }}<br/>
</div>
-->
</needs-provider>
</template> </template>
<script setup> <script setup>
import {useStore} from "@/store/store.js"; import {useStore} from "@/store/store.js";
import PhoneCard from "@/components/PhoneCard.vue"; import PhoneCard from "@/components/PhoneCard.vue";
import {computed, defineAsyncComponent} from "vue"; import {computed, defineAsyncComponent, ref} from "vue";
import {vaultAddress} from "@/blockchain/contract.js"; import {vaultAddress} from "@/blockchain/contract.js";
import CopyButton from "@/components/CopyButton.vue"; import CopyButton from "@/components/CopyButton.vue";
import NeedsProvider from "@/components/NeedsProvider.vue";
import Withdraw from "@/components/Withdraw.vue";
console.log('vault setup')
const TokenRow = defineAsyncComponent(()=>import('./TokenRow.vue')) const TokenRow = defineAsyncComponent(()=>import('./TokenRow.vue'))
const s = useStore() const s = useStore()
@@ -63,9 +75,17 @@ const balances = computed(()=>{
console.log('balances', addr.value, s.vaultBalances, bs) console.log('balances', addr.value, s.vaultBalances, bs)
return bs || {} return bs || {}
}) })
const tokenAddrs = computed(()=>Object.keys(balances))
const empty = computed(()=>Object.keys(balances.value).length===0) const empty = computed(()=>Object.keys(balances.value).length===0)
const exists = computed(()=>s.vaults.length>0) const exists = computed(()=>s.vaults.length>0)
const withdrawToken = ref(null)
const withdrawShow = ref(false)
function onWithdraw(addr) {
const token = s.tokens[addr]
console.log('withdraw', addr, token)
withdrawToken.value = token
withdrawShow.value = true
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -0,0 +1,64 @@
<template>
<v-dialog :model-value="modelValue" @update:modelValue="$emit('update:modelValue', $event)">
<v-card>
<v-card-title>
<v-icon icon="mdi-down-arrow"/>&nbsp;Withdraw {{ token.symbol }}
</v-card-title>
<v-card-item>
<v-text-field class="text-end" type="number" variant="outlined" :min="0" :max="balanceFloat"
v-model="floatAmount" :step="balanceFloat/10">
<template v-slot:prepend-inner>
<v-btn variant="text" text="max" @click="floatAmount=balanceFloat"/>
</template>
<template v-slot:append-inner>
<span>{{ token.symbol }}</span>
</template>
</v-text-field>
<v-card-actions>
<v-btn text="Cancel" @click="$emit('update:modelValue', false)"/>
<v-btn text="Withdraw" color="red" @click="withdraw"/>
</v-card-actions>
</v-card-item>
</v-card>
</v-dialog>
</template>
<script setup>
import {useStore} from "@/store/store";
import {computed, ref} from "vue";
import {tokenFloat} from "@/misc.js";
import {contractOrNull} from "@/blockchain/contract.js"
import {vaultAbi} from "@/blockchain/abi.js";
import {pendTransaction} from "@/blockchain/wallet.js";
import {FixedNumber} from "ethers";
const s = useStore()
const props = defineProps(['modelValue', 'vault', 'token'])
const emit = defineEmits(['update:modelValue'])
const balance = computed(() => {
console.log('balance', props.vault, props.token, s.vaultBalances)
return s.vaultBalances[props.vault][props.token.address] || 0
})
const balanceFloat = computed(() => tokenFloat(props.token, balance.value))
const floatAmount = ref(0)
function withdraw() {
const vaultAddr = props.vault
const valueStr = floatAmount.value.toString();
const amount = FixedNumber.fromString(valueStr,{decimals:props.token.decimals, width:256, signed: false}).value;
console.log('pending withdrawl', valueStr, amount, props.token.symbol)
if( amount === 0n )
return
pendTransaction(async (signer)=>{
const vault = contractOrNull(vaultAddr, vaultAbi, signer)
return await vault['withdraw(address,uint256)'](props.token.address, amount)
})
emit('update:modelValue', false)
}
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;
</style>

View File

@@ -4,7 +4,7 @@
<div> <div>
<Alerts/> <Alerts/>
<router-view/> <router-view/>
<v-skeleton-loader v-if="!store.chainInfo" type="card" class="order-card"/> <v-skeleton-loader v-if="!store.chainInfo" type="card" class="order-card"/> <!--todo static chainInfo -->
</div> </div>
</v-container> </v-container>
</v-main> </v-main>
@@ -14,6 +14,7 @@
import Alerts from "@/components/Alerts.vue"; import Alerts from "@/components/Alerts.vue";
import {VSkeletonLoader} from "vuetify/labs/VSkeletonLoader"; import {VSkeletonLoader} from "vuetify/labs/VSkeletonLoader";
import {useStore} from "@/store/store.js"; import {useStore} from "@/store/store.js";
import NeedsProvider from "@/components/NeedsProvider.vue";
const store = useStore() const store = useStore()
</script> </script>

View File

@@ -1,3 +1,4 @@
import {FixedNumber} from "ethers";
export class SingletonCoroutine { export class SingletonCoroutine {
constructor(f, delay=10, retry=true) { constructor(f, delay=10, retry=true) {
@@ -43,3 +44,11 @@ export const vAutoSelect = {
} }
export const uint64max = 18446744073709551615n export const uint64max = 18446744073709551615n
export const uint32max = 4294967295n export const uint32max = 4294967295n
export function tokenNumber(token, balance) {
return FixedNumber.fromValue(balance, token.decimals, {decimals:token.decimals, width: 256})
}
export function tokenFloat(token, balance) {
return tokenNumber(token,balance).toUnsafeFloat()
}

View File

@@ -2,6 +2,14 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import {knownTokens} from "@/knownTokens.js"; import {knownTokens} from "@/knownTokens.js";
let rawProvider = null
let rawProviderChainId = null
export function setProvider( provider, chainId ) {
rawProvider = provider
rawProviderChainId = chainId
}
export const useStore = defineStore('app', { export const useStore = defineStore('app', {
state: () => ({ state: () => ({
chainId: null, chainId: null,
@@ -9,17 +17,18 @@ export const useStore = defineStore('app', {
vaultInitCodeHash: null, vaultInitCodeHash: null,
account: null, account: null,
vaults: [], vaults: [],
pendingOrders: [], // created but not yet sent to metamask. maybe waiting on vault creation. transactionSenders: [], // a list of function(signer) that send transactions
errors: [{ errors: [
title: 'DANGER!', // todo re-enable danger warning
text: 'This is early development (alpha) software. There could be severe bugs that lose all your money. Thank you for testing a SMALL amount!', // {title: 'DANGER!', text: 'This is early development (alpha) software. There could be severe bugs that lose all your money. Thank you for testing a SMALL amount!', closeable: true}
closeable: false ],
}],
extraTokens: {}, extraTokens: {},
poolPrices: {}, poolPrices: {},
vaultBalances: {}, // indexed by vault addr then by token addr. value is an int vaultBalances: {}, // indexed by vault addr then by token addr. value is an int
}), }),
getters: { getters: {
vault: (s)=>s.vaults.length===0 ? null : s.vaults[0],
provider: (s)=>s.chainId===rawProviderChainId ? rawProvider : null,
chain: (s)=> !s.chainInfo ? null : (s.chainInfo[s.chainId] || null), chain: (s)=> !s.chainInfo ? null : (s.chainInfo[s.chainId] || null),
tokens: (s)=>{ tokens: (s)=>{
const chains = s.chainId in s.chainInfo && s.chainInfo[s.chainId].tokens !== undefined ? s.chainInfo[s.chainId].tokens : [] const chains = s.chainId in s.chainInfo && s.chainInfo[s.chainId].tokens !== undefined ? s.chainInfo[s.chainId].tokens : []
@@ -37,8 +46,8 @@ export const useStore = defineStore('app', {
helper: (s)=>!s.chain?null:s.chain.helper, helper: (s)=>!s.chain?null:s.chain.helper,
}, },
actions: { actions: {
removePendingOrder(order) { removeTransactionSender(sender) {
this.pendingOrders = this.pendingOrders.filter((v) => v !== order) this.transactionSenders = this.transactionSenders.filter((v) => v !== sender)
}, },
error(title, text, closeable=true) { error(title, text, closeable=true) {
this.errors.push({title, text, closeable}) this.errors.push({title, text, closeable})

View File

@@ -33,8 +33,11 @@
justify-content: center; justify-content: center;
} }
.v-text-field.text-end input {
text-align: end;
}
} }
.uniswap-pink { .uniswap-color {
color: v.$uniswap-pink; color: v.$uniswap-color;
} }

View File

@@ -4,10 +4,11 @@ $green: #00CC33;
$red: #CC0033; $red: #CC0033;
$yellow: #ffcc00; $yellow: #ffcc00;
$blue: #0033CC; $blue: #0033CC;
$white: #fffefd; // just a touch green $white: #fdfffe; // just a touch greenblue
$black: #000102; $black: #000201; // just a touch greenblue
$uniswap-pink: #ff007a; $arbitrum-color: #12aaff;
$uniswap-color: #ff007a;
$primary: $blue; $primary: $blue;
$primary-50: transparentize($primary,0.5); $primary-50: transparentize($primary,0.5);
@@ -31,7 +32,7 @@ $all-colors: (
green: $green, green: $green,
yellow: $yellow, yellow: $yellow,
red: $red, red: $red,
uniswap: $uniswap-pink, uniswap: $uniswap-color,
); );
$body-font-family: 'Saira Semi Condensed', monospace, sans-serif; // fairly geometric, horizontal s's, clean sans, readable $body-font-family: 'Saira Semi Condensed', monospace, sans-serif; // fairly geometric, horizontal s's, clean sans, readable
@@ -50,8 +51,9 @@ $body-font-family: 'Saira Semi Condensed', monospace, sans-serif; // fairly geom
//$body-font-family: 'Chakra Petch', sans-serif; //$body-font-family: 'Chakra Petch', sans-serif;
//$heading-font-family: 'Tektur', sans-serif; //$heading-font-family: 'Tektur', sans-serif;
$heading-font-family: 'Orbitron', sans-serif; $heading-font-family: 'Orbitron', sans-serif;
//$heading-font-family: 'Exo 2', sans-serif;
//$heading-font-family: 'Quantico', sans-serif; //$heading-font-family: 'Quantico', sans-serif;
//$heading-font-family: 'Chakra Petch', sans-serif; //$heading-font-family: 'Chakra Petch', sans-serif;
$sm-breakpoint: 600px; $sm-breakpoint: 600px;
$card-maxw: 25em; $card-maxw: 34em;