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">
:class="error?'error':copied?'success':''" <template v-slot:activator="{ props }">
:icon="error?'mdi-close-box-outline':copied?'mdi-check-circle-outline':'mdi-content-copy'"/> <v-btn v-bind="props" v-if="permitted" rounded variant="text" size="small" density="compact" @click="copy()"
:class="error?'error':copied?'success':''"
: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,58 +1,70 @@
<template> <template>
<!-- todo we can use something like this for ethereum where the vault creation is too expensive to subsidize <needs-provider>
<PhoneCard v-if="s.vault===null || s.vault.length === 0"> <!-- todo we can use something like this for ethereum where the vault creation is too expensive to subsidize
<v-card-title><v-icon color="warning" icon="mdi-alert"/>&nbsp;Setup&nbsp;Vault</v-card-title> <PhoneCard v-if="s.vault===null || s.vault.length === 0">
<v-card-subtitle>Create Your Own Personal Dexorder Vault</v-card-subtitle> <v-card-title><v-icon color="warning" icon="mdi-alert"/>&nbsp;Setup&nbsp;Vault</v-card-title>
<v-card-text> <v-card-subtitle>Create Your Own Personal Dexorder Vault</v-card-subtitle>
Dexorder never has access to your tokens. Instead, you create a personal <v-card-text>
vault which acts like your Dexorder account. Dexorder never has access to your tokens. Instead, you create a personal
Create your own personal asset vault to get started with Dexorder. This vault vault which acts like your Dexorder account.
acts like your Dexorder account. For security, only <i>you</i> can deposit Create your own personal asset vault to get started with Dexorder. This vault
or withdraw tokens from your vault, and no token approvals are ever given to acts like your Dexorder account. For security, only <i>you</i> can deposit
Dexorder. Instead, Dexorder sends trade requests to your vault at the right or withdraw tokens from your vault, and no token approvals are ever given to
times, then your vault checks the validity of those trade requests before Dexorder. Instead, Dexorder sends trade requests to your vault at the right
trading directly from your vault to the dex liquidity pool. Dexorder never times, then your vault checks the validity of those trade requests before
has any access to the funds in your vault. trading directly from your vault to the dex liquidity pool. Dexorder never
</v-card-text> has any access to the funds in your vault.
<v-card-text> </v-card-text>
Creating your personal vault is a one-time setup operation. Your vault address <v-card-text>
is unique to you and never changes. You may deposit or withdraw funds in your Creating your personal vault is a one-time setup operation. Your vault address
vault at any time, and you may save your vault address in your wallet for is unique to you and never changes. You may deposit or withdraw funds in your
easy access. vault at any time, and you may save your vault address in your wallet for
</v-card-text> easy access.
</PhoneCard> </v-card-text>
</PhoneCard>
-->
<v-card v-if="s.vaults.length<num">
<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">Create an order first, then your vault account will appear here to accept a deposit of trading funds.</v-card-text>
</v-card>
<v-card v-if="s.vaults.length>num">
<v-card-title>Vault {{s.vaults.length>1?'#'+(num+1):''}}</v-card-title> <!-- todo vault nicknames -->
<v-card-subtitle v-if="exists">{{addr}} <copy-button :text="addr"/></v-card-subtitle>
<v-card-text v-if="empty">
<p>There are no funds currently in your vault.</p>
<p>Send tokens to the address above to fund your vault.</p>
</v-card-text>
<v-card-item v-if="!empty">
<v-table>
<tbody>
<suspense v-for="(amount,addr) of balances">
<token-row :addr="addr" :amount="amount" :onWithdraw="onWithdraw"/>
</suspense>
</tbody>
</v-table>
</v-card-item>
</v-card>
<withdraw :vault="addr" :token="withdrawToken" v-model="withdrawShow"/>
<!--
<div>
addr {{ addr }}<br/>
empty {{ empty }}<br/>
exists {{ exists }}<br/>
</div>
--> -->
<v-card v-if="s.vaults.length<num"> </needs-provider>
<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">Create an order first, then your vault account will appear here to accept a deposit of trading funds.</v-card-text>
</v-card>
<v-card v-if="s.vaults.length>num">
<v-card-title>Vault {{s.vaults.length>1?'#'+(num+1):''}}</v-card-title> <!-- todo vault nicknames -->
<v-card-subtitle v-if="exists">{{addr}} <copy-button :text="addr"/></v-card-subtitle>
<v-card-text v-if="empty">
<p>There are no funds currently in your vault.</p>
<p>Send tokens to the address above to fund your vault.</p>
</v-card-text>
<v-card-item>
<v-table v-if="!empty">
<tbody>
<suspense v-for="(amount,addr) of balances">
<token-row :addr="addr" :amount="amount"/>
</suspense>
</tbody>
</v-table>
</v-card-item>
</v-card>
</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;