complete store refactor; moved form inputs into store; refactored components out of TimedOrderEntry

This commit is contained in:
Tim Olson
2023-11-25 16:21:42 -04:00
parent c3f05deff1
commit 094108d806
13 changed files with 192 additions and 174 deletions

View File

@@ -4,7 +4,6 @@ import {useStore} from "@/store/store.js";
export function vaultAddress( owner, num=0) {
console.log('va', owner, num)
if( !owner )
return null
const s = useStore()

View File

@@ -15,10 +15,10 @@ export function subPrices( routes ) {
const subRoutes = []
let chainId = null
for( const route of routes ) {
console.log('sub route', route, subscriptionCounts)
// console.log('sub route', route, subscriptionCounts)
if( !(route in subscriptionCounts) || subscriptionCounts[route] === 0 ) {
subscriptionCounts[route] = 1
console.log('subscribing to pool', route.pool)
// console.log('subscribing to pool', route.pool)
subRoutes.push(route)
}
else {
@@ -33,9 +33,7 @@ export function subPrices( routes ) {
// perform a local query if necessary
for( const route of subRoutes ) {
const s = useStore()
console.log('route in prices?', route.pool in s.poolPrices, route.pool, s.poolPrices)
if( !(route.pool in s.poolPrices) ) {
console.log('querying initial route price', route.pool)
getPriceForRoute(route).then((price)=>s.poolPrices[route.pool]=price)
}
}
@@ -46,7 +44,7 @@ export function unsubPrices( routes ) {
let chainId = null
const unsubAddrs = []
for( const route of routes ) {
console.log('unsub route', route, subscriptionCounts)
// console.log('unsub route', route, subscriptionCounts)
if( !(route in subscriptionCounts) ) {
console.error('unsubscribed to a nonexistent route', route)
}
@@ -56,7 +54,7 @@ export function unsubPrices( routes ) {
unsubAddrs.push(route.pool)
if( chainId !== null && route.chainId !== chainId )
throw Error('cannot mix chainIds in a subscription list')
console.log('unsubscribing from pool', route.pool)
// console.log('unsubscribing from pool', route.pool)
chainId = route.chainId
}
else if( subscriptionCounts[route] < 0 ) {
@@ -71,7 +69,6 @@ export function unsubPrices( routes ) {
async function getPriceForRoute(route) {
console.log('route is',route)
if( route.exchange === Exchange.UniswapV3 ) {
const addr = uniswapV3PoolAddress(route.chainId, route.token0.address, route.token1.address, route.fee)
const store = useStore();
@@ -88,7 +85,7 @@ async function getPriceForRoute(route) {
price = FixedNumber.fromValue(price,0,WIDE_PRICE_FORMAT)
price = price.div(FixedNumber.fromValue(2n**(96n*2n),0,WIDE_PRICE_FORMAT))
price = price.round(18).toString()
console.log(`price for ${route.token0.symbol}/${route.token1.symbol}`,price)
// console.log(`price for ${route.token0.symbol}/${route.token1.symbol}`,price)
store.poolPrices[addr] = price
return price
}

View File

@@ -10,7 +10,7 @@ export async function findRoute(tokenA, tokenB) {
const chainId = useStore().chainId
const rawRoutes = await helper.getRoutes(tokenA.address, tokenB.address)
// todo expose all available pools
console.log('raw routes', rawRoutes)
// console.log('raw routes', rawRoutes)
let result = null // we actually only find a single pool for now
for (let [exchange, fee, pool] of rawRoutes) {
exchange = Number(exchange)

View File

@@ -18,7 +18,6 @@ export function uniswapV3PoolAddress(chainId, tokenAddrA, tokenAddrB, fee) {
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(['address', 'address', 'uint24'], [addr0, addr1, fee]);
const salt = ethers.keccak256(encoded)
const factory = uniswapV3Addresses[chainId]?.factory
console.log('uni3addr', addr0, addr1, fee, salt, factory)
if (!factory) {
console.log('no uniswap factory for chain', chainId)
return null

View File

@@ -1,5 +1,5 @@
import {ethers} from "ethers";
import {setProvider, useStore} from "@/store/store";
import {useStore} from "@/store/store";
import {socket} from "@/socket.js";
import {contractOrNull, vaultAddress} from "@/blockchain/contract.js";
import {vaultAbi} from "@/blockchain/abi.js";
@@ -13,7 +13,7 @@ export function onChainChanged(chainId) {
store.chainId = chainId // touch the chainId last. will cause any clients of the store's provider getter to refresh
store.account = null
const provider = new ethers.BrowserProvider(window.ethereum, chainId);
setProvider(provider, chainId)
store.provider = provider
provider.listAccounts().then((accounts)=>changeAccounts(accounts.map((a)=>a.address)))
}
}
@@ -30,12 +30,10 @@ function changeAccounts(accounts) {
const addr = accounts[0]
const store = useStore()
store.account = addr
console.log('set store.account to', addr, store.account)
discoverVaults()
flushTransactions()
socket.emit('address', store.chainId, addr)
}
console.log('changeAccounts ended')
}
function onAccountsChanged(accounts) {
@@ -102,7 +100,6 @@ function discoverVaults() {
s.vaults = []
else
_discoverVaults(owner).then((result)=>{
console.log('read store.account', s.account)
if( s.account === owner ) { // double-check the account since it could have changed during our await
s.vaults = result
if( pendingOrders.length )

26
src/components/Amount.vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
v-model="s.amount" :rules="[validateRequired,validateAmount]" v-auto-select>
<template v-slot:append-inner>
<v-btn @click="s.amountIsTokenA=!s.amountIsTokenA" variant="outlined" class="mr-2">
{{ s.amountIsTokenA ? s.tokenA.symbol : s.tokenB.symbol }}
</v-btn>
<v-btn :text="s.amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
@click="s.amountIsTotal=!s.amountIsTotal" class="total"/>
</template>
</v-text-field>
</template>
<script setup>
import {useStore} from "@/store/store";
import {validateRequired, validateAmount, vAutoSelect} from "@/misc.js";
const s = useStore()
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;
</style>

View File

@@ -44,7 +44,7 @@ const tokenA = computed({
},
set(value) {
if( !s.tokenA || s.tokenA.address !== value.address ) {
s.tokenA.value = value
s.tokenA = value
routeFinder.invoke()
}
}
@@ -64,25 +64,25 @@ const tokenB = computed({
const routes = computed({
get() {
return s.routes.value
return s.routes
},
set(value) {
console.log('setting new routes', s.routes.value, value)
s.routes.value = value
console.log('setting new routes', s.routes, value)
s.routes = value
}
})
async function componentFindRoute() {
const tokenA = s.tokenA
const tokenB = s.tokenB
console.log('finding route', tokenA, tokenB)
// console.log('finding route', tokenA, tokenB)
s.routes = []
if (!tokenA || !tokenB)
return
s.routesPending = true
try {
const result = await findRoute(tokenA, tokenB)
console.log('found route', result)
// console.log('found route', result)
s.routes = result
}
catch (e) {

View File

@@ -21,7 +21,7 @@ const price = computed(()=>{
if( !route || !(route.pool in s.poolPrices) )
return ''
let p = s.poolPrices[route.pool]
console.log('pool price is',typeof p, p)
// console.log('pool price is',typeof p, p)
if( !p )
return ''
p = FixedNumber.fromString(p, WIDE_PRICE_FORMAT).toUnsafeFloat()

View File

@@ -6,26 +6,13 @@
<v-card-item>
<pair-choice/>
<div v-if="s.route && !s.routesPending">
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
v-model="amount" :rules="[validateRequired,validateAmount]" v-auto-select>
<template v-slot:append-inner>
<v-btn @click="amountIsTokenA=!amountIsTokenA" variant="outlined" class="mr-2">
{{ amountIsTokenA ? s.tokenA.symbol : s.tokenB.symbol }}
</v-btn>
<v-btn :text="amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
@click="amountIsTotal=!amountIsTotal" class="total"/>
</template>
</v-text-field>
<amount/>
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255"
v-model="tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
<!-- <template v-slot:prepend-inner>-->
<!-- <div>{{ amountIsTotal ? 'Split into' : 'Times' }}</div>-->
<!-- </template>-->
<template v-slot:append-inner>tranches</template>
</v-text-field>
<v-text-field type="number" variant="outlined" :min="1" v-model="interval" class="interval"
:label="intervalIsTotal ? 'Completion time' : 'Time between tranches'" v-auto-select>
<!-- <template v-slot:append>APART</template>-->
<template v-slot:prepend-inner>
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within mr-2"
@click="intervalIsTotal=!intervalIsTotal"/>
@@ -34,11 +21,11 @@
<v-btn variant="outlined" :text="timeUnits[timeUnitIndex]" @click="toggleTimeUnits" class="time-units"/>
</template>
</v-text-field>
<v-text-field v-model="limitPrice" :label="(limitIsMinimum?'Minimum':'Maximum')+' Price'" type="number"
<v-text-field v-model="s.limitPrice" :label="(s.limitIsMinimum?'Minimum':'Maximum')+' Price'" type="number"
variant="outlined" aria-valuemin="0" min="0"
clearable :rules="[validateAmount, validateMin]" v-auto-select>
<template v-slot:append-inner>
<v-btn variant="outlined" @click="inverted=!inverted">
<v-btn variant="outlined" @click="s.inverted=!s.inverted">
{{s.pairSymbol}}
</v-btn>
</template>
@@ -64,9 +51,10 @@
import {useStore} from "@/store/store";
import {computed, ref} from "vue";
import PhoneCard from "@/components/PhoneCard.vue";
import Amount from "@/components/Amount.vue"
// noinspection ES6UnusedImports
import {routeInverted, SingletonCoroutine, vAutoSelect} from "@/misc.js";
import {newLimitConstraint, newOrder, newTimeConstraint, sqrtX96, TimeMode} from "@/blockchain/orderlib.js";
import {isEmpty, routeInverted, SingletonCoroutine, vAutoSelect, validateRequired, validateAmount} from "@/misc.js";
import {newLimitConstraint, newOrder, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js";
import {FixedNumber} from "ethers";
import {pendOrder} from "@/blockchain/wallet.js";
import NeedsProvider from "@/components/NeedsProvider.vue";
@@ -76,21 +64,14 @@ import PairChoice from "@/components/PairChoice.vue";
const s = useStore()
const amount = ref(100) // todo 0
const amountIsTokenA = ref(false)
const amountIsTotal = ref(true)
const tranches = ref(3)
const inverted = ref(false)
const minPrice = ref(null)
const maxPrice = ref(null)
const limitPrice = ref(null)
const interval = ref(1)
const intervalIsTotal = ref(true)
const timeUnits = ['minutes', 'hours', 'days']
const timeUnitIndex = ref(0)
const limitIsMinimum = computed(() => !(s.buy ^ s.inverted))
const validOrder = computed(()=>amount.value > 0 && s.routes.length > 0 )
const validOrder = computed(()=>s.validOrder)
function toggleTimeUnits() {
timeUnitIndex.value++
@@ -99,18 +80,6 @@ function toggleTimeUnits() {
}
function isEmpty(v) {
return v === null || typeof v === 'string' && v.trim() === ''
}
function validateRequired(v) {
if (isEmpty(v))
return 'Required'
return true
}
function validateTranches(v) {
const i = parseInt(v)
if (parseFloat(v) !== i)
@@ -123,17 +92,6 @@ function validateTranches(v) {
}
function validateAmount(v) {
if (isEmpty(v))
return true
const floatRegex = /^-?\d*(?:[.,]\d*?)?$/
if (!floatRegex.test(v))
return 'Amount must be a number'
if (parseFloat(v) <= 0)
return 'Amount must be positive'
return true
}
function validateMax(v) {
if (!isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value))
return 'Must be greater than the minimum price'
@@ -152,9 +110,7 @@ function placeOrder() {
const tokenIn = s.buy ? tb.address : ta.address
const tokenOut = s.buy ? ta.address : tb.address
const route = s.route
const amountToken = amountIsTokenA.value ? ta : tb
const amt = FixedNumber.fromString(amount.value.toString(), {decimals: amountToken.decimals}).value
const amountIsInput = amountIsTokenA.value !== s.buy
const amt = FixedNumber.fromString(s.amount.toString(), {decimals: s.amountToken.decimals}).value
// build tranches
const n = tranches.value // num tranches
@@ -173,20 +129,18 @@ function placeOrder() {
const ceil = oneHundredPercent % BigInt(n) ? 1n : 0n
const amtPerTranche = oneHundredPercent / BigInt(n) + ceil
duration -= 15 // subtract 15 seconds so the last tranche completes before the deadline
console.log('duration', duration)
let priceConstraint = null
if( limitPrice.value ) {
if( s.limitPrice ) {
const inverted = routeInverted(route)
const isAbove = limitIsMinimum.value ^ inverted
const isAbove = s.limitIsMinimum ^ inverted
const isRatio = false // todo ratios
const decimals = 10 ** (s.tokenA.decimals - s.tokenB.decimals)
const limit = inverted ? decimals/limitPrice.value : limitPrice.value/decimals
priceConstraint = !limitPrice.value ? null : newLimitConstraint(isAbove, isRatio, limit)
const limit = inverted ? decimals/s.limitPrice : s.limitPrice/decimals
priceConstraint = !s.limitPrice ? null : newLimitConstraint(isAbove, isRatio, limit)
}
for (let i = 0; i < n; i++) {
const start = Math.floor(i * (duration / Math.max((n - 1), 1)))
const end = start + window
console.log('tranche window', start, end, (end-start)/60)
const cs = [newTimeConstraint(TimeMode.SinceOrderStart, start, TimeMode.SinceOrderStart, end)]
if( priceConstraint !== null )
cs.push(priceConstraint)

View File

@@ -4,7 +4,7 @@ const known_chains = [
{
name: 'Arbitrum One',
id: 42161,
image: null,
image: '/arbitrum-logo.svg',
}
]

View File

@@ -58,3 +58,24 @@ export function routeInverted(route) {
const s = useStore()
return route && (route.token0 === s.tokenA) === s.inverted
}
export function isEmpty(v) {
return v === null || typeof v === 'string' && v.trim() === ''
}
export function validateRequired(v) {
if (isEmpty(v))
return 'Required'
return true
}
export function validateAmount(v) {
if (isEmpty(v))
return true
const floatRegex = /^-?\d*(?:[.,]\d*?)?$/
if (!floatRegex.test(v))
return 'Amount must be a number'
if (parseFloat(v) <= 0)
return 'Amount must be positive'
return true
}

View File

@@ -17,18 +17,10 @@ socket.on('disconnect', () => {
socket.on('welcome', async (data) => {
console.log('welcome', data)
const mockCoins = data['chainInfo'][31337].mockCoins
console.log('coin order:',
mockCoins[1] > mockCoins[0] ? "coin1 > coin0" : "coin0 > coin1 (inverted)",
mockCoins)
const s = useStore()
// todo put the vaultInitCodeHash into the chainInfo
s.chainInfo = data.chainInfo
s.vaultInitCodeHash = data.vaultInitCodeHash
// set default tokens in pair choice dropdown
if( s.tokenA === null && Object.values(s.tokens).length >= 1 )
s.tokenA = Object.values(s.tokens)[0]
if( s.tokenB === null && Object.values(s.tokens).length >= 2 )
s.tokenB = Object.values(s.tokens)[1]
const p = new ethers.BrowserProvider(window.ethereum)
const network = await p.getNetwork()
if (network !== null)

View File

@@ -3,90 +3,123 @@ import { defineStore } from 'pinia'
import {knownTokens} from "@/knownTokens.js";
import {computed, ref} from "vue";
let rawProvider = null
let rawProviderChainId = null
export function setProvider( provider, chainId ) {
rawProvider = provider
rawProviderChainId = chainId
}
export const useStore = defineStore('app', ()=> {
const _chainId = ref(null)
const _chainInfo = ref({})
const tokenA = ref(null)
const tokenB = ref(null)
export const useStore = defineStore('app', {
state: () => ({
chainId: null,
chainInfo: {},
vaultInitCodeHash: null,
account: null,
vaults: [],
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
orders: {}, // indexed by vault, value is another dictionary with orderIndex as key and order status values
function getTokenList() {
const chains = _chainId.value in _chainInfo.value && _chainInfo.value[_chainId.value].tokens !== undefined ?
_chainInfo.value[_chainId.value].tokens : []
let known = knownTokens[_chainId.value]
known = known ? Object.values(known) : []
let extras = extraTokens[_chainId.value]
extras = extras ? Object.values(extras) : []
return [...chains, ...known, ...extras]; // put chains first so the Mockcoin pool is automatically selected
}
function getTokens() {
const result = {}
const all = getTokenList();
for (const token of all)
result[token.address] = token
return result
}
function setDefaultTokens() {
const tokens = getTokenList()
if( tokens.length > 0 )
tokenA.value = tokens[0]
if( tokens.length > 1 )
tokenB.value = tokens[1]
}
const chainId = computed({
get() {return _chainId},
set(v) {_chainId.value=v; setDefaultTokens()}
})
const chainInfo = computed({
get() {return _chainInfo},
set(v) {_chainInfo.value=v; setDefaultTokens()}
})
const chain = computed(() => !_chainId.value ? null : (_chainInfo.value[_chainId.value] || null))
// making the provider directly reactive causes exceptions (calling private method...) when calling provider
// functions, so we use a separate ref to signal changes
let _provider = null
const _providerTouch = ref(false)
const provider = computed({
get() {_providerTouch.value; return _provider},
set(v) {_provider=v; _providerTouch.value = !_providerTouch.value}
})
const vaultInitCodeHash = ref(null)
const account = ref(null)
const vaults = ref([])
const transactionSenders = ref([]) // a list of function(signer) that send transactions
const errors = ref([])
const extraTokens = ref({})
const poolPrices = ref({})
const vaultBalances = ref({}) // indexed by vault addr then by token addr. value is an int
const orders = ref({}) // indexed by vault value is another dictionary with orderIndex as key and order status values
// Order Input Forms
tokenA: null,
tokenB: null,
routes: [],
routesPending: false,
inverted: false,
}),
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 : []
let known = knownTokens[s.chainId]
known = known ? Object.values(known) : []
let extras = s.extraTokens[s.chainId]
extras = extras ? Object.values(extras) : []
const result = {}
const all = [...chains, ...known, ...extras]; // put chains first so the Mockcoin pool is automatically selected
for( const token of all)
result[token.address] = token
return result
},
factory: (s)=>!s.chain?null:s.chain.factory,
helper: (s)=>!s.chain?null:s.chain.helper,
mockenv: (s)=>!s.chain?null:s.chain.mockenv,
mockCoins: (s)=>!s.chain?[]:!s.chain.mockCoins?[]:s.chain.mockCoins,
route: (s)=>s.routes.length===0 ? null : s.routes[0],
pairSymbol: (s)=>s.base?.symbol+'\\'+s.quote?.symbol,
base: (s)=>{
const token = s.inverted ? s.tokenB : s.tokenA
return !token?{}:token
},
quote: (s)=> {
const token = s.inverted ? s.tokenA : s.tokenB
return !token ? {} : token
},
},
actions: {
removeTransactionSender(sender) {
this.transactionSenders = this.transactionSenders.filter((v) => v !== sender)
},
error(title, text, closeable=true) {
this.errors.push({title, text, closeable})
},
closeError(title, text) {
const result = []
this.errors.forEach((i)=>{if(i.title!==title && i.text!==text) result.push(i)})
this.errors = result
},
addToken(chainId, info) {
this.$patch((s) => {
let extras = s.extraTokens[chainId]
if (extras === undefined) {
extras = {}
s.extraTokens[chainId] = extras
}
extras[info.address] = info
})
},
},
// Order Input Forms
// const tokenA = ref(null) // defined at top
// const tokenB = ref(null)
const buy = ref(false)
const inverted = ref(false)
const amount = ref(100) // todo
const amountIsTokenA = ref(false) // todo
const amountIsTotal = ref(true)
const limitPrice = ref(null)
const routes = ref([])
const routesPending = ref(false)
const validOrder = computed(() => amount.value > 0 && routes.value.length > 0)
const vault = computed(() => vaults.value.length === 0 ? null : vaults.value[0])
const tokens = computed(getTokens)
const factory = computed(() => !chain.value ? null : chain.value.factory)
const helper = computed(() => !chain.value ? null : chain.value.helper)
const mockenv = computed(() => !chain.value ? null : chain.value.mockenv)
const mockCoins = computed(() => !chain.value ? [] : !chain.value.mockCoins ? [] : chain.value.mockCoins)
const route = computed(() => routes.value.length === 0 ? null : routes.value[0])
const base = computed(() => {
const token = inverted.value ? tokenB.value : tokenA.value
return !token ? {} : token
})
const quote = computed(() => {
const token = inverted.value ? tokenA.value : tokenB.value
return !token ? {} : token
})
const pairSymbol = computed(() => base.value?.symbol + '\\' + quote.value?.symbol)
const limitIsMinimum = computed(() => !(buy.value ^ inverted.value))
const amountToken = computed(() => amountIsTokenA.value ? tokenA.value : tokenB.value)
const amountIsInput = computed(() => amountIsTokenA.value !== buy.value)
function removeTransactionSender(sender) {
this.transactionSenders = this.transactionSenders.filter((v) => v !== sender)
}
function error(title, text, closeable=true) {
this.errors.push({title, text, closeable})
}
function closeError(title, text) {
const result = []
this.errors.forEach((i)=>{if(i.title!==title && i.text!==text) result.push(i)})
this.errors = result
}
function addToken(chainId, info) {
this.$patch(() => {
let extras = extraTokens[chainId]
if (extras === undefined) {
extras = {}
extraTokens[chainId] = extras
}
extras[info.address] = info
})
}
return {
chainId, chainInfo, chain, provider, vaultInitCodeHash, account, vaults, transactionSenders, errors, extraTokens,
poolPrices, vaultBalances, orders, tokenA, tokenB, routes, routesPending, inverted, amount, amountIsTokenA,
amountIsTotal, limitPrice, validOrder, vault, tokens, factory, helper, mockenv, mockCoins, route,
pairSymbol, base, quote, limitIsMinimum, amountToken, amountIsInput, removeTransactionSender, error, closeError,
addToken,
}
})