major refactor of web store into vue setup style declaration; reactivity debugging; order view has known refresh issues
This commit is contained in:
@@ -4,9 +4,10 @@ import {useStore} from "@/store/store.js";
|
|||||||
|
|
||||||
|
|
||||||
export function vaultAddress( owner, num=0) {
|
export function vaultAddress( owner, num=0) {
|
||||||
|
const s = useStore()
|
||||||
|
// console.log('vaultAddress', owner, s.factory, s.vaultInitCodeHash)
|
||||||
if( !owner )
|
if( !owner )
|
||||||
return null
|
return null
|
||||||
const s = useStore()
|
|
||||||
const salt = ethers.solidityPackedKeccak256(['address','uint8'],[owner,num])
|
const salt = ethers.solidityPackedKeccak256(['address','uint8'],[owner,num])
|
||||||
return ethers.getCreate2Address(s.factory, salt, s.vaultInitCodeHash)
|
return ethers.getCreate2Address(s.factory, salt, s.vaultInitCodeHash)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,25 +16,28 @@ export function subPrices( routes ) {
|
|||||||
let chainId = null
|
let chainId = null
|
||||||
for( const route of routes ) {
|
for( const route of routes ) {
|
||||||
// console.log('sub route', route, subscriptionCounts)
|
// console.log('sub route', route, subscriptionCounts)
|
||||||
if( !(route in subscriptionCounts) || subscriptionCounts[route] === 0 ) {
|
const routeKey = [route.chainId, route.pool]
|
||||||
subscriptionCounts[route] = 1
|
if( !(routeKey in subscriptionCounts) || subscriptionCounts[routeKey] === 0 ) {
|
||||||
// console.log('subscribing to pool', route.pool)
|
subscriptionCounts[routeKey] = 1
|
||||||
subRoutes.push(route)
|
subRoutes.push(route)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
subscriptionCounts[route]++
|
subscriptionCounts[routeKey]++
|
||||||
}
|
}
|
||||||
if( chainId !== null && route.chainId !== chainId )
|
if( chainId !== null && route.chainId !== chainId )
|
||||||
throw Error('cannot mix chainIds in a subscription list')
|
throw Error('cannot mix chainIds in a subscription list')
|
||||||
chainId = route.chainId
|
chainId = route.chainId
|
||||||
}
|
}
|
||||||
if( subRoutes.length ) {
|
if( subRoutes.length ) {
|
||||||
socket.emit('subPools', chainId, routes.map((r)=>r.pool) )
|
const pools = routes.map((r)=>r.pool);
|
||||||
|
// console.log('subscribing to pools', pools)
|
||||||
|
socket.emit('subPools', chainId, pools )
|
||||||
// perform a local query if necessary
|
// perform a local query if necessary
|
||||||
|
const s = useStore()
|
||||||
for( const route of subRoutes ) {
|
for( const route of subRoutes ) {
|
||||||
const s = useStore()
|
const routeKey = [route.chainId, route.pool]
|
||||||
if( !(route.pool in s.poolPrices) ) {
|
if( !(routeKey in s.poolPrices) ) {
|
||||||
getPriceForRoute(route).then((price)=>s.poolPrices[route.pool]=price)
|
getPriceForRoute(route).then((price)=>s.poolPrices[routeKey]=price)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,26 +48,27 @@ export function unsubPrices( routes ) {
|
|||||||
const unsubAddrs = []
|
const unsubAddrs = []
|
||||||
for( const route of routes ) {
|
for( const route of routes ) {
|
||||||
// console.log('unsub route', route, subscriptionCounts)
|
// console.log('unsub route', route, subscriptionCounts)
|
||||||
if( !(route in subscriptionCounts) ) {
|
const routeKey = [route.chainId, route.pool]
|
||||||
|
if( !(routeKey in subscriptionCounts) ) {
|
||||||
console.error('unsubscribed to a nonexistent route', route)
|
console.error('unsubscribed to a nonexistent route', route)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
subscriptionCounts[route]--
|
subscriptionCounts[routeKey]--
|
||||||
if( subscriptionCounts[route] === 0 ) {
|
if( subscriptionCounts[routeKey] === 0 ) {
|
||||||
unsubAddrs.push(route.pool)
|
unsubAddrs.push(route.pool)
|
||||||
if( chainId !== null && route.chainId !== chainId )
|
if( chainId !== null && route.chainId !== chainId )
|
||||||
throw Error('cannot mix chainIds in a subscription list')
|
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
|
chainId = route.chainId
|
||||||
}
|
}
|
||||||
else if( subscriptionCounts[route] < 0 ) {
|
else if( subscriptionCounts[routeKey] < 0 ) {
|
||||||
console.error('unsubscribed to an already unsubbed route', route)
|
console.error('unsubscribed to an already unsubbed route', route)
|
||||||
subscriptionCounts[route] = 0 // fix
|
subscriptionCounts[routeKey] = 0 // fix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if( unsubAddrs.length )
|
if( unsubAddrs.length )
|
||||||
socket.emit('unsubPool', chainId, unsubAddrs )
|
socket.emit('unsubPools', chainId, unsubAddrs )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ async function getPriceForRoute(route) {
|
|||||||
price = price.div(FixedNumber.fromValue(2n**(96n*2n),0,WIDE_PRICE_FORMAT))
|
price = price.div(FixedNumber.fromValue(2n**(96n*2n),0,WIDE_PRICE_FORMAT))
|
||||||
price = price.round(18).toString()
|
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
|
store.poolPrices[[route.chainId,addr]] = price
|
||||||
return price
|
return price
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import {Exchange} from "@/blockchain/orderlib.js";
|
|||||||
import {useStore} from "@/store/store.js";
|
import {useStore} from "@/store/store.js";
|
||||||
|
|
||||||
|
|
||||||
export async function findRoute(tokenA, tokenB) {
|
export async function findRoute(chainId, tokenA, tokenB) {
|
||||||
const helper = await queryHelperContract()
|
const helper = await queryHelperContract()
|
||||||
if (!helper)
|
if (!helper)
|
||||||
throw Error('no helper')
|
throw Error('no helper')
|
||||||
const chainId = useStore().chainId
|
|
||||||
const rawRoutes = await helper.getRoutes(tokenA.address, tokenB.address)
|
const rawRoutes = await helper.getRoutes(tokenA.address, tokenB.address)
|
||||||
// todo expose all available pools
|
// 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
|
let result = null // we actually only find a single pool for now
|
||||||
for (let [exchange, fee, pool] of rawRoutes) {
|
for (let [exchange, fee, pool] of rawRoutes) {
|
||||||
exchange = Number(exchange)
|
exchange = Number(exchange)
|
||||||
|
|||||||
@@ -10,36 +10,37 @@ export function onChainChanged(chainId) {
|
|||||||
const store = useStore()
|
const store = useStore()
|
||||||
if( chainId !== store.chainId ) {
|
if( chainId !== store.chainId ) {
|
||||||
console.log('chain changed', chainId)
|
console.log('chain changed', chainId)
|
||||||
store.chainId = chainId // touch the chainId last. will cause any clients of the store's provider getter to refresh
|
store.chainId = chainId
|
||||||
store.account = null
|
store.account = null
|
||||||
const provider = new ethers.BrowserProvider(window.ethereum, chainId);
|
const provider = new ethers.BrowserProvider(window.ethereum, chainId);
|
||||||
store.provider = provider
|
store.provider = provider
|
||||||
provider.listAccounts().then((accounts)=>changeAccounts(accounts.map((a)=>a.address)))
|
provider.listAccounts().then((accounts)=>changeAccounts(chainId, accounts.map((a)=>a.address)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeAccounts(accounts) {
|
function changeAccounts(chainId, accounts) {
|
||||||
console.log('change accounts', accounts)
|
// console.log('changeAccounts', chainId, accounts)
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
if( accounts.length === 0 ) {
|
if( accounts.length === 0 ) {
|
||||||
|
console.log('account logged out')
|
||||||
store.account = null
|
store.account = null
|
||||||
store.vaults = []
|
store.vaults = []
|
||||||
store.vaultBalances = {}
|
store.vaultBalances = {}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const addr = accounts[0]
|
const addr = accounts[0]
|
||||||
const store = useStore()
|
console.log('account logged in', addr)
|
||||||
store.account = addr
|
store.account = addr
|
||||||
discoverVaults()
|
discoverVaults(addr)
|
||||||
flushTransactions()
|
flushTransactions()
|
||||||
socket.emit('address', store.chainId, addr)
|
socket.emit('address', chainId, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAccountsChanged(accounts) {
|
function onAccountsChanged(accounts) {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
if (accounts.length === 0 || accounts[0] !== store.account)
|
if (accounts.length === 0 || accounts[0] !== store.account)
|
||||||
changeAccounts(accounts);
|
changeAccounts(store.chainId.value, accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function detectChain() {
|
export function detectChain() {
|
||||||
@@ -93,15 +94,14 @@ export async function connectWallet() {
|
|||||||
let pendingOrders = []
|
let pendingOrders = []
|
||||||
|
|
||||||
|
|
||||||
function discoverVaults() {
|
function discoverVaults(owner) {
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
const owner = s.account.value
|
|
||||||
if( owner === null )
|
if( owner === null )
|
||||||
s.vaults.value = []
|
s.vaults = []
|
||||||
else
|
else
|
||||||
_discoverVaults(owner).then((result)=>{
|
_discoverVaults(owner).then((result)=>{
|
||||||
if( s.account.value === owner ) { // double-check the account since it could have changed during our await
|
if( s.account === owner ) { // double-check the account since it could have changed during our await
|
||||||
s.vaults.value = result
|
s.vaults = result
|
||||||
if( pendingOrders.length )
|
if( pendingOrders.length )
|
||||||
if( result.length )
|
if( result.length )
|
||||||
flushOrders(result[0])
|
flushOrders(result[0])
|
||||||
@@ -114,18 +114,22 @@ function discoverVaults() {
|
|||||||
async function _discoverVaults(owner) {
|
async function _discoverVaults(owner) {
|
||||||
const result = []
|
const result = []
|
||||||
// todo multi-vault scan
|
// todo multi-vault scan
|
||||||
const addr = vaultAddress(owner, 0)
|
const num = 0
|
||||||
|
const addr = vaultAddress(owner, num)
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
const vault = new ethers.Contract(addr, vaultAbi, s.provider)
|
const vault = new ethers.Contract(addr, vaultAbi, s.provider)
|
||||||
let version = -1
|
let version = -1
|
||||||
try {
|
try {
|
||||||
version = await vault.version();
|
version = await vault.version();
|
||||||
if( version === 1 )
|
if( version === 1 ) {
|
||||||
|
console.log(`found vault ${num} at ${addr}`)
|
||||||
result.push(addr)
|
result.push(addr)
|
||||||
|
}
|
||||||
else
|
else
|
||||||
console.error(`bad vault version ${version}`)
|
console.error(`bad vault version ${version}`)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
console.log(`no vault ${num}`)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -133,11 +137,16 @@ async function _discoverVaults(owner) {
|
|||||||
|
|
||||||
export function ensureVault() {
|
export function ensureVault() {
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
ensureVault2(s.chainId.value, s.account.value, 0)
|
const owner = s.account;
|
||||||
|
console.log('ensureVault', s.chainId.value, owner)
|
||||||
|
if( !owner )
|
||||||
|
return
|
||||||
|
ensureVault2(s.chainId.value, owner, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function ensureVault2(chainId, owner, num) {
|
export function ensureVault2(chainId, owner, num) {
|
||||||
|
console.log('ensureVault2', chainId, owner, num)
|
||||||
if( !chainId ) {
|
if( !chainId ) {
|
||||||
console.log('cannot create vault: no chain selected')
|
console.log('cannot create vault: no chain selected')
|
||||||
return
|
return
|
||||||
@@ -153,12 +162,12 @@ export function ensureVault2(chainId, owner, num) {
|
|||||||
export async function pendOrder(order) {
|
export async function pendOrder(order) {
|
||||||
console.log('order', JSON.stringify(order))
|
console.log('order', JSON.stringify(order))
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if (!s.vaults.value.length) {
|
if (!s.vaults.length) {
|
||||||
pendingOrders.push(order)
|
pendingOrders.push(order)
|
||||||
ensureVault()
|
ensureVault()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const vault = s.vaults.value[0];
|
const vault = s.vaults[0];
|
||||||
pendOrderAsTransaction(vault, order)
|
pendOrderAsTransaction(vault, order)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,11 +233,11 @@ export async function asyncFlushTransactions() {
|
|||||||
export async function asyncFlushTransactions2() {
|
export async function asyncFlushTransactions2() {
|
||||||
// todo rework into flushTransactions()
|
// todo rework into flushTransactions()
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.provider.value === null ) {
|
if( s.provider === null ) {
|
||||||
console.log('warning: asyncFlushOrders() cancelled due to null provider')
|
console.log('warning: asyncFlushOrders() cancelled due to null provider')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const senders = s.transactionSenders.value
|
const senders = s.transactionSenders
|
||||||
if (!senders.length)
|
if (!senders.length)
|
||||||
return
|
return
|
||||||
console.log(`flushing ${senders.length} transactions`)
|
console.log(`flushing ${senders.length} transactions`)
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
|
<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>
|
v-model="os.amount" :rules="[validateRequired,validateAmount]" v-auto-select>
|
||||||
<template v-slot:append-inner>
|
<template v-slot:append-inner>
|
||||||
<v-btn @click="s.amountIsTokenA=!s.amountIsTokenA" variant="outlined" class="mr-2">
|
<v-btn @click="os.amountIsTokenA=!os.amountIsTokenA" variant="outlined" class="mr-2">
|
||||||
{{ s.amountIsTokenA ? s.tokenA.symbol : s.tokenB.symbol }}
|
{{ os.amountIsTokenA ? os.tokenA.symbol : os.tokenB.symbol }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn :text="s.amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
|
<v-btn :text="os.amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
|
||||||
@click="s.amountIsTotal=!s.amountIsTotal" class="total"/>
|
@click="os.amountIsTotal=!os.amountIsTotal" class="total"/>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useStore} from "@/store/store";
|
import {useOrderStore} from "@/store/store";
|
||||||
import {validateRequired, validateAmount, vAutoSelect} from "@/misc.js";
|
// noinspection ES6UnusedImports
|
||||||
|
import {vAutoSelect} from "@/misc.js";
|
||||||
|
import {validateAmount, validateRequired} from "@/validate.js";
|
||||||
|
|
||||||
const s = useStore()
|
const os = useOrderStore()
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function gib() {
|
|||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.account ) {
|
if( s.account ) {
|
||||||
disabled.value = true
|
disabled.value = true
|
||||||
socket.emit('faucet', s.chainId, s.account)
|
socket.emit('faucet', s.chainId.value, s.account)
|
||||||
setTimeout(()=>disabled.value=false, 60*1000)
|
setTimeout(()=>disabled.value=false, 60*1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/components/LadderOrder.vue
Normal file
25
src/components/LadderOrder.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<order :tranches="buildTranches">
|
||||||
|
<limit-price :required="true" label=""/>
|
||||||
|
<limit-price :show-price="false" store-var="limitPrice2" :required="true" label=""/>
|
||||||
|
</order>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useStore} from "@/store/store";
|
||||||
|
import LimitPrice from "@/components/LimitPrice.vue";
|
||||||
|
import Order from "@/components/Order.vue";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
|
||||||
|
function buildTranches() {
|
||||||
|
const ts = []
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "src/styles/vars" as *;
|
||||||
|
|
||||||
|
</style>
|
||||||
38
src/components/LimitPrice.vue
Normal file
38
src/components/LimitPrice.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<v-text-field v-model="os[storeVar]" :label="getLabel" type="number"
|
||||||
|
variant="outlined" aria-valuemin="0" min="0"
|
||||||
|
clearable :rules="rules" v-auto-select>
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" @click="os.inverted=!os.inverted">
|
||||||
|
{{ os.pairSymbol }}
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<template #details style="flex-direction: column-reverse">
|
||||||
|
<div>
|
||||||
|
Current price <route-price :inverted="routeInverted(os.route)" :route="os.route" class="text-green"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useOrderStore, useStore} from "@/store/store";
|
||||||
|
import {routeInverted} from "@/misc.js";
|
||||||
|
import RoutePrice from "@/components/RoutePrice.vue";
|
||||||
|
import {validateAmount, validateRequired} from "@/validate.js";
|
||||||
|
import {computed} from "vue";
|
||||||
|
// noinspection ES6UnusedImports
|
||||||
|
import {vAutoSelect} from "@/misc.js";
|
||||||
|
|
||||||
|
const os = useOrderStore()
|
||||||
|
const props = defineProps({
|
||||||
|
required: {default: false},
|
||||||
|
label: {default: null},
|
||||||
|
storeVar: {default: 'limitPrice'},
|
||||||
|
showPrice: {default: true},
|
||||||
|
})
|
||||||
|
const rules = computed(()=>props.required ? [validateAmount, validateRequired] : [validateAmount])
|
||||||
|
|
||||||
|
const getLabel = computed(()=>props.label !== null ? props.label : (os.limitIsMinimum?'Minimum':'Maximum') +' Price')
|
||||||
|
|
||||||
|
</script>
|
||||||
63
src/components/Order.vue
Normal file
63
src/components/Order.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<needs-provider>
|
||||||
|
<phone-card class="tordercard">
|
||||||
|
<v-card-title class="big">{{title}}</v-card-title>
|
||||||
|
<v-card-subtitle>{{subtitle}}</v-card-subtitle>
|
||||||
|
<v-card-item>
|
||||||
|
<pair-choice/>
|
||||||
|
<div v-if="os.route && !os.routesPending">
|
||||||
|
<amount/>
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</v-card-item>
|
||||||
|
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
||||||
|
<v-btn variant="outlined" color="red">Cancel</v-btn>
|
||||||
|
<v-btn variant="flat" color="green" :disabled="!valid" @click="placeOrder">Place Dexorder</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</phone-card>
|
||||||
|
</needs-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useOrderStore, 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, TimeMode} from "@/blockchain/orderlib.js";
|
||||||
|
import {FixedNumber} from "ethers";
|
||||||
|
import {pendOrder} from "@/blockchain/wallet.js";
|
||||||
|
import NeedsProvider from "@/components/NeedsProvider.vue";
|
||||||
|
import RoutePrice from "@/components/RoutePrice.vue";
|
||||||
|
import router from "@/router/index.js";
|
||||||
|
import PairChoice from "@/components/PairChoice.vue";
|
||||||
|
import {isEmpty, validateAmount, validateRequired} from "@/validate.js";
|
||||||
|
|
||||||
|
const s = useStore()
|
||||||
|
const os = useOrderStore()
|
||||||
|
const props = defineProps(['title','subtitle','valid','tranches'])
|
||||||
|
|
||||||
|
function placeOrder() {
|
||||||
|
const ta = os.tokenA;
|
||||||
|
const tb = os.tokenB;
|
||||||
|
const tokenIn = os.buy ? tb.address : ta.address
|
||||||
|
const tokenOut = os.buy ? ta.address : tb.address
|
||||||
|
const route = os.route
|
||||||
|
const amt = FixedNumber.fromString(os.amount.toString(), {decimals: os.amountToken.decimals}).value
|
||||||
|
const ts = props.tranches()
|
||||||
|
const order = newOrder(tokenIn, tokenOut, route.exchange, route.fee, amt, os.amountIsInput, ts)
|
||||||
|
pendOrder(order)
|
||||||
|
router.push('/orders')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/styles/vars" as *;
|
||||||
|
|
||||||
|
.tordercard {
|
||||||
|
max-width: $card-maxw;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -70,8 +70,8 @@ function tokenAmount(tokenAddr, amount) {
|
|||||||
const t = token(tokenAddr)
|
const t = token(tokenAddr)
|
||||||
if( !t )
|
if( !t )
|
||||||
return ''
|
return ''
|
||||||
console.log('tokenAmount amount', typeof amount, amount)
|
// console.log('tokenAmount amount', typeof amount, amount)
|
||||||
console.log('tokenAmount decimals', typeof t.decimals, t.decimals)
|
// console.log('tokenAmount decimals', typeof t.decimals, t.decimals)
|
||||||
const amt = FixedNumber.fromValue(amount, t.decimals, {width:256, decimals:t.decimals, signed:false})
|
const amt = FixedNumber.fromValue(amount, t.decimals, {width:256, decimals:t.decimals, signed:false})
|
||||||
return `${amt} ${t.symbol}`
|
return `${amt} ${t.symbol}`
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,6 @@ function pair(inTokenAddr, outTokenAddr, vaultAddr, index) {
|
|||||||
|
|
||||||
const orders = computed(()=>{
|
const orders = computed(()=>{
|
||||||
const result = []
|
const result = []
|
||||||
console.log('computing orders')
|
|
||||||
// for( const [status] of pendingOrders.reverse() ) {
|
// for( const [status] of pendingOrders.reverse() ) {
|
||||||
// console.log('adding pended order')
|
// console.log('adding pended order')
|
||||||
// const inTokenAddr = status[0]
|
// const inTokenAddr = status[0]
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<token-choice v-model="tokenA" class="token-choice mb-1">
|
<token-choice v-model="tokenA" class="token-choice mb-1">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-btn :text="s.buy ? 'Buy' : 'Sell'" :color="s.buy ? 'green' : 'red'"
|
<v-btn :text="os.buy ? 'Buy' : 'Sell'" :color="os.buy ? 'green' : 'red'"
|
||||||
variant="outlined" @click="s.buy=!s.buy" class="bs-button"/>
|
variant="outlined" @click="os.buy=!os.buy" class="bs-button"/>
|
||||||
</template>
|
</template>
|
||||||
</token-choice>
|
</token-choice>
|
||||||
<token-choice v-model="tokenB" class="token-choice">
|
<token-choice v-model="tokenB" class="token-choice">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-btn :text="!s.buy ? 'Buy' : 'Sell'" :color="!s.buy ? 'green' : 'red'"
|
<v-btn :text="!os.buy ? 'Buy' : 'Sell'" :color="!os.buy ? 'green' : 'red'"
|
||||||
variant="outlined" @click="s.buy=!s.buy" class="bs-button"/>
|
variant="outlined" @click="os.buy=!os.buy" class="bs-button"/>
|
||||||
</template>
|
</template>
|
||||||
</token-choice>
|
</token-choice>
|
||||||
|
|
||||||
@@ -16,46 +16,48 @@
|
|||||||
{{ 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-color ml-0 mr-1">v3</span>
|
<span class="uniswap-color ml-0 mr-1">v3</span>
|
||||||
<span>{{ s.pairSymbol }} {{ r.fee / 10000 }}%</span>
|
<span>{{ os.pairSymbol }} {{ r.fee / 10000 }}%</span>
|
||||||
<route-price :route="r" :inverted="routeInverted(r)" class="text-green clickable" @click="s.inverted=!s.inverted"/>
|
<route-price :route="r" :inverted="routeInverted(r)" class="text-green clickable" @click="os.inverted=!os.inverted"/>
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
|
||||||
<div v-if="s.routesPending">
|
<div v-if="os.routesPending">
|
||||||
<v-progress-circular indeterminate/> Searching for {{s.pairSymbol}} pools...
|
<v-progress-circular indeterminate/>
|
||||||
|
Searching for {{ os.pairSymbol }} pools...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-alert v-if="!s.route && !s.routesPending" text="No pool found!"/>
|
<v-alert v-if="!os.route && !os.routesPending" text="No pool found!"/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TokenChoice from "@/components/TokenChoice.vue"
|
import TokenChoice from "@/components/TokenChoice.vue"
|
||||||
import {useStore} from "@/store/store";
|
import {useOrderStore, useStore} from "@/store/store";
|
||||||
import RoutePrice from "@/components/RoutePrice.vue";
|
import RoutePrice from "@/components/RoutePrice.vue";
|
||||||
import {findRoute} from "@/blockchain/route.js";
|
import {findRoute} from "@/blockchain/route.js";
|
||||||
import {SingletonCoroutine, routeInverted} from "@/misc.js";
|
import {SingletonCoroutine, routeInverted} from "@/misc.js";
|
||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
|
const os = useOrderStore()
|
||||||
|
|
||||||
const tokenA = computed({
|
const tokenA = computed({
|
||||||
get() {
|
get() {
|
||||||
return s.tokenA
|
return os.tokenA
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
if( !s.tokenA || s.tokenA.address !== value.address ) {
|
if( !os.tokenA || os.tokenA.address !== value.address ) {
|
||||||
s.tokenA = value
|
os.tokenA = value
|
||||||
routeFinder.invoke()
|
routeFinder.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const tokenB = computed({
|
const tokenB = computed({
|
||||||
get() {
|
get() {
|
||||||
return s.tokenB
|
return os.tokenB
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
if( !s.tokenB || s.tokenB.address !== value.address ) {
|
if( !os.tokenB || os.tokenB.address !== value.address ) {
|
||||||
s.tokenB = value
|
os.tokenB = value
|
||||||
routeFinder.invoke()
|
routeFinder.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,32 +66,32 @@ const tokenB = computed({
|
|||||||
|
|
||||||
const routes = computed({
|
const routes = computed({
|
||||||
get() {
|
get() {
|
||||||
return s.routes
|
return os.routes
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
console.log('setting new routes', s.routes, value)
|
console.log('setting new routes', os.routes, value)
|
||||||
s.routes = value
|
os.routes = value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
async function componentFindRoute() {
|
async function componentFindRoute() {
|
||||||
const tokenA = s.tokenA
|
const tokenA = os.tokenA
|
||||||
const tokenB = s.tokenB
|
const tokenB = os.tokenB
|
||||||
// console.log('finding route', tokenA, tokenB)
|
// console.log('finding route', tokenA, tokenB)
|
||||||
s.routes = []
|
os.routes = []
|
||||||
if (!tokenA || !tokenB)
|
if (!tokenA || !tokenB)
|
||||||
return
|
return
|
||||||
s.routesPending = true
|
os.routesPending = true
|
||||||
try {
|
try {
|
||||||
const result = await findRoute(tokenA, tokenB)
|
const result = await findRoute(s.chainId.value, tokenA, tokenB)
|
||||||
// console.log('found route', result)
|
console.log('found route', result)
|
||||||
s.routes = result
|
os.routes = result
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.log('ignoring routes exception', e)
|
console.log('ignoring routes exception', e)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
s.routesPending = false
|
os.routesPending = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ const props = defineProps({
|
|||||||
|
|
||||||
const price = computed(()=>{
|
const price = computed(()=>{
|
||||||
const route = props.route;
|
const route = props.route;
|
||||||
if( !route || !(route.pool in s.poolPrices) )
|
if( !route )
|
||||||
return ''
|
return ''
|
||||||
let p = s.poolPrices[route.pool]
|
const routeKey = [route.chainId, route.pool]
|
||||||
|
if( !(routeKey in s.poolPrices) )
|
||||||
|
return ''
|
||||||
|
let p = s.poolPrices[routeKey]
|
||||||
// console.log('pool price is',typeof p, p)
|
// console.log('pool price is',typeof p, p)
|
||||||
if( !p )
|
if( !p )
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
63
src/components/TimedOrder.vue
Normal file
63
src/components/TimedOrder.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<order title="DCA / TWAP" subtitle="Multiple tranches over a time range" :tranches="buildTranches" :valid="validOrder">
|
||||||
|
<Tranches/>
|
||||||
|
<LimitPrice/>
|
||||||
|
</order>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// noinspection ES6UnusedImports
|
||||||
|
import {routeInverted, SingletonCoroutine, vAutoSelect} from "@/misc.js";
|
||||||
|
import Order from "@/components/Order.vue";
|
||||||
|
import LimitPrice from "@/components/LimitPrice.vue";
|
||||||
|
import {timesliceTranches, limitConstraint} from "@/orderbuild.js";
|
||||||
|
import Tranches from "@/components/Tranches.vue";
|
||||||
|
import {useOrderStore} from "@/store/store.js";
|
||||||
|
|
||||||
|
const os = useOrderStore()
|
||||||
|
|
||||||
|
function buildTranches() {
|
||||||
|
const ts = timesliceTranches();
|
||||||
|
const priceConstraint = limitConstraint();
|
||||||
|
if( priceConstraint !== null )
|
||||||
|
for( let i=0; i<ts.length; i++)
|
||||||
|
ts[i][1].push(priceConstraint)
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
function validOrder() {
|
||||||
|
return os.validOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/styles/vars" as *;
|
||||||
|
|
||||||
|
.tordercard {
|
||||||
|
max-width: $card-maxw;
|
||||||
|
}
|
||||||
|
.token-choice {
|
||||||
|
width: 16em;
|
||||||
|
}
|
||||||
|
.bs-button {
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
.amount {
|
||||||
|
width: 23em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
width: 9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-into {
|
||||||
|
width: 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-input {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<needs-provider>
|
|
||||||
<phone-card class="toecard">
|
|
||||||
<v-card-title class="big">DCA / TWAP</v-card-title>
|
|
||||||
<v-card-subtitle>Multiple tranches over a time range</v-card-subtitle>
|
|
||||||
<v-card-item>
|
|
||||||
<pair-choice/>
|
|
||||||
<div v-if="s.route && !s.routesPending">
|
|
||||||
<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: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:prepend-inner>
|
|
||||||
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within mr-2"
|
|
||||||
@click="intervalIsTotal=!intervalIsTotal"/>
|
|
||||||
</template>
|
|
||||||
<template v-slot:append-inner>
|
|
||||||
<v-btn variant="outlined" :text="timeUnits[timeUnitIndex]" @click="toggleTimeUnits" class="time-units"/>
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
<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="s.inverted=!s.inverted">
|
|
||||||
{{s.pairSymbol}}
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<template #details style="flex-direction: column-reverse">
|
|
||||||
<div>
|
|
||||||
Current price <route-price :inverted="routeInverted(s.route)" :route="s.route" class="text-green"/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</v-card-item>
|
|
||||||
|
|
||||||
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
|
||||||
<v-btn variant="outlined" color="red">Cancel</v-btn>
|
|
||||||
<v-btn variant="flat" color="green" :disabled="!validOrder" @click="placeOrder">Place Dexorder</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</phone-card>
|
|
||||||
</needs-provider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
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 {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";
|
|
||||||
import RoutePrice from "@/components/RoutePrice.vue";
|
|
||||||
import router from "@/router/index.js";
|
|
||||||
import PairChoice from "@/components/PairChoice.vue";
|
|
||||||
|
|
||||||
const s = useStore()
|
|
||||||
|
|
||||||
const tranches = ref(3)
|
|
||||||
const minPrice = ref(null)
|
|
||||||
const maxPrice = ref(null)
|
|
||||||
const interval = ref(1)
|
|
||||||
const intervalIsTotal = ref(true)
|
|
||||||
const timeUnits = ['minutes', 'hours', 'days']
|
|
||||||
const timeUnitIndex = ref(0)
|
|
||||||
const validOrder = computed(()=>s.validOrder)
|
|
||||||
|
|
||||||
function toggleTimeUnits() {
|
|
||||||
timeUnitIndex.value++
|
|
||||||
if (timeUnitIndex.value >= timeUnits.length)
|
|
||||||
timeUnitIndex.value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function validateTranches(v) {
|
|
||||||
const i = parseInt(v)
|
|
||||||
if (parseFloat(v) !== i)
|
|
||||||
return 'Whole numbers only'
|
|
||||||
if (i < 1)
|
|
||||||
return 'Must have at least one tranche'
|
|
||||||
if (i > 255)
|
|
||||||
return 'Maximum 255 tranches'
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function validateMax(v) {
|
|
||||||
if (!isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value))
|
|
||||||
return 'Must be greater than the minimum price'
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateMin(v) {
|
|
||||||
if (!isEmpty(maxPrice.value) && !isEmpty(v) && parseFloat(v) > parseFloat(maxPrice.value))
|
|
||||||
return 'Must be less than the maximum price'
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function placeOrder() {
|
|
||||||
const ta = s.tokenA;
|
|
||||||
const tb = s.tokenB;
|
|
||||||
const tokenIn = s.buy ? tb.address : ta.address
|
|
||||||
const tokenOut = s.buy ? ta.address : tb.address
|
|
||||||
const route = s.route
|
|
||||||
const amt = FixedNumber.fromString(s.amount.toString(), {decimals: s.amountToken.decimals}).value
|
|
||||||
|
|
||||||
// build tranches
|
|
||||||
const n = tranches.value // num tranches
|
|
||||||
const ts = []
|
|
||||||
let duration = timeUnitIndex.value === 0 ? interval.value * 60 : // minutes
|
|
||||||
timeUnitIndex.value === 1 ? interval.value * 60 * 60 : // hours
|
|
||||||
interval.value * 24 * 60 * 60; // days
|
|
||||||
let window
|
|
||||||
if (!intervalIsTotal.value) {
|
|
||||||
window = duration
|
|
||||||
duration *= n // duration is the total time for all tranches
|
|
||||||
} else {
|
|
||||||
window = Math.round(duration / n)
|
|
||||||
}
|
|
||||||
const oneHundredPercent = 65535n // by contract definition of uint16 fraction
|
|
||||||
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
|
|
||||||
let priceConstraint = null
|
|
||||||
if( s.limitPrice ) {
|
|
||||||
const inverted = routeInverted(route)
|
|
||||||
const isAbove = s.limitIsMinimum ^ inverted
|
|
||||||
const isRatio = false // todo ratios
|
|
||||||
const decimals = 10 ** (s.tokenA.decimals - s.tokenB.decimals)
|
|
||||||
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
|
|
||||||
const cs = [newTimeConstraint(TimeMode.SinceOrderStart, start, TimeMode.SinceOrderStart, end)]
|
|
||||||
if( priceConstraint !== null )
|
|
||||||
cs.push(priceConstraint)
|
|
||||||
ts.push([amtPerTranche, cs])
|
|
||||||
}
|
|
||||||
const order = newOrder(tokenIn, tokenOut, route.exchange, route.fee, amt, amountIsInput, ts)
|
|
||||||
pendOrder(order)
|
|
||||||
router.push('/orders')
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@use "@/styles/vars" as *;
|
|
||||||
|
|
||||||
.toecard {
|
|
||||||
max-width: $card-maxw;
|
|
||||||
}
|
|
||||||
.token-choice {
|
|
||||||
width: 16em;
|
|
||||||
}
|
|
||||||
.bs-button {
|
|
||||||
width: 6em;
|
|
||||||
}
|
|
||||||
.amount {
|
|
||||||
width: 23em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.total {
|
|
||||||
width: 9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-into {
|
|
||||||
width: 8em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-input {
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interval {
|
|
||||||
//width: 18em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.within {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-units {
|
|
||||||
width: 8em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
51
src/components/Tranches.vue
Normal file
51
src/components/Tranches.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1" max="255"
|
||||||
|
v-model="os.tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
|
||||||
|
<template v-slot:append-inner>tranches</template>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field type="number" variant="outlined" :min="1" v-model="os.interval" class="interval"
|
||||||
|
:label="os.intervalIsTotal ? 'Completion time' : 'Time between tranches'" v-auto-select>
|
||||||
|
<template v-slot:prepend-inner>
|
||||||
|
<v-btn variant="outlined" :text="os.intervalIsTotal ? 'Within' : 'Spaced apart'" class="within mr-2"
|
||||||
|
@click="os.intervalIsTotal=!os.intervalIsTotal"/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append-inner>
|
||||||
|
<v-btn variant="outlined" :text="timeUnitsStr" @click="toggleTimeUnits" class="time-units"/>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useOrderStore, useStore} from "@/store/store.js";
|
||||||
|
import {validateRequired, validateTranches} from "@/validate.js";
|
||||||
|
// noinspection ES6UnusedImports
|
||||||
|
import {vAutoSelect} from "@/misc.js";
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
|
const os = useOrderStore()
|
||||||
|
|
||||||
|
const timeUnits = ['minutes', 'hours', 'days']
|
||||||
|
const timeUnitsStr = computed(()=>timeUnits[os.timeUnitIndex])
|
||||||
|
|
||||||
|
function toggleTimeUnits() {
|
||||||
|
os.timeUnitIndex++
|
||||||
|
if (os.timeUnitIndex >= timeUnits.length)
|
||||||
|
os.timeUnitIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use "@/styles/vars" as *;
|
||||||
|
.interval {
|
||||||
|
//width: 18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.within {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-units {
|
||||||
|
width: 8em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -110,9 +110,19 @@ function onWithdraw(addr) {
|
|||||||
withdrawShow.value = true
|
withdrawShow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function checkVault() {
|
||||||
|
console.log('checkVault', props.num, s.account, s.vault)
|
||||||
|
if(props.num===0 && s.account && !s.vault)
|
||||||
|
ensureVault()
|
||||||
|
}
|
||||||
|
|
||||||
// todo remove automatic vault creation for Alpha 2
|
// todo remove automatic vault creation for Alpha 2
|
||||||
if(props.num===0 && !s.vault)
|
s.$subscribe((mutation, state)=>{
|
||||||
ensureVault()
|
console.log('test')
|
||||||
|
checkVault()
|
||||||
|
})
|
||||||
|
checkVault()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
20
src/misc.js
20
src/misc.js
@@ -59,23 +59,3 @@ export function routeInverted(route) {
|
|||||||
return route && (route.token0 === s.tokenA) === s.inverted
|
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
|
|
||||||
}
|
|
||||||
46
src/orderbuild.js
Normal file
46
src/orderbuild.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {routeInverted} from "@/misc.js";
|
||||||
|
import {newLimitConstraint, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js";
|
||||||
|
import {useOrderStore, useStore} from "@/store/store.js";
|
||||||
|
|
||||||
|
export function limitConstraint() {
|
||||||
|
const s = useStore()
|
||||||
|
if (!s.limitPrice)
|
||||||
|
return null
|
||||||
|
const route = s.route
|
||||||
|
const inverted = routeInverted(route)
|
||||||
|
const isAbove = s.limitIsMinimum ^ inverted
|
||||||
|
const isRatio = false // todo ratios
|
||||||
|
const decimals = 10 ** (s.tokenA.decimals - s.tokenB.decimals)
|
||||||
|
const limit = inverted ? decimals / s.limitPrice : s.limitPrice / decimals
|
||||||
|
return newLimitConstraint(isAbove, isRatio, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timesliceTranches() {
|
||||||
|
const ts = []
|
||||||
|
const os = useOrderStore()
|
||||||
|
const n = os.tranches // num tranches
|
||||||
|
const interval = os.interval;
|
||||||
|
const timeUnitIndex = os.timeUnitIndex;
|
||||||
|
let duration =
|
||||||
|
timeUnitIndex === 0 ? interval * 60 : // minutes
|
||||||
|
timeUnitIndex === 1 ? interval * 60 * 60 : // hours
|
||||||
|
interval * 24 * 60 * 60; // days
|
||||||
|
let window
|
||||||
|
if (!os.intervalIsTotal) {
|
||||||
|
window = duration
|
||||||
|
duration *= n // duration is the total time for all tranches
|
||||||
|
} else {
|
||||||
|
window = Math.round(duration / n)
|
||||||
|
}
|
||||||
|
const oneHundredPercent = 65535n // by contract definition of uint16 fraction
|
||||||
|
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
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const start = Math.floor(i * (duration / Math.max((n - 1), 1)))
|
||||||
|
const end = start + window
|
||||||
|
const cs = [newTimeConstraint(TimeMode.SinceOrderStart, start, TimeMode.SinceOrderStart, end)]
|
||||||
|
ts.push([amtPerTranche, cs])
|
||||||
|
}
|
||||||
|
return ts
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import {useStore} from "@/store/store.js";
|
|||||||
import {flushOrders, onChainChanged} from "@/blockchain/wallet.js";
|
import {flushOrders, onChainChanged} from "@/blockchain/wallet.js";
|
||||||
import {ethers} from "ethers";
|
import {ethers} from "ethers";
|
||||||
import {applyFills} from "@/blockchain/common.js";
|
import {applyFills} from "@/blockchain/common.js";
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
export const socket = io(import.meta.env.VITE_WS_URL || undefined, {transports: ["websocket"]})
|
export const socket = io(import.meta.env.VITE_WS_URL || undefined, {transports: ["websocket"]})
|
||||||
|
|
||||||
@@ -28,20 +27,16 @@ socket.on('welcome', async (data) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.on('p', async (chainId, pool, price) => {
|
socket.on('p', async (chainId, pool, price) => {
|
||||||
|
console.log('pool price from message', chainId, pool, price)
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.chainId !== chainId )
|
if( s.chainId.value !== chainId )
|
||||||
return
|
return
|
||||||
const prices = {}
|
s.poolPrices[[chainId,pool]] = price
|
||||||
prices[pool] = price
|
|
||||||
console.log('pool price from message', pool, typeof price, price)
|
|
||||||
const poolPrices = s.poolPrices
|
|
||||||
poolPrices[pool] = price
|
|
||||||
s.poolPrices = poolPrices
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('vb', async (chainId, vault, balances) => {
|
socket.on('vb', async (chainId, vault, balances) => {
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.chainId !== chainId )
|
if( s.chainId.value !== chainId )
|
||||||
return
|
return
|
||||||
console.log('vb', vault, balances)
|
console.log('vb', vault, balances)
|
||||||
const vb = {}
|
const vb = {}
|
||||||
@@ -52,9 +47,9 @@ socket.on('vb', async (chainId, vault, balances) => {
|
|||||||
|
|
||||||
socket.on('vaults', (chainId, owner, vaults)=>{
|
socket.on('vaults', (chainId, owner, vaults)=>{
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.chainId !== chainId || s.account !== owner )
|
|
||||||
return
|
|
||||||
console.log('vaults', vaults)
|
console.log('vaults', vaults)
|
||||||
|
if( s.chainId.value !== chainId || s.account !== owner )
|
||||||
|
return
|
||||||
s.vaults = vaults
|
s.vaults = vaults
|
||||||
if( vaults.length ) {
|
if( vaults.length ) {
|
||||||
const vault = vaults[0]
|
const vault = vaults[0]
|
||||||
@@ -65,14 +60,12 @@ socket.on('vaults', (chainId, owner, vaults)=>{
|
|||||||
|
|
||||||
function handleOrderStatus(chainId, vault, orderIndex, status) {
|
function handleOrderStatus(chainId, vault, orderIndex, status) {
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.chainId !== chainId )
|
if( s.chainId.value !== chainId )
|
||||||
return
|
return
|
||||||
console.log('o', chainId, vault, orderIndex, status)
|
console.log('o', chainId, vault, orderIndex, status)
|
||||||
const orders = s.orders
|
if( !(vault in s.orders) )
|
||||||
if( !(vault in orders) )
|
s.orders[vault] = {}
|
||||||
orders[vault] = {}
|
s.orders[vault][orderIndex] = status
|
||||||
orders[vault][orderIndex] = status
|
|
||||||
s.orders = orders
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('os', (chainId, vault, orders) => {
|
socket.on('os', (chainId, vault, orders) => {
|
||||||
@@ -85,19 +78,18 @@ socket.on( 'o', handleOrderStatus)
|
|||||||
|
|
||||||
socket.on( 'of', (chainId, vault, orderIndex, fills)=>{
|
socket.on( 'of', (chainId, vault, orderIndex, fills)=>{
|
||||||
const s = useStore()
|
const s = useStore()
|
||||||
if( s.chainId !== chainId )
|
if( s.chainId.value !== chainId )
|
||||||
return
|
return
|
||||||
console.log('of', chainId, vault, orderIndex, fills)
|
console.log('of', chainId, vault, orderIndex, fills)
|
||||||
const orders = s.orders
|
if( !(vault in s.orders) ) {
|
||||||
if( !(vault in orders) ) {
|
|
||||||
console.log('warning: got fill on an order in an unknown vault')
|
console.log('warning: got fill on an order in an unknown vault')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if( !(orderIndex in orders[vault]) ) {
|
if( !(orderIndex in s.orders[vault]) ) {
|
||||||
console.log(`warning: orderIndex ${orderIndex} missing from vault ${vault}`)
|
console.log(`warning: orderIndex ${orderIndex} missing from vault ${vault}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const order = orders[vault][orderIndex]
|
const order = s.orders[vault][orderIndex]
|
||||||
applyFills(order, fills)
|
applyFills(order, fills)
|
||||||
orders[vault][orderIndex] = order
|
s.orders[vault][orderIndex] = order
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,11 +4,33 @@ import {knownTokens} from "@/knownTokens.js";
|
|||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
|
|
||||||
|
// USING THE STORE:
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// When defining the store, use Vue script setup syntax, which requires assignment to .value:
|
||||||
|
//
|
||||||
|
// defineStore('foostore', ()=>{
|
||||||
|
// const foo = ref(true)
|
||||||
|
// const obj = reactive({})
|
||||||
|
// function reset() {
|
||||||
|
// obj.value = {}
|
||||||
|
// foo.value = false
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Then use the store in components:
|
||||||
|
//
|
||||||
|
// const s = useStore()
|
||||||
|
// if( s.foo ) // store variables may be read simply
|
||||||
|
// s.reset() // store actions (functions) may be called directly
|
||||||
|
// const stuff = [ {}, [], true ]
|
||||||
|
// if( s.obj.value in stuff || s.obj.value === {} ) ... // use .value to access the raw object instead of its reference
|
||||||
|
|
||||||
|
|
||||||
export const useStore = defineStore('app', ()=> {
|
export const useStore = defineStore('app', ()=> {
|
||||||
const _chainId = ref(null)
|
const _chainId = ref(null)
|
||||||
const _chainInfo = ref({})
|
const _chainInfo = ref({})
|
||||||
const tokenA = ref(null)
|
|
||||||
const tokenB = ref(null)
|
|
||||||
|
|
||||||
function getTokenList() {
|
function getTokenList() {
|
||||||
const chains = _chainId.value in _chainInfo.value && _chainInfo.value[_chainId.value].tokens !== undefined ?
|
const chains = _chainId.value in _chainInfo.value && _chainInfo.value[_chainId.value].tokens !== undefined ?
|
||||||
@@ -26,20 +48,16 @@ export const useStore = defineStore('app', ()=> {
|
|||||||
result[token.address] = token
|
result[token.address] = token
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
function setDefaultTokens() {
|
function refreshChain() {
|
||||||
const tokens = getTokenList()
|
useOrderStore().setDefaultTokens(getTokenList())
|
||||||
if( tokens.length > 0 )
|
|
||||||
tokenA.value = tokens[0]
|
|
||||||
if( tokens.length > 1 )
|
|
||||||
tokenB.value = tokens[1]
|
|
||||||
}
|
}
|
||||||
const chainId = computed({
|
const chainId = computed({
|
||||||
get() {return _chainId},
|
get() {return _chainId},
|
||||||
set(v) {_chainId.value=v; setDefaultTokens()}
|
set(v) {_chainId.value=v; refreshChain()}
|
||||||
})
|
})
|
||||||
const chainInfo = computed({
|
const chainInfo = computed({
|
||||||
get() {return _chainInfo},
|
get() {return _chainInfo},
|
||||||
set(v) {_chainInfo.value=v; setDefaultTokens()}
|
set(v) {_chainInfo.value=v; refreshChain()}
|
||||||
})
|
})
|
||||||
const chain = computed(() => !_chainId.value ? null : (_chainInfo.value[_chainId.value] || null))
|
const chain = computed(() => !_chainId.value ? null : (_chainInfo.value[_chainId.value] || null))
|
||||||
// making the provider directly reactive causes exceptions (calling private method...) when calling provider
|
// making the provider directly reactive causes exceptions (calling private method...) when calling provider
|
||||||
@@ -56,42 +74,16 @@ export const useStore = defineStore('app', ()=> {
|
|||||||
const transactionSenders = ref([]) // a list of function(signer) that send transactions
|
const transactionSenders = ref([]) // a list of function(signer) that send transactions
|
||||||
const errors = ref([])
|
const errors = ref([])
|
||||||
const extraTokens = ref({})
|
const extraTokens = ref({})
|
||||||
const poolPrices = ref({})
|
const poolPrices = ref({}) // keyed by [chainId,addr]
|
||||||
const vaultBalances = ref({}) // indexed by vault addr then by token addr. value is an int
|
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
|
const orders = ref({}) // indexed by vault value is another dictionary with orderIndex as key and order status values
|
||||||
|
|
||||||
// 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 vault = computed(() => vaults.value.length === 0 ? null : vaults.value[0])
|
||||||
const tokens = computed(getTokens)
|
const tokens = computed(getTokens)
|
||||||
const factory = computed(() => !chain.value ? null : chain.value.factory)
|
const factory = computed(() => !chain.value ? null : chain.value.factory)
|
||||||
const helper = computed(() => !chain.value ? null : chain.value.helper)
|
const helper = computed(() => !chain.value ? null : chain.value.helper)
|
||||||
const mockenv = computed(() => !chain.value ? null : chain.value.mockenv)
|
const mockenv = computed(() => !chain.value ? null : chain.value.mockenv)
|
||||||
const mockCoins = computed(() => !chain.value ? [] : !chain.value.mockCoins ? [] : chain.value.mockCoins)
|
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) {
|
function removeTransactionSender(sender) {
|
||||||
this.transactionSenders = this.transactionSenders.filter((v) => v !== sender)
|
this.transactionSenders = this.transactionSenders.filter((v) => v !== sender)
|
||||||
@@ -117,9 +109,58 @@ export const useStore = defineStore('app', ()=> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
chainId, chainInfo, chain, provider, vaultInitCodeHash, account, vaults, transactionSenders, errors, extraTokens,
|
chainId, chainInfo, chain, provider, vaultInitCodeHash, account, vaults, transactionSenders, errors, extraTokens,
|
||||||
poolPrices, vaultBalances, orders, tokenA, tokenB, routes, routesPending, inverted, amount, amountIsTokenA,
|
poolPrices, vaultBalances, orders, vault, tokens, factory, helper, mockenv, mockCoins, removeTransactionSender,
|
||||||
amountIsTotal, limitPrice, validOrder, vault, tokens, factory, helper, mockenv, mockCoins, route,
|
error, closeError, addToken,
|
||||||
pairSymbol, base, quote, limitIsMinimum, amountToken, amountIsInput, removeTransactionSender, error, closeError,
|
|
||||||
addToken,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useOrderStore = defineStore('order', ()=> {
|
||||||
|
const tokenA = ref(null)
|
||||||
|
const tokenB = ref(null)
|
||||||
|
|
||||||
|
// 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 adjust default
|
||||||
|
const amountIsTokenA = ref(false) // todo adjust default
|
||||||
|
const amountIsTotal = ref(true)
|
||||||
|
const limitPrice = ref(null)
|
||||||
|
const limitPrice2 = ref(null)
|
||||||
|
const tranches = ref(3)
|
||||||
|
const interval = ref(1)
|
||||||
|
const intervalIsTotal = ref(true)
|
||||||
|
const timeUnitIndex = ref(0)
|
||||||
|
const routes = ref([])
|
||||||
|
const routesPending = ref(false)
|
||||||
|
|
||||||
|
const validOrder = computed(() => amount.value > 0 && routes.value.length > 0)
|
||||||
|
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 setDefaultTokens(tokens) {
|
||||||
|
if( tokens.length > 0 )
|
||||||
|
tokenA.value = tokens[0]
|
||||||
|
if( tokens.length > 1 )
|
||||||
|
tokenB.value = tokens[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tokenA, tokenB, buy, inverted, amount, amountIsTokenA, amountIsTotal, limitPrice, limitPrice2, tranches,
|
||||||
|
interval, intervalIsTotal, timeUnitIndex, routes, routesPending, validOrder, route, base, quote, pairSymbol,
|
||||||
|
limitIsMinimum, amountToken, amountIsInput, setDefaultTokens,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -37,6 +37,11 @@
|
|||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-input {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.maxw {
|
.maxw {
|
||||||
max-width: v.$card-maxw;
|
max-width: v.$card-maxw;
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/validate.js
Normal file
44
src/validate.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMax(v) {
|
||||||
|
if (!isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value))
|
||||||
|
return 'Must be greater than the minimum price'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMin(v) {
|
||||||
|
if (!isEmpty(maxPrice.value) && !isEmpty(v) && parseFloat(v) > parseFloat(maxPrice.value))
|
||||||
|
return 'Must be less than the maximum price'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTranches(v) {
|
||||||
|
const i = parseInt(v)
|
||||||
|
if (parseFloat(v) !== i)
|
||||||
|
return 'Whole numbers only'
|
||||||
|
if (i < 1)
|
||||||
|
return 'Must have at least one tranche'
|
||||||
|
if (i > 255)
|
||||||
|
return 'Maximum 255 tranches'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TimedOrder from "@/components/TimedOrderEntry.vue";
|
import TimedOrder from "@/components/TimedOrder.vue";
|
||||||
import Vault from "@/components/Vault.vue";
|
import Vault from "@/components/Vault.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TimedOrder from "@/components/TimedOrderEntry.vue";
|
import TimedOrder from "@/components/TimedOrder.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
Reference in New Issue
Block a user