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 {factoryAbi, queryHelperAbi, vaultAbi} from "@/blockchain/abi.js";
import {useStore} from "@/store/store.js";
import {provider} from "@/blockchain/wallet.js";
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 {
return new ethers.Contract(addr,abi,provider)
}
@@ -22,16 +21,17 @@ function contractOrNull(addr,abi,provider) {
export async function factoryContract() {
const s = useStore()
return contractOrNull(s.factory, factoryAbi, provider)
return contractOrNull(s.factory, factoryAbi, s.provider)
}
export async function queryHelperContract() {
const s = useStore()
return contractOrNull(s.helper, queryHelperAbi, provider)
return contractOrNull(s.helper, queryHelperAbi, s.provider)
}
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) {

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 {useStore} from "@/store/store";
import {setProvider, useStore} from "@/store/store";
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) {
chainId = Number(chainId)
console.log('chain changed', chainId)
const store = useStore()
if( chainId !== store.chainId ) {
store.chainId = chainId
const provider = new ethers.BrowserProvider(window.ethereum, chainId);
setProvider(provider, chainId)
store.account = null
provider = new ethers.BrowserProvider(window.ethereum, chainId)
provider.listAccounts().then(changeAccounts)
new ethers.Interface([
// 'event DexorderSwapCreated' // todo
])
store.chainId = chainId // touch the chainId last. will cause any clients of the store's provider getter to refresh
}
}
function changeAccounts(accounts) {
console.log('change accounts', accounts)
const store = useStore()
if( accounts.length === 0 ) {
store.account = null
store.vaults = []
@@ -29,16 +29,15 @@ function changeAccounts(accounts) {
else {
const store = useStore()
store.account = accounts[0].address
flushOrders()
flushTransactions()
socket.emit('address', store.chainId, accounts[0].address)
}
}
function onAccountsChanged(accounts) {
// console.log('accounts changed', accounts)
const store = useStore()
if (accounts.length === 0 || accounts[0] !== store.account)
changeAccounts(store, accounts);
changeAccounts(accounts);
}
export async function watchWallet() {
@@ -82,31 +81,56 @@ const errorHandlingProxy = {
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))
const s = useStore()
s.pendingOrders.push(order)
flushOrders()
}
export function flushOrders() {
// noinspection JSIgnoredPromiseFromCall
asyncFlushOrders()
}
export async function asyncFlushOrders() {
const s = useStore()
const orders = s.pendingOrders
if (!orders.length)
const vault = s.vault;
if(vault === null ) {
console.error('vault was null during pendOrder')
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
try {
signer = await provider.getSigner();
signer = await s.provider.getSigner();
} catch (e) {
// {
// "code": -32002,
@@ -119,28 +143,27 @@ export async function asyncFlushOrders() {
socket.emit('ensureVault', s.chainId, await signer.getAddress(), 0)
return
}
const contract = await vaultContract(0, signer)
if (!contract) {
console.error(`no contract for vault 0 of ${signer.address}`)
return
}
for (const order of orders)
doPlaceOrder(s, contract, order)
for (const sender of senders)
doSendTransaction(sender, signer)
}
function doPlaceOrder(s, contract, order) {
contract.placeOrder(order).then((tx)=>{
console.log('placed order', tx)
s.removePendingOrder(order)
function doSendTransaction(sender, signer) {
const s = useStore();
sender(signer).then((tx)=>{
console.log('sent transaction', tx)
s.removeTransactionSender(sender)
tx.wait().then((tr)=>console.log('tx receipt',tr))
}).catch((e)=>{
if( e.info.error.code === 4001 ) {
console.log(`user rejected order`, order)
s.removePendingOrder(order)
if( e.info?.error?.code === 4001 ) {
console.log(`user rejected transaction`)
s.removeTransactionSender(sender)
}
else {
console.error('error placing order', order, e.reason, e.info)
s.removePendingOrder(order)
if( e.reason && e.info )
console.error('error sending transaction', e.reason, e.info)
else
console.error('error sending transaction', e)
s.removeTransactionSender(sender)
// todo retry?
}
})
@@ -150,5 +173,5 @@ socket.on('vaults', (vaults)=>{
const s = useStore()
console.log('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>
<v-btn 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'"/>
<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':''"
:icon="error?'mdi-close-box-outline':copied?'mdi-check-circle-outline':'mdi-content-copy'"/>
</template>
<span>Copied!</span>
</v-tooltip>
</template>
<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>
<v-btn prepend-icon="mdi-plus" text="New Order"/>
</template>
<script setup>

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,24 @@
<template>
<tr>
<td><v-img v-if="imageSrc" :src="imageSrc"/></td>
<td>{{token.symbol}}</td>
<td class="d-none d-sm-table-cell">{{token.name||''}}</td>
<td>{{fixed}}</td>
<td><!-- todo actions --></td>
<td>
<v-avatar v-if="imageSrc" :image="imageSrc"/>
</td>
<td class="d-none d-sm-table-cell">{{ token.name || '' }}</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>
</template>
@@ -12,13 +26,17 @@
import {useStore} from "@/store/store";
import {getToken} from "@/blockchain/token.js";
import {FixedNumber} from "ethers";
import {computed} from "vue";
import {computed, ref} from "vue";
const s = useStore()
const props = defineProps(['addr','amount'])
const props = defineProps(['addr', 'amount', 'onWithdraw'])
const token = await getToken(props.addr)
console.log('token', props.addr, token)
const fixed = computed(()=>FixedNumber.fromValue(props.amount, token.decimals, {width:256, decimals: token.decimals}))
const imageSrc = computed(()=>null )
const fixed = computed(() => FixedNumber.fromValue(props.amount, token.decimals, {
width: 256,
decimals: token.decimals
}))
const imageSrc = computed(() => null)
const withdrawing = ref(false)
</script>
<style scoped lang="scss">

View File

@@ -1,58 +1,70 @@
<template>
<!-- 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">
<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-text>
Dexorder never has access to your tokens. Instead, you create a personal
vault which acts like your Dexorder account.
Create your own personal asset vault to get started with Dexorder. This vault
acts like your Dexorder account. For security, only <i>you</i> can deposit
or withdraw tokens from your vault, and no token approvals are ever given to
Dexorder. Instead, Dexorder sends trade requests to your vault at the right
times, then your vault checks the validity of those trade requests before
trading directly from your vault to the dex liquidity pool. Dexorder never
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
is unique to you and never changes. You may deposit or withdraw funds in your
vault at any time, and you may save your vault address in your wallet for
easy access.
</v-card-text>
</PhoneCard>
<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">
<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-text>
Dexorder never has access to your tokens. Instead, you create a personal
vault which acts like your Dexorder account.
Create your own personal asset vault to get started with Dexorder. This vault
acts like your Dexorder account. For security, only <i>you</i> can deposit
or withdraw tokens from your vault, and no token approvals are ever given to
Dexorder. Instead, Dexorder sends trade requests to your vault at the right
times, then your vault checks the validity of those trade requests before
trading directly from your vault to the dex liquidity pool. Dexorder never
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
is unique to you and never changes. You may deposit or withdraw funds in your
vault at any time, and you may save your vault address in your wallet for
easy access.
</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">
<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>
</needs-provider>
</template>
<script setup>
import {useStore} from "@/store/store.js";
import PhoneCard from "@/components/PhoneCard.vue";
import {computed, defineAsyncComponent} from "vue";
import {computed, defineAsyncComponent, ref} from "vue";
import {vaultAddress} from "@/blockchain/contract.js";
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 s = useStore()
@@ -63,9 +75,17 @@ const balances = computed(()=>{
console.log('balances', addr.value, s.vaultBalances, bs)
return bs || {}
})
const tokenAddrs = computed(()=>Object.keys(balances))
const empty = computed(()=>Object.keys(balances.value).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>
<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>
<Alerts/>
<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>
</v-container>
</v-main>
@@ -14,6 +14,7 @@
import Alerts from "@/components/Alerts.vue";
import {VSkeletonLoader} from "vuetify/labs/VSkeletonLoader";
import {useStore} from "@/store/store.js";
import NeedsProvider from "@/components/NeedsProvider.vue";
const store = useStore()
</script>

View File

@@ -1,3 +1,4 @@
import {FixedNumber} from "ethers";
export class SingletonCoroutine {
constructor(f, delay=10, retry=true) {
@@ -42,4 +43,12 @@ export const vAutoSelect = {
}
}
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 {knownTokens} from "@/knownTokens.js";
let rawProvider = null
let rawProviderChainId = null
export function setProvider( provider, chainId ) {
rawProvider = provider
rawProviderChainId = chainId
}
export const useStore = defineStore('app', {
state: () => ({
chainId: null,
@@ -9,17 +17,18 @@ export const useStore = defineStore('app', {
vaultInitCodeHash: null,
account: null,
vaults: [],
pendingOrders: [], // created but not yet sent to metamask. maybe waiting on vault creation.
errors: [{
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: false
}],
transactionSenders: [], // a list of function(signer) that send transactions
errors: [
// todo re-enable danger warning
// {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}
],
extraTokens: {},
poolPrices: {},
vaultBalances: {}, // indexed by vault addr then by token addr. value is an int
}),
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),
tokens: (s)=>{
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,
},
actions: {
removePendingOrder(order) {
this.pendingOrders = this.pendingOrders.filter((v) => v !== order)
removeTransactionSender(sender) {
this.transactionSenders = this.transactionSenders.filter((v) => v !== sender)
},
error(title, text, closeable=true) {
this.errors.push({title, text, closeable})

View File

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

View File

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