initial checkin with timed order ui
This commit is contained in:
22
src/components/BuySellToggle.vue
Normal file
22
src/components/BuySellToggle.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-btn :text="modelValue ? 'Buy' : 'Sell'" :color="modelValue ? 'green' : 'red'"
|
||||
variant="outlined" size="x-large" @click="toggle"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
|
||||
const s = useStore()
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
function toggle() {
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
75
src/components/HelloWorld.vue
Normal file
75
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive class="align-center text-center fill-height">
|
||||
<v-img height="300" src="@/assets/logo.svg" />
|
||||
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
|
||||
|
||||
<div class="py-14" />
|
||||
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
href="https://vuetifyjs.com/components/all/"
|
||||
min-width="164"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-view-dashboard"
|
||||
size="large"
|
||||
start
|
||||
/>
|
||||
|
||||
Components
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||
min-width="228"
|
||||
rel="noopener noreferrer"
|
||||
size="x-large"
|
||||
target="_blank"
|
||||
variant="flat"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-speedometer"
|
||||
size="large"
|
||||
start
|
||||
/>
|
||||
|
||||
Get Started
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
href="https://community.vuetifyjs.com/"
|
||||
min-width="164"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-account-group"
|
||||
size="large"
|
||||
start
|
||||
/>
|
||||
|
||||
Community
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
40
src/components/PairEntry.vue
Normal file
40
src/components/PairEntry.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<token-choice v-model="modelValue.tokenA" class="token-choice mb-1">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="modelValue.buy ? 'Buy' : 'Sell'" :color="modelValue.buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="modelValue.buy=!modelValue.buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
<token-choice v-model="modelValue.tokenB" class="token-choice">
|
||||
<template v-slot:prepend>
|
||||
<v-btn :text="!modelValue.buy ? 'Buy' : 'Sell'" :color="!modelValue.buy ? 'green' : 'red'"
|
||||
variant="outlined" @click="modelValue.buy=!modelValue.buy" class="bs-button"/>
|
||||
</template>
|
||||
</token-choice>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import TokenChoice from "@/components/TokenChoice.vue";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
// {
|
||||
// tokenA, tokenB, buy
|
||||
// }
|
||||
|
||||
const props = defineProps(['modelValue'])
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
.token-choice {
|
||||
width: 16em;
|
||||
}
|
||||
.bs-button {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
</style>
|
||||
177
src/components/TimedOrderEntry.vue
Normal file
177
src/components/TimedOrderEntry.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<v-card title="DCA / TWAP" subtitle="Split order across time" class="order-card" elevation="4">
|
||||
<v-card-text>
|
||||
<pair-entry v-model="pair"/>
|
||||
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
|
||||
:model-value="amount" :rules="[validateRequired,validateAmount]" class="amount">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn @click="amountIsBase=!amountIsBase" variant="outlined">
|
||||
{{ amountIsBase ? pair.tokenA.symbol : pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-btn :text="amountIsTotal ? 'Total' : 'Per Tranche'" variant="outlined"
|
||||
@click="amountIsTotal=!amountIsTotal" class="total"/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field label="Tranches" type="number" variant="outlined" aria-valuemin="1" min="1"
|
||||
:model-value="tranches" :rules="[validateRequired]">
|
||||
<template v-slot:prepend>
|
||||
<v-btn class="split-into" variant="outlined" @click="amountIsTotal=!amountIsTotal">
|
||||
{{ amountIsTotal ? 'Split into' : 'Times' }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field type="number" variant="outlined" :min="1" v-model="interval" class="interval"
|
||||
:label="intervalIsTotal ? 'Complete within' : 'Time between tranches'">
|
||||
<!-- <template v-slot:append>APART</template>-->
|
||||
<template v-slot:prepend>
|
||||
<v-btn variant="outlined" :text="intervalIsTotal ? 'Within' : 'Spaced apart'" class="within"
|
||||
@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="limitPrice" :label="(limitIsMinimum?'Minimum':'Maximum')+' Price'" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMin]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<!--
|
||||
<v-text-field v-model="minPrice" label="Minimum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMin]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
<v-text-field v-model="maxPrice" label="Maximum Price" type="number" variant="outlined" aria-valuemin="0" min="0"
|
||||
clearable :rules="[validateAmount, validateMax]">
|
||||
<template v-slot:append-inner>
|
||||
<v-btn variant="outlined" @click="inverted=!inverted">
|
||||
{{ inverted ? pair.tokenB.symbol + '/' + pair.tokenA.symbol : pair.tokenA.symbol + '/' + pair.tokenB.symbol }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
-->
|
||||
|
||||
</v-card-text>
|
||||
|
||||
<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">Place Order</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
import {computed, ref} from "vue";
|
||||
import PairEntry from "@/components/PairEntry.vue";
|
||||
|
||||
const s = useStore()
|
||||
const pair = ref({tokenA: s.tokens[0], tokenB: s.tokens[1], buy: true})
|
||||
const amount = ref(1)
|
||||
const amountIsBase = 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(10)
|
||||
const intervalIsTotal = ref(true)
|
||||
const timeUnits = ['minutes', 'hours', 'days']
|
||||
const timeUnitIndex = ref(1)
|
||||
|
||||
const limitIsMinimum = computed(()=>!(pair.value.buy ^ inverted.value))
|
||||
|
||||
|
||||
function toggleTimeUnits() {
|
||||
timeUnitIndex.value++
|
||||
if( timeUnitIndex.value >= timeUnits.length )
|
||||
timeUnitIndex.value = 0
|
||||
}
|
||||
|
||||
|
||||
function isEmpty(v) {
|
||||
return v === null || typeof v === 'string' && v.trim() === ''
|
||||
}
|
||||
|
||||
|
||||
function validateRequired(v) {
|
||||
if( isEmpty(v) )
|
||||
return 'Required'
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
console.log('validate max',v,isEmpty(v),minPrice,isEmpty(minPrice))
|
||||
if( !isEmpty(minPrice.value) && !isEmpty(v) && parseFloat(v) < parseFloat(minPrice.value) )
|
||||
return 'Must be greater than the minimum price'
|
||||
return true
|
||||
}
|
||||
|
||||
function validateMin(v) {
|
||||
console.log('validate min',v,isEmpty(v),maxPrice,isEmpty(maxPrice))
|
||||
if( !isEmpty(maxPrice.value) && !isEmpty(v) && parseFloat(v) > parseFloat(maxPrice.value) )
|
||||
return 'Must be less than the maximum price'
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@/styles/vars" as *;
|
||||
|
||||
.order-card {
|
||||
width: 25em;
|
||||
}
|
||||
|
||||
.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>
|
||||
18
src/components/TokenChip.vue
Normal file
18
src/components/TokenChip.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<v-chip>
|
||||
<!-- <v-avatar start :image="token.icon === null ? '' : token.icon"/>-->
|
||||
{{token.symbol}} {{token.name}}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore} from "@/store/store";
|
||||
|
||||
const s = useStore()
|
||||
const props = defineProps(['token'])
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
</style>
|
||||
105
src/components/TokenChoice.vue
Normal file
105
src/components/TokenChoice.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<v-combobox :items="s.tokens" :auto-select-first="true"
|
||||
item-title="symbol"
|
||||
:filter-keys="['raw.name','raw.symbol','raw.address']"
|
||||
:model-value="modelValue"
|
||||
:error-messages="errors"
|
||||
:loading="loading"
|
||||
:hide-selected="true"
|
||||
:label="label"
|
||||
v-bind="modelValue" @update:modelValue="updateValue"
|
||||
variant="outlined"
|
||||
>
|
||||
<template v-slot:prepend><slot name="prepend"/></template>
|
||||
<template v-slot:item="{props,item}">
|
||||
<v-list-item v-bind="props" :prepend-avatar="item.raw.image===null?'':item.raw.image" :title="item.raw.symbol" :subtitle="item.raw.name"/>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useStore as useStore2} from "@/store/store";
|
||||
import {ref} from "vue";
|
||||
import {ethers} from "ethers";
|
||||
|
||||
const s = useStore2()
|
||||
const props = defineProps(['modelValue', 'label'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const loading = ref(false)
|
||||
const errors = ref([])
|
||||
|
||||
function good() {
|
||||
errors.value = []
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
errors.value = [msg]
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function updateValue(v) {
|
||||
if( v === null ) {
|
||||
error('Type a token name, symbol, or address')
|
||||
}
|
||||
else if( typeof v !== 'string' ) {
|
||||
emit('update:modelValue', v);
|
||||
good()
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const addr = ethers.getAddress(v);
|
||||
const found = s.tokens[addr]
|
||||
if(found !== undefined ) {
|
||||
good()
|
||||
emit('update:modelValue', found)
|
||||
return
|
||||
}
|
||||
errors.value = []
|
||||
loading.value = true;
|
||||
addExtraToken(addr).then((info)=>{
|
||||
if(loading.value && errors.value.length===0) {
|
||||
good()
|
||||
emit('update:modelValue', info)
|
||||
}
|
||||
}).catch((e)=>{
|
||||
console.log(e)
|
||||
error(`${addr} is not a valid ERC20 token`)
|
||||
})
|
||||
}
|
||||
catch {
|
||||
error('Invalid token or address')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import {ethers} from "ethers";
|
||||
import wallet from "@/blockchain/wallet.js";
|
||||
import {erc20Abi} from "@/blockchain/abi.js";
|
||||
import {useStore} from "@/store/store.js";
|
||||
|
||||
const s = useStore()
|
||||
|
||||
async function addExtraToken(addr) {
|
||||
const token = new ethers.Contract(addr, erc20Abi, await wallet.provider())
|
||||
const symbol = await token.symbol()
|
||||
const decimals = Number(await token.decimals())
|
||||
const info = {name:`${symbol} (${addr})`, symbol, decimals, address:addr}
|
||||
s.$patch((state)=>{
|
||||
let extras = state.extraTokens[state.chain.id]
|
||||
if( extras === undefined ) {
|
||||
extras = {}
|
||||
state.extraTokens[state.chain.id] = extras
|
||||
}
|
||||
extras[info.address] = info
|
||||
})
|
||||
return info
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "src/styles/vars" as *;
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user