extracted PairChoice component
This commit is contained in:
106
src/components/PairChoice.vue
Normal file
106
src/components/PairChoice.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<token-choice v-model="tokenA" class="token-choice mb-1">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="s.buy ? 'Buy' : 'Sell'" :color="s.buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="s.buy=!s.buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
<token-choice v-model="tokenB" class="token-choice">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="!s.buy ? 'Buy' : 'Sell'" :color="!s.buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="s.buy=!s.buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
|
||||
<v-chip v-for="r in routes" variant="text">
|
||||
{{ s.chain.name }}
|
||||
<v-img src="https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg" width="1.5em"/>
|
||||
<span class="uniswap-color ml-0 mr-1">v3</span>
|
||||
<span>{{ s.pairSymbol }} {{ r.fee / 10000 }}%</span>
|
||||
<route-price :route="r" :inverted="routeInverted(r)" class="text-green clickable" @click="s.inverted=!s.inverted"/>
|
||||
</v-chip>
|
||||
|
||||
<div v-if="s.routesPending">
|
||||
<v-progress-circular indeterminate/> Searching for {{s.pairSymbol}} pools...
|
||||
</div>
|
||||
|
||||
<v-alert v-if="!s.route && !s.routesPending" text="No pool found!"/>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TokenChoice from "@/components/TokenChoice.vue"
|
||||
import {useStore} from "@/store/store";
|
||||
import RoutePrice from "@/components/RoutePrice.vue";
|
||||
import {findRoute} from "@/blockchain/route.js";
|
||||
import {SingletonCoroutine, routeInverted} from "@/misc.js";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
const tokenA = computed({
|
||||
get() {
|
||||
return s.tokenA
|
||||
},
|
||||
set(value) {
|
||||
if( !s.tokenA || s.tokenA.address !== value.address ) {
|
||||
s.tokenA.value = value
|
||||
routeFinder.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
const tokenB = computed({
|
||||
get() {
|
||||
return s.tokenB
|
||||
},
|
||||
set(value) {
|
||||
if( !s.tokenB || s.tokenB.address !== value.address ) {
|
||||
s.tokenB = value
|
||||
routeFinder.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const routes = computed({
|
||||
get() {
|
||||
return s.routes.value
|
||||
},
|
||||
set(value) {
|
||||
console.log('setting new routes', s.routes.value, value)
|
||||
s.routes.value = value
|
||||
}
|
||||
})
|
||||
|
||||
async function componentFindRoute() {
|
||||
const tokenA = s.tokenA
|
||||
const tokenB = s.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)
|
||||
s.routes = result
|
||||
}
|
||||
catch (e) {
|
||||
console.log('ignoring routes exception', e)
|
||||
}
|
||||
finally {
|
||||
s.routesPending = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const routeFinder = new SingletonCoroutine(componentFindRoute,10)
|
||||
routeFinder.invoke()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
@@ -3,40 +3,14 @@
|
||||
<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-text>
|
||||
<token-choice v-model="tokenA" class="token-choice mb-1">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="buy ? 'Buy' : 'Sell'" :color="buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="buy=!buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
<token-choice v-model="tokenB" class="token-choice">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="!buy ? 'Buy' : 'Sell'" :color="!buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="buy=!buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
|
||||
<v-chip v-for="r in routes" variant="text">
|
||||
{{ s.chain.name }}
|
||||
<v-img src="https://upload.wikimedia.org/wikipedia/commons/e/e7/Uniswap_Logo.svg" width="1.5em"/>
|
||||
<span class="uniswap-color ml-0 mr-1">v3</span>
|
||||
<span>{{pairSymbol}} {{r.fee/10000}}%</span>
|
||||
<route-price :route="r" :inverted="routeInverted(r)" class="text-green clickable" @click="inverted=!inverted"/>
|
||||
</v-chip>
|
||||
|
||||
<div v-if="routesPending">
|
||||
<v-progress-circular indeterminate/> Searching for {{pairSymbol}} pools...
|
||||
</div>
|
||||
|
||||
<v-alert v-if="!route && !routesPending" text="No pool found!"/>
|
||||
|
||||
<div v-if="route && !routesPending">
|
||||
<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 ? tokenA.symbol : tokenB.symbol }}
|
||||
{{ amountIsTokenA ? s.tokenA.symbol : s.tokenB.symbol }}
|
||||
</v-btn>
|
||||
<v-btn :text="amountIsTotal ? 'total' : 'per tranche'" variant="outlined"
|
||||
@click="amountIsTotal=!amountIsTotal" class="total"/>
|
||||
@@ -65,18 +39,18 @@
|
||||
clearable :rules="[validateAmount, validateMin]" v-auto-select>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{pairSymbol}}
|
||||
{{s.pairSymbol}}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template #details style="flex-direction: column-reverse">
|
||||
<div>
|
||||
Current price <route-price :inverted="routeInverted(route)" :route="route" class="text-green"/>
|
||||
Current price <route-price :inverted="routeInverted(s.route)" :route="s.route" class="text-green"/>
|
||||
</div>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-actions class="d-flex justify-space-evenly mb-4">
|
||||
<v-btn variant="outlined" color="red">Cancel</v-btn>
|
||||
@@ -88,75 +62,25 @@
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {computed, onBeforeUnmount, ref} from "vue";
|
||||
import TokenChoice from "@/components/TokenChoice.vue"
|
||||
import {computed, ref} from "vue";
|
||||
import PhoneCard from "@/components/PhoneCard.vue";
|
||||
// noinspection ES6UnusedImports
|
||||
import {SingletonCoroutine, vAutoSelect} from "@/misc.js";
|
||||
import {routeInverted, SingletonCoroutine, vAutoSelect} from "@/misc.js";
|
||||
import {newLimitConstraint, newOrder, newTimeConstraint, sqrtX96, TimeMode} from "@/blockchain/orderlib.js";
|
||||
import {FixedNumber} from "ethers";
|
||||
import {pendOrder} from "@/blockchain/wallet.js";
|
||||
import NeedsProvider from "@/components/NeedsProvider.vue";
|
||||
import {findRoute} from "@/blockchain/route.js";
|
||||
import RoutePrice from "@/components/RoutePrice.vue";
|
||||
import router from "@/router/index.js";
|
||||
import PairChoice from "@/components/PairChoice.vue";
|
||||
|
||||
const s = useStore()
|
||||
const buy = ref(false)
|
||||
let _tokenA = ref(Object.values(s.tokens).length >= 1 ? Object.values(s.tokens)[0] : null)
|
||||
let _tokenB = ref(Object.values(s.tokens).length >= 2 ? Object.values(s.tokens)[1] : null)
|
||||
const tokenA = computed({
|
||||
get() {
|
||||
return _tokenA.value
|
||||
},
|
||||
set(value) {
|
||||
if( !_tokenA.value || _tokenA.value.address !== value.address ) {
|
||||
_tokenA.value = value
|
||||
routeFinder.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
const tokenB = computed({
|
||||
get() {
|
||||
return _tokenB.value
|
||||
},
|
||||
set(value) {
|
||||
if( !_tokenB.value || _tokenB.value.address !== value.address ) {
|
||||
_tokenB.value = value
|
||||
routeFinder.invoke()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const pairSymbol = computed(()=>base.value?.symbol+'\\'+quote.value?.symbol)
|
||||
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 _routes = ref([])
|
||||
const routes = computed({
|
||||
get() {
|
||||
return _routes.value
|
||||
},
|
||||
set(value) {
|
||||
console.log('setting new routes', _routes.value, value)
|
||||
_routes.value = value
|
||||
}
|
||||
})
|
||||
const route = computed(()=>_routes.value.length===0 ? null : _routes.value[0])
|
||||
const routesPending = ref(false)
|
||||
const amount = ref(100) // todo 0
|
||||
const amountIsTokenA = ref(false)
|
||||
const amountIsTotal = ref(true)
|
||||
const tranches = ref(3)
|
||||
const inverted = ref(false)
|
||||
function routeInverted(route) {
|
||||
return route && (route.token0 === tokenA.value) === inverted.value
|
||||
}
|
||||
const minPrice = ref(null)
|
||||
const maxPrice = ref(null)
|
||||
const limitPrice = ref(null)
|
||||
@@ -164,33 +88,10 @@ const interval = ref(1)
|
||||
const intervalIsTotal = ref(true)
|
||||
const timeUnits = ['minutes', 'hours', 'days']
|
||||
const timeUnitIndex = ref(0)
|
||||
const limitIsMinimum = computed(() => !(buy.value ^ inverted.value))
|
||||
const validOrder = computed(()=>amount.value > 0 && routes.value.length > 0 )
|
||||
const limitIsMinimum = computed(() => !(s.buy ^ s.inverted))
|
||||
const validOrder = computed(()=>amount.value > 0 && s.routes.length > 0 )
|
||||
|
||||
|
||||
async function componentFindRoute() {
|
||||
console.log('finding route', _tokenA.value, _tokenB.value)
|
||||
routes.value = []
|
||||
if (!_tokenA.value || !_tokenB.value)
|
||||
return
|
||||
routesPending.value = true
|
||||
try {
|
||||
const result = await findRoute(tokenA.value, tokenB.value)
|
||||
console.log('found route', result)
|
||||
routes.value = result
|
||||
}
|
||||
catch (e) {
|
||||
console.log('ignoring routes exception', e)
|
||||
}
|
||||
finally {
|
||||
routesPending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const routeFinder = new SingletonCoroutine(componentFindRoute,10)
|
||||
routeFinder.invoke()
|
||||
|
||||
function toggleTimeUnits() {
|
||||
timeUnitIndex.value++
|
||||
if (timeUnitIndex.value >= timeUnits.length)
|
||||
@@ -246,14 +147,14 @@ function validateMin(v) {
|
||||
}
|
||||
|
||||
function placeOrder() {
|
||||
const ta = tokenA.value;
|
||||
const tb = tokenB.value;
|
||||
const tokenIn = buy.value ? tb.address : ta.address
|
||||
const tokenOut = buy.value ? ta.address : tb.address
|
||||
const route = routes.value[0];
|
||||
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 amountToken = amountIsTokenA.value ? ta : tb
|
||||
const amt = FixedNumber.fromString(amount.value.toString(), {decimals: amountToken.decimals}).value
|
||||
const amountIsInput = amountIsTokenA.value !== buy.value
|
||||
const amountIsInput = amountIsTokenA.value !== s.buy
|
||||
|
||||
// build tranches
|
||||
const n = tranches.value // num tranches
|
||||
@@ -278,12 +179,11 @@ function placeOrder() {
|
||||
const inverted = routeInverted(route)
|
||||
const isAbove = limitIsMinimum.value ^ inverted
|
||||
const isRatio = false // todo ratios
|
||||
const decimals = 10 ** (tokenA.value.decimals - tokenB.value.decimals)
|
||||
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)
|
||||
}
|
||||
for (let i = 0; i < n; i++) {
|
||||
// const start = Math.floor(i * (duration / (n - 1)))
|
||||
const start = Math.floor(i * (duration / Math.max((n - 1), 1)))
|
||||
const end = start + window
|
||||
console.log('tranche window', start, end, (end-start)/60)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {FixedNumber} from "ethers";
|
||||
import {useStore} from "@/store/store.js";
|
||||
|
||||
export class SingletonCoroutine {
|
||||
constructor(f, delay=10, retry=true) {
|
||||
@@ -52,3 +53,8 @@ export function tokenNumber(token, balance) {
|
||||
export function tokenFloat(token, balance) {
|
||||
return tokenNumber(token,balance).toUnsafeFloat()
|
||||
}
|
||||
|
||||
export function routeInverted(route) {
|
||||
const s = useStore()
|
||||
return route && (route.token0 === s.tokenA) === s.inverted
|
||||
}
|
||||
|
||||
@@ -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 {ref} from "vue";
|
||||
|
||||
export const socket = io(import.meta.env.VITE_WS_URL || undefined, {transports: ["websocket"]})
|
||||
|
||||
@@ -23,6 +24,11 @@ socket.on('welcome', async (data) => {
|
||||
const s = useStore()
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Utilities
|
||||
import { defineStore } from 'pinia'
|
||||
import {knownTokens} from "@/knownTokens.js";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
let rawProvider = null
|
||||
let rawProviderChainId = null
|
||||
@@ -26,6 +27,13 @@ export const useStore = defineStore('app', {
|
||||
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
|
||||
|
||||
// Order Input Forms
|
||||
tokenA: null,
|
||||
tokenB: null,
|
||||
routes: [],
|
||||
routesPending: false,
|
||||
inverted: false,
|
||||
}),
|
||||
getters: {
|
||||
vault: (s)=>s.vaults.length===0 ? null : s.vaults[0],
|
||||
@@ -47,6 +55,16 @@ export const useStore = defineStore('app', {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user