orderspec refactor for server and web

This commit is contained in:
Tim Olson
2023-12-07 18:37:11 -04:00
parent 545583586c
commit 83619ea248
12 changed files with 306 additions and 110 deletions

View File

@@ -1,5 +1,5 @@
export function applyFills( orderStatus, filled ) {
// console.log('apply fills', orderStatus, filled)
console.log('apply fills', orderStatus, filled)
orderStatus[4] = filled[0][0]
orderStatus[5] = filled[0][1]
for( const i in filled[1] ) {
@@ -7,7 +7,7 @@ export function applyFills( orderStatus, filled ) {
orderStatus[6][i] = filledIn
orderStatus[7][i] = filledOut
}
// console.log('applied fills', orderStatus)
console.log('applied fills', orderStatus)
}

View File

@@ -5,7 +5,7 @@ import {useStore} from "@/store/store.js";
export function vaultAddress( owner, num=0) {
const s = useStore()
// console.log('vaultAddress', owner, s.factory, s.vaultInitCodeHash)
console.log('vaultAddress', owner, s.factory, s.vaultInitCodeHash)
if( !owner )
return null
const salt = ethers.solidityPackedKeccak256(['address','uint8'],[owner,num])

View File

@@ -86,16 +86,91 @@ export function newTranche({
}
}
// enum Exchange {
// UniswapV2,
// UniswapV3
// }
export const Exchange = {
UniswapV2: 0,
UniswapV3: 1,
}
export function sqrtX96(value) {
return BigInt(Math.round(Math.sqrt(value * 2 ** (96*2))))
export const OrderState = {
Signing: -1,
Unknown: 0,
Open: 1,
Canceled: 2,
Filled: 3,
Expired: 4,
Underfunded: 5,
}
export function parseOrderStatus(status) {
let [
order,
state,
start,
ocoGroup,
filledIn,
filledOut,
trancheFilledIn,
trancheFilledOut,
] = status
order = parseOrder(order)
filledIn = BigInt(filledIn)
filledOut = BigInt(filledOut)
trancheFilledIn = trancheFilledIn.map((f)=>BigInt(f))
trancheFilledOut = trancheFilledOut.map((f)=>BigInt(f))
return {
order, state, start, ocoGroup, filledIn, filledOut, trancheFilledIn, trancheFilledOut,
}
}
export function parseOrder(order) {
let [
tokenIn,
tokenOut,
route,
amount,
minFillAmount,
amountIsInput,
outputDirectlyToOwner,
chainOrder,
tranches,
] = order
route = parseRoute(route)
amount = BigInt(amount)
minFillAmount = BigInt(minFillAmount)
tranches = tranches.map(parseTranche)
return {
tokenIn, tokenOut, route, amount, minFillAmount, amountIsInput, outputDirectlyToOwner, chainOrder, tranches
}
}
export function parseRoute(route) {
let [exchange, fee] = route
return {exchange, fee} // todo enum?
}
export function parseTranche(tranche) {
let [
fraction,
startTimeIsRelative,
endTimeIsRelative,
minIsBarrier,
maxIsBarrier,
marketOrder,
_reserved5,
_reserved6,
_reserved7,
_reserved8,
_reserved16,
startTime,
endTime,
minIntercept,
minSlope,
maxIntercept,
maxSlope,
] = tranche
return {
fraction, startTimeIsRelative, endTimeIsRelative, minIsBarrier, maxIsBarrier, marketOrder,
startTime, endTime, minIntercept, minSlope, maxIntercept, maxSlope,
}
}

View File

@@ -6,6 +6,10 @@ import {ethers} from "ethers";
// synchronous version may return null but will trigger a lookup
export function token(addr) {
if( !addr ) {
// console.log('ignoring call to token', addr)
return null
}
const s = useStore()
if( !(addr in s.tokens) ) {
getToken(addr)
@@ -23,35 +27,48 @@ export async function getToken(addr) {
return s.tokens[addr]
}
const _inFlightLookups = {}
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,
}
if( !addr ) {
console.log('ignoring call to add extra token', addr)
return
}
if( !_inFlightLookups[addr] ) {
_inFlightLookups[addr] = true
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', addr, 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)
}
else {
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
const result = await prom
delete _inFlightLookups[addr]
return result
}
}

View File

@@ -96,6 +96,7 @@ let pendingOrders = []
function discoverVaults(owner) {
const s = useStore()
console.log('discoverVaults', owner)
if( owner === null )
s.vaults = []
else
@@ -112,6 +113,7 @@ function discoverVaults(owner) {
}
async function _discoverVaults(owner) {
console.log('_discoverVaults',owner)
const result = []
// todo multi-vault scan
const num = 0
@@ -174,6 +176,7 @@ export async function pendOrder(order) {
export async function cancelOrder(vault, orderIndex) {
console.log('cancel order', vault, orderIndex)
pendTransaction(async (signer)=> {
const contract = contractOrNull(vault, vaultAbi, signer)
if( contract === null ) {

View File

@@ -2,11 +2,9 @@
<v-table>
<thead>
<tr>
<th class="num d-none d-md-table-cell">#</th> <!-- todo placement date instead -->
<th class="token d-none d-sm-table-cell">Buy</th>
<th class="token d-none d-sm-table-cell">Sell</th>
<th class="token d-sm-none">Pair</th>
<th class="amount d-none d-md-table-cell">Amount</th>
<th class="num d-none d-md-table-cell">Date</th> <!-- todo placement date instead -->
<th class="token d-none d-sm-table-cell">Input</th>
<th class="token d-none d-sm-table-cell">Output</th>
<th class="amount">Remaining</th>
<th class="amount">Filled</th>
<th class="amount d-none d-md-table-cell">Avg&nbsp;Price</th>
@@ -15,33 +13,32 @@
</tr>
</thead>
<tbody>
<tr v-for="[index, inTokenAddr, outTokenAddr, amount, amountTokenAddr, filled, avgPrice, state] in orders">
<td class="d-none d-md-table-cell" style="width: 1em">{{parseInt(index)+1}}</td>
<td class="token d-none d-sm-table-cell">{{tokenSymbol(inTokenAddr)}}</td>
<td class="token d-none d-sm-table-cell">{{tokenSymbol(outTokenAddr)}}</td>
<td class="pair d-sm-none">Buy {{tokenSymbol(inTokenAddr)}}<br/>Sell {{tokenSymbol(outTokenAddr)}}</td>
<td class="amount d-none d-md-table-cell">{{tokenAmount(amountTokenAddr, amount)}}</td>
<td class="amount">{{tokenAmount(amountTokenAddr, amount-filled)}}</td>
<td class="amount">{{tokenAmount(amountTokenAddr, filled)}}</td>
<td class="amount d-none d-md-table-cell">
{{pairPrice(inTokenAddr, outTokenAddr, vaultAddr, index, avgPrice)}}
<btn v-if="pairPrice(inTokenAddr, outTokenAddr, vaultAddr, index, avgPrice)!==''" size="small"
<tr v-for="st of orders">
<td class="">{{st.start}}</td>
<td class="amount"><suspense><token-amount :addr="st.order.tokenIn" :amount="st.order.amountIsInput ? st.order.amount : null"/></suspense></td>
<td class="amount"><suspense><token-amount :addr="st.order.tokenOut" :amount="!st.order.amountIsInput ? st.order.amount : null"/></suspense></td>
<td class="amount"><suspense><token-amount :addr="st.amountToken" :amount="st.order.amount-st.filled"/></suspense></td>
<td class="amount"><suspense><token-amount :addr="st.amountToken" :amount="st.filled"/></suspense></td>
<td class="amount">
{{pairPrice(st.order.tokenIn, st.order.tokenOut, vaultAddr, st.index, st.avg)}}
<btn v-if="pairPrice(st.order.tokenIn, st.order.tokenOut, vaultAddr, st.index, st.avg)!==''" size="small"
variant="plain"
@click="inverted[[vaultAddr,index]] = !inverted[[vaultAddr,index]]">
{{pair(inTokenAddr, outTokenAddr, vaultAddr,index)}}
@click="inverted[[vaultAddr,st.index]] = !inverted[[vaultAddr,st.index]]">
{{pair(st.order.tokenIn, st.order.tokenOut, vaultAddr, st.index)}}
</btn>
</td>
<td class="status">
<v-chip v-if="state===-1" prepend-icon="mdi-signature" color="yellow">Signing</v-chip>
<v-chip v-if="state===0" class="d-none d-lg-inline-flex" prepend-icon="mdi-dots-horizontal" color="green">Open</v-chip>
<btn v-if="state===0" class="d-none d-sm-inline-flex d-lg-none" icon="mdi-cancel" color="red" @click="cancelOrder(vaultAddr,index)">Cancel</btn>
<btn v-if="state===0" class="d-sm-none" variant="plain" icon="mdi-cancel" color="red" @click="cancelOrder(vaultAddr,index)"/>
<v-chip v-if="state===1" prepend-icon="mdi-cancel" color="red">Canceled</v-chip>
<v-chip v-if="state===2" prepend-icon="mdi-check-circle-outline" color="green">Completed</v-chip>
<v-chip v-if="state===3" prepend-icon="mdi-progress-check" color="grey-darken-1">Expired</v-chip>
<v-chip v-if="st.state===OrderState.Signing" prepend-icon="mdi-signature" color="yellow">Signing</v-chip>
<v-chip v-if="st.state===OrderState.Open" class="d-none d-lg-inline-flex" prepend-icon="mdi-dots-horizontal" color="green">Open</v-chip>
<btn v-if="st.state===OrderState.Open" class="d-none d-sm-inline-flex d-lg-none" icon="mdi-cancel" color="red" @click="cancelOrder(vaultAddr,st.index)">Cancel</btn>
<btn v-if="st.state===OrderState.Open" class="d-sm-none" variant="plain" icon="mdi-cancel" color="red" @click="cancelOrder(vaultAddr,st.index)"/>
<v-chip v-if="st.state===OrderState.Canceled" prepend-icon="mdi-cancel" color="red">Canceled</v-chip>
<v-chip v-if="st.state===OrderState.Filled" prepend-icon="mdi-check-circle-outline" color="green">Completed</v-chip>
<v-chip v-if="st.state===OrderState.Expired" prepend-icon="mdi-progress-check" color="grey-darken-1">Expired</v-chip>
<v-chip v-if="st.state===OrderState.Underfunded" prepend-icon="mdi-alert" color="warning">Underfunded</v-chip>
</td>
<td class="cancel d-none d-lg-table-cell">
<btn v-if="state===0" icon="mdi-cancel" color="red" @click="cancelOrder(vaultAddr,index)">Cancel</btn>
<btn v-if="st.state===OrderState.Open" icon="mdi-cancel" color="red" @click="cancelOrder(vaultAddr,st.index)">Cancel</btn>
</td>
</tr>
</tbody>
@@ -51,10 +48,13 @@
<script setup>
import {FixedNumber} from "ethers";
import {useStore} from "@/store/store";
import {computed, reactive} from "vue";
import {computed, defineAsyncComponent, reactive} from "vue";
import {token} from "@/blockchain/token.js";
import Btn from "@/components/Btn.vue"
import {cancelOrder} from "@/blockchain/wallet.js";
import {intervalString, dateString} from "@/misc.js";
import {OrderState} from "@/blockchain/orderlib.js";
const TokenAmount = defineAsyncComponent(()=>import('./TokenAmount.vue'))
const s = useStore()
const props = defineProps(['vault'])
@@ -66,15 +66,20 @@ function tokenSymbol(addr) {
return t ? t.symbol : addr
}
/*
function tokenAmount(tokenAddr, amount) {
if( !tokenAddr || !amount )
return ''
console.log('token amount', tokenAddr, amount)
if( typeof tokenAddr !== 'string' )
throw Error('broke')
const t = token(tokenAddr)
if( !t )
return ''
// console.log('tokenAmount amount', typeof amount, amount)
// console.log('tokenAmount decimals', typeof t.decimals, t.decimals)
const amt = FixedNumber.fromValue(amount, t.decimals, {width:256, decimals:t.decimals, signed:false})
return `${amt} ${t.symbol}`
}
*/
// todo create a Price component that keeps inversion flags in the store and defaults to stablecoins as the quote
function pairPrice(inTokenAddr, outTokenAddr, vaultAddr, index, price) {
@@ -110,43 +115,94 @@ function pair(inTokenAddr, outTokenAddr, vaultAddr, index) {
}
const orders = computed(()=>{
// example twap
// status = [
// [ order
// "0x52412507302F6bAB17f56370b6a8F304CbB30Ce1", in token
// "0x63e187162a4c33A4D14465eA3859fFe423647710", out token
// [1, 500], route
// "100000000", amount
// "1000000", min amount
// false, amount is input
// false, output to owner
// 18446744073709552000, chain order
// [ tranches
// [21845, fraction
// true, start time relative
// true, end time relative
// false, min is barrier
// false, max is barrier
// false, market order
// false, reserved
// false, reserved
// false, reserved
// 0, reserved
// 0, reserved
// 0, start time
// 20, end time
// 730643660, min intercept
// 0, min slope
// 0, max intercept
// 0 max slope
// ],
// [...],
// [...],
// ]
// ],
// 4, state
// 1701809053, started at
// null, oco group
// "0", filled in
// "0", filled out
// ["0", "0", "0"], tranche filled in
// ["0", "0", "0"] tranche filled out
// ]
const result = []
// for( const [status] of pendingOrders.reverse() ) {
// console.log('adding pended order')
// const inTokenAddr = status[0]
// const outTokenAddr = status[1]
// const amountIsInput = !!(status[4])
// const amountTokenAddr = amountIsInput ? inTokenAddr : outTokenAddr
// const amount = 0
// const filled = 0
// const avg = ''
// const state = -1 // pending
// result.push(['...', inTokenAddr, outTokenAddr, amount, amountTokenAddr, filled, avg, state])
// }
if( vaultAddr.value in s.orders ) {
for (const [index, status] of Object.entries(s.orders[vaultAddr.value]).reverse()) {
// console.log('order status', status)
// [index, symbolA, symbolB, amount, amountSymbol, filled]
const inTokenAddr = status[0][0]
const outTokenAddr = status[0][1]
const amountIsInput = !!(status[0][4])
const amountTokenAddr = amountIsInput ? inTokenAddr : outTokenAddr
// console.log('getamount', status[0][3])
const amount = BigInt(status[0][3])
// console.log('amount', amount)
// console.log('getfilled', amountIsInput ? status[4] : status[5])
const filled = BigInt(amountIsInput ? status[4] : status[5])
// console.log('filled', filled)
const amountIn = BigInt(status[4])
const amountOut = BigInt(status[5])
const st = {...status}
console.log('order status', st)
const o = {...st.order}
st.order = o
result.push(st)
st.index = parseInt(index)
o.tranches = o.tranches.map((tranche)=>{
const t = {...tranche}
t.startTime = t.startTimeIsRelative ? intervalString(t.startTime) : dateString(t.startTime)
t.endTime = t.endTimeIsRelative ? intervalString(t.endTime) : dateString(t.endTime)
return t
})
st.start = dateString(st.start)
const fmtX18 = {decimals: 18, width: 256, signed: false};
const avg = !amountIn || !amountOut ? null :
FixedNumber.fromValue(status[5], 0, fmtX18).div(FixedNumber.fromValue(status[4], 0, fmtX18))
const state = status[1]
result.push([index, inTokenAddr, outTokenAddr, amount, amountTokenAddr, filled, avg, state])
st.filled = st.amountIsInput ? st.filledIn : st.filledOut
st.avg = BigInt(st.filled) === 0n ? null :
FixedNumber.fromValue(status.filledOut, 0, fmtX18)
.div(FixedNumber.fromValue(status.filledIn, 0, fmtX18))
.toUnsafeFloat().toPrecision(5) // todo precision
st.trancheFilled = o.amountIsInput ? st.trancheFilledIn : st.trancheFilledOut
st.amountToken = o.amountIsInput ? o.tokenIn : o.tokenOut
// // [index, symbolA, symbolB, amount, amountSymbol, filled]
// const inTokenAddr = status[0][0]
// const outTokenAddr = status[0][1]
// const amountIsInput = !!(status[0][4])
// const amountTokenAddr = amountIsInput ? inTokenAddr : outTokenAddr
// // console.log('getamount', status[0][3])
// const amount = BigInt(status[0][3])
// // console.log('amount', amount)
// // console.log('getfilled', amountIsInput ? status[4] : status[5])
// const filled = BigInt(amountIsInput ? status[4] : status[5])
// // console.log('filled', filled)
// const amountIn = BigInt(status[4])
// const amountOut = BigInt(status[5])
// const avg = !amountIn || !amountOut ? null :
// FixedNumber.fromValue(status[5], 0, fmtX18).div(FixedNumber.fromValue(status[4], 0, fmtX18))
// const state = status[1]
// result.push([index, inTokenAddr, outTokenAddr, amount, amountTokenAddr, filled, avg, state])
}
}
// console.log('result', result)
console.log('orders', result)
return result
})

View File

@@ -0,0 +1,27 @@
<template>
<span>{{fmtAmount}} {{ token.symbol || '' }}</span>
</template>
<script setup>
import {useStore} from "@/store/store";
import {getToken} from "@/blockchain/token.js";
import {FixedNumber} from "ethers";
import {computed, ref} from "vue";
const s = useStore()
const props = defineProps(['addr', 'amount'])
const token = await getToken(props.addr)
const fmtAmount = computed(() => {
if( props.amount === null || props.amount === undefined )
return ''
return FixedNumber.fromValue(props.amount, token.decimals, {
width: 256,
decimals: token.decimals
}).toUnsafeFloat().toPrecision(5) // todo precision
})
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;
</style>

View File

@@ -36,7 +36,6 @@ const fixed = computed(() => FixedNumber.fromValue(props.amount, token.decimals,
decimals: token.decimals
}))
const imageSrc = computed(() => token.image)
const withdrawing = ref(false)
</script>
<style scoped lang="scss">

View File

@@ -83,10 +83,8 @@ import {computed, defineAsyncComponent, ref} from "vue";
import {vaultAddress} from "@/blockchain/contract.js";
import {ensureVault} from "@/blockchain/wallet.js";
import CopyButton from "@/components/CopyButton.vue";
import NeedsProvider from "@/components/NeedsProvider.vue";
import Withdraw from "@/components/Withdraw.vue";
import PhoneCard from "@/components/PhoneCard.vue";
import Btn from "@/components/Btn.vue";
const TokenRow = defineAsyncComponent(()=>import('./TokenRow.vue'))
const s = useStore()

View File

@@ -59,3 +59,21 @@ export function routeInverted(route) {
return route && (route.token0 === s.tokenA) === s.inverted
}
export function intervalString(seconds) {
if( seconds < 1 )
return 'now'
else if( seconds < 60 )
return `${seconds} seconds`
else if( seconds < 3600 )
return `${(seconds/60).toFixed(1)} minutes`
else if( seconds < 86400 )
return `${(seconds/3600).toFixed(1)} hours`
else
return `${(seconds/86400).toFixed(1)} days`
}
const _dateFormat = new Intl.DateTimeFormat( undefined, {dateStyle: 'medium', timeStyle: 'short'})
export function dateString(seconds) {
const date = new Date(seconds*1000)
return _dateFormat.format(date)
}

View File

@@ -3,6 +3,7 @@ import {useStore} from "@/store/store.js";
import {flushOrders, onChainChanged} from "@/blockchain/wallet.js";
import {ethers} from "ethers";
import {applyFills} from "@/blockchain/common.js";
import {parseOrderStatus} from "@/blockchain/orderlib.js";
export const socket = io(import.meta.env.VITE_WS_URL || undefined, {transports: ["websocket"]})
@@ -62,13 +63,16 @@ function handleOrderStatus(chainId, vault, orderIndex, status) {
const s = useStore()
if( s.chainId.value !== chainId )
return
console.log('o', chainId, vault, orderIndex, status)
// message 'o' is a single order status
const parsed = parseOrderStatus(status);
console.log('o', chainId, vault, orderIndex, status, parsed)
if( !(vault in s.orders) )
s.orders[vault] = {}
s.orders[vault][orderIndex] = status
s.orders[vault][orderIndex] = parsed
}
socket.on('os', (chainId, vault, orders) => {
// message 'os' has multiple order statuses
console.log('os', orders)
for( const [orderIndex, status] of orders )
handleOrderStatus(chainId, vault, orderIndex, status)

View File

@@ -97,14 +97,13 @@ export const useStore = defineStore('app', ()=> {
this.errors = result
}
function addToken(chainId, info) {
this.$patch(() => {
let extras = extraTokens[chainId]
if (extras === undefined) {
extras = {}
extraTokens[chainId] = extras
}
extras[info.address] = info
})
let extras = this.extraTokens[chainId]
if (extras === undefined) {
extras = {}
this.extraTokens[chainId] = extras
}
extras[info.address] = info
this.extraTokens = extras
}
return {
@@ -165,4 +164,4 @@ export const useOrderStore = defineStore('order', ()=> {
interval, intervalIsTotal, timeUnitIndex, routes, routesPending, validOrder, route, base, quote, pairSymbol,
limitIsMinimum, amountToken, amountIsInput, setDefaultTokens, totalAmount, trancheAmount,
}
})
})