initial checkin with timed order ui

This commit is contained in:
Tim Olson
2023-08-27 21:01:17 -04:00
commit 986a876f86
39 changed files with 2592 additions and 0 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
VITE_WS_URL=ws://localhost:4334
VITE_GENERATED_URL=

42
build.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# build.sh sets these env vars as output:
# DEXORDER_WEB_VERSION
# DEXORDER_WEB_IMAGE
DOCKER=docker
CONTAINER=dexorder-web
SCRIPT_HOME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
WEB_HOME=${WEB_HOME:-$SCRIPT_HOME}
cd $WEB_HOME || exit 1
DIRTY="$( git status | grep "Changes " )"
if [ "$DIRTY" != "" ] && [ "$1" != "dev" ]; then
echo "$WEB_HOME has uncommited changes"
exit 1
fi
DEXORDER_WEB_VERSION="$( git log --oneline | head -1 | cut -d ' ' -f 1 )"
if [ "$1" == "dev" ]; then
shift
DEXORDER_WEB_VERSION="dev`date +%Y%m%d%H%M%S`"
fi
DEXORDER_WEB_IMAGE="gcr.io/cointrader-211623/$CONTAINER:$DEXORDER_WEB_VERSION"
echo $DEXORDER_WEB_IMAGE
echo "const dexorderWebVersion='$DEXORDER_WEB_VERSION'" > "$WEB_HOME/public/version.js"
npm run build
DOCKERFILE=deploy/Dockerfile
$DOCKER build -f "$DOCKERFILE" -t "$DEXORDER_WEB_IMAGE" .
if [ $? != 0 ]; then
echo 'container build failed'
exit 1
fi
export DEXORDER_WEB_VERSION
export DEXORDER_WEB_IMAGE
echo $(date) build complete

14
deploy.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
source ./build.sh $@
# build.sh sets these env vars:
# DEXORDER_WEB_VERSION
# DEXORDER_WEB_IMAGE
$DOCKER push $DEXORDER_WEB_IMAGE > /dev/null
if [ $? != 0 ]; then
echo 'container push failed'
exit 1
fi
sed -e "s|\$DEXORDER_WEB_IMAGE|$DEXORDER_WEB_IMAGE|g" k8s/deployment.yaml | kubectl --context=la apply -f -

5
deploy/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM nginx:stable-alpine
RUN sed -i '1idaemon off;' /etc/nginx/nginx.conf
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY dist /app
CMD ["nginx"]

25
deploy/nginx.conf Normal file
View File

@@ -0,0 +1,25 @@
server {
listen 80 default_server;
gzip on;
gzip_min_length 1000;
gzip_types text/plain text/xml application/javascript text/css;
root /app;
# normal routes
# serve given url and default to index.html if not found
# e.g. /, /user and /foo/bar will return index.html
location / {
add_header Cache-Control "no-store";
try_files $uri $uri/index.html /index.html;
}
# files
# for all routes matching a dot, check for files and return 404 if not found
# e.g. /file.js returns a 404 if not found
location ~ \.(?!html) {
add_header Cache-Control "public, max-age=2678400";
try_files $uri =404;
}
}

18
index.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Dexorder</title>
</head>
<body>
<div id="app"></div>
<script src="/version.js"></script>
<script src="/generated.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

19
jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es6",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

13
k8s/certificate.yaml Normal file
View File

@@ -0,0 +1,13 @@
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: dexorder-trade
spec:
secretName: dexorder-trade-tls
commonName: dexorder.trade
dnsNames:
- dexorder.trade
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer

30
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,30 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dexorder-web
labels:
app: dexorder-web
spec:
replicas: 1
selector:
matchLabels:
app: dexorder-web
template:
metadata:
labels:
app: dexorder-web
annotations:
prometheus.io/scrape: "false"
spec:
containers:
- name: dexorder-web
image: $DEXORDER_WEB_IMAGE
ports:
- name: www
containerPort: 80
protocol: TCP
resources:
requests:
cpu: 10m
memory: 100M

23
k8s/ingress.yaml Normal file
View File

@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dexorder-web
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
tls:
- secretName: dexorder-trade-tls
hosts:
- dexorder.trade
rules:
- host: dexorder.trade
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dexorder-web
port:
number: 80

15
k8s/le.yaml Normal file
View File

@@ -0,0 +1,15 @@
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: tim.olson.crypto@gmail.com
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-privkey-prod
solvers:
- http01:
ingress:
class: nginx

16
k8s/service.yaml Normal file
View File

@@ -0,0 +1,16 @@
---
apiVersion: v1
kind: Service
metadata:
name: dexorder-web
labels:
app: dexorder-web
spec:
type: NodePort
selector:
app: dexorder-web
ports:
- name: web
port: 80
targetPort: 80
protocol: TCP

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "dexorder",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"@mdi/font": "7.0.96",
"core-js": "^3.29.0",
"ethers": "^6.7.1",
"pinia": "^2.0.0",
"roboto-fontface": "*",
"vue": "^3.2.0",
"vue-router": "^4.0.0",
"vuetify": "^3.0.0",
"webfontloader": "^1.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.0.0",
"eslint": "^8.37.0",
"eslint-plugin-vue": "^9.3.0",
"sass": "^1.60.0",
"vite": "^4.2.0",
"vite-plugin-vuetify": "^1.0.0"
}
}

91
public/generated.js Normal file
View File

@@ -0,0 +1,91 @@
known_chains = [
{
name: 'Arbitrum One',
id: 42161,
icon: null,
}
]
_known_tokens = {
42161: [ // Arbitrum
{
name: 'Wrapped Ether',
symbol: 'WETH',
decimals: 18,
icon: null,
address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
},
{
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
icon: null,
address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
},
{
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
icon: null,
address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
},
{
name: 'Wrapped Bitcoin',
symbol: 'WBTC',
decimals: 8,
icon: null,
address: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
},
{
name: 'DAI Stablecoin',
symbol: 'DAI',
decimals: 18,
icon: null,
address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
},
{
name: 'Uniswap',
symbol: 'UNI',
decimals: 18,
icon: null,
address: '0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0',
},
{
name: 'Chainlink Token',
symbol: 'LINK',
decimals: 18,
icon: null,
address: '0xf97f4df75117a78c1A5a0DBb814Af92458539FB4',
},
{
name: 'TrueUSD',
symbol: 'TUSD',
decimals: 18,
icon: null,
address: '0x4D15a3A2286D883AF0AA1B3f21367843FAc63E07',
},
{
name: 'Lido DAO Token',
symbol: 'LDO',
decimals: 18,
icon: null,
address: '0x13Ad51ed4F1B7e9Dc168d8a00cB3f4dDD85EfA60',
},
{
name: 'Arbitrum',
symbol: 'ARB',
decimals: 18,
icon: null,
address: '0x912CE59144191C1204E64559FE8253a0e49E6548',
},
]
};
known_tokens = {}
for( const chainId in _known_tokens ) {
known_tokens[chainId] = {}
for( const info of _known_tokens[chainId] )
known_tokens[chainId][info.address] = info
}

6
src/App.vue Normal file
View File

@@ -0,0 +1,6 @@
<template>
<router-view/>
</template>
<script setup>
</script>

42
src/blockchain/abi.js Normal file
View File

@@ -0,0 +1,42 @@
export const erc20Abi = [
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'function transfer(address,uint256) returns (bool)',
'function transferFrom(address,address,uint256) returns (bool)',
'function approve(address,uint256) returns (bool success)',
'function allowance(address,address) view returns (uint256)',
'event Transfer(address indexed,address indexed,uint256)',
'event Approval(address indexed,address indexed,uint256)',
]
const TimedOrderSpec = '(' +
'address tokenIn,' +
'address tokenOut,' +
'uint24 fee,' +
'uint32 deadline,' +
'uint32 leeway,' +
'uint160 minSqrtPriceX96,' +
'uint160 maxSqrtPriceX96,' +
'uint8 numTranches,' +
'uint256 amount,' +
'bool amountIsInput' +
')'
export const timedOrderAbi = [
'event TimedOrderCreated (address owner, uint64 index, Spec spec)',
'event TimedOrderFilled (address owner, uint64 index, uint256 amountIn, uint256 amountOut)',
'event TimedOrderCompleted (address owner, uint64 index)',
'event TimedOrderError (address owner, uint64 index, string reason)',
`timedOrder(${TimedOrderSpec}) returns (uint64 index)`,
]
export const abi = {
'ERC20': erc20Abi,
'TimedOrder': timedOrderAbi,
}

112
src/blockchain/wallet.js Normal file
View File

@@ -0,0 +1,112 @@
import {ethers} from "ethers";
import {useStore} from "@/store/store";
const store = useStore()
class Wallet {
connected = false
_provider = null
_signer = null
_onSignerInits = {}
_enabled = true // prevents tight loops on errors
async connect() {
this._enabled = true
if( !this.connected )
await this.signer()
}
async provider() {
if (this._provider === null && this._enabled) {
console.log('creating provider')
const provider = new ethers.BrowserProvider(window.ethereum, store.chain.id)
await provider.getNetwork() // this invokes a check on having the correct network connected
this._provider = provider
console.log('wallet connected')
}
return this._provider
}
async signer() {
/*
if( !store.geo.approved ) {
console.log('not approved')
this._connected(false)
return null
}
*/
const provider = await this.provider()
if( provider === null ) {
console.log('provider null')
this._connected(false)
return null
}
const signer = await provider.getSigner()
if( this._signer?.address !== signer.address ) {
console.log('new signer', signer.address)
this._signer = signer
for (const key in this._onSignerInits) {
try {
await this._onSignerInits[key](signer)
}
catch (e) {
console.log('during onSignerInit', e)
}
}
console.log('wallet connected')
}
this._connected(true)
return signer
}
async address() {
const signer = await this.signer()
if( signer === null )
return null
return await signer.getAddress()
}
_connected(value) {
this.connected = value
store.$patch({wallet:{connected:value}})
}
onSignerInit(id, cb) {
this._onSignerInits[id] = cb
}
}
const handler = {
get(target, prop, proxy) {
const got = Reflect.get(target, prop, proxy);
if( typeof got !== 'function' ) {
return got
}
else {
return async function (...args) {
try {
return await got.apply(target, args)
}
catch (x) {
target._connected(false)
target._enabled = false
if( x.code === 'NETWORK_ERROR' ) {
store.error('Wrong Blockchain', 'Your wallet is connected to a different blockchain. Please select '+import.meta.env.VITE_CHAIN_NAME+' in your wallet.')
console.log('wallet network error', x)
}
else {
console.log('wallet error')
throw x
}
}
}
}
}
}
export default new Proxy(new Wallet(), handler)

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,18 @@
<template>
<v-chip>
<!-- <v-avatar start :image="token.icon === null ? '' : token.icon"/>-->
{{token.symbol}}&nbsp;{{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>

View 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>

View File

@@ -0,0 +1,11 @@
<template>
<v-app-bar flat>
<v-app-bar-title class="sc">
<v-icon icon="mdi-chart-line" />
DexOrder
</v-app-bar-title>
</v-app-bar>
</template>
<script setup>
</script>

View File

@@ -0,0 +1,11 @@
<template>
<v-app>
<default-bar />
<default-view />
</v-app>
</template>
<script setup>
import DefaultBar from './AppBar.vue'
import DefaultView from './View.vue'
</script>

View File

@@ -0,0 +1,10 @@
<template>
<v-main>
<v-container>
<router-view />
</v-container>
</v-main>
</template>
<script setup>
</script>

26
src/main.js Normal file
View File

@@ -0,0 +1,26 @@
/**
* main.js
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
// Plugins
import { registerPlugins } from '@/plugins'
import '@/styles/style.scss'
import {useStore} from "@/store/store.js";
const app = createApp(App)
registerPlugins(app)
const s = useStore()
s.chains = known_chains
s.chain = known_chains[0]
app.mount('#app')

19
src/plugins/index.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
// Plugins
import { loadFonts } from './webfontloader'
import vuetify from './vuetify'
import pinia from '../store'
import router from '../router'
export function registerPlugins (app) {
loadFonts()
app
.use(vuetify)
.use(router)
.use(pinia)
}

35
src/plugins/vuetify.js Normal file
View File

@@ -0,0 +1,35 @@
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
themes: {
light: {
colors: {
background: '#fffefd',
surface: '#fffefd',
primary: '#1A6CAB',
// secondary: '#59B8FF',
success: '#00CC33',
// info: '#fba92c',
warning: '#ffcc00',
error: '#CC0033',
},
dark: false,
variables: {},
},
// todo dark mode
},
},
})

View File

@@ -0,0 +1,17 @@
/**
* plugins/webfontloader.js
*
* webfontloader documentation: https://github.com/typekit/webfontloader
*/
export async function loadFonts () {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
webFontLoader.load({
google: {
// families: ['Roboto:100,300,400,500,700,900&display=swap'],
// families: ['Marcellus','Marcellus SC'],
families: ['Victor Mono', 'Tektur'],
},
})
}

26
src/router/index.js Normal file
View File

@@ -0,0 +1,26 @@
// Composables
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: () => import('@/layouts/default/Default.vue'),
children: [
{
path: '',
name: 'Home',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router

4
src/store/index.js Normal file
View File

@@ -0,0 +1,4 @@
// Utilities
import { createPinia } from 'pinia'
export default createPinia()

32
src/store/store.js Normal file
View File

@@ -0,0 +1,32 @@
// Utilities
import { defineStore } from 'pinia'
export const useStore = defineStore('app', {
state: () => ({
chain: {name:'Not Connected', id:0, icon:''},
wallet: {
connected: false,
},
errors: [],
extraTokens: {},
}),
getters: {
tokens: (s)=>{
const extras = s.extraTokens[s.chain.id]
return extras === undefined ? Object.values(known_tokens[s.chain.id])
: [...Object.values(known_tokens[s.chain.id]), ...Object.values(extras)]
},
},
actions: {
error(title, message) {
this.errors.push({title:title, message:message})
},
closeError(title, message) {
console.log('closing error', title, message)
const result = []
this.errors.forEach((i)=>{if(i.title!==title && i.message!==message) result.push(i)})
this.errors = result
},
}
})

14
src/styles/settings.scss Normal file
View File

@@ -0,0 +1,14 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
@use "src/styles/vars" as v;
// https://vuetifyjs.com/features/sass-variables/`
@forward 'vuetify/settings' with (
//$variable: false,
$body-font-family: v.$body-font-family,
$heading-font-family: v.$heading-font-family,
);

26
src/styles/style.scss Normal file
View File

@@ -0,0 +1,26 @@
@use "/src/styles/vars" as v;
//.app {
// .v-btn {
// font-family: v.$heading-font-family;
// text-transform: none;
// }
//
// .v-card-title {
// font-family: v.$heading-font-family;
// color: v.$blue;
// }
//
// .title {
// font-family: v.$heading-font-family;
// }
//
// .v-list-subheader {
// font-family: v.$heading-font-family;
// }
//}
.sc {
font-family: v.$heading-font-family;
}

38
src/styles/vars.scss Normal file
View File

@@ -0,0 +1,38 @@
// these must also be set in vuetify.ts for the "theme"
$green: #00CC33;
$red: #CC0033;
$yellow: #ffcc00;
$blue: #0033CC;
$white: #fffefd; // just a touch green
$black: #000102;
$primary: $blue;
$primary-50: transparentize($primary,0.5);
$primary-25: transparentize($primary,0.75);
$theme: (
background: $white,
surface: $white,
primary: $blue,
//secondary: $sky,
success: $green,
info: $primary-25,
warning: $yellow,
error: $red,
);
$all-colors: (
white: $white,
black: $black,
blue: $blue,
green: $green,
yellow: $yellow,
red: $red,
);
$body-font-family: 'Victor Mono', monospace, sans-serif;
$heading-font-family: 'Tektur', sans-serif;
$sm-breakpoint: 600px;
$card-maxw: 30rem;

7
src/views/Home.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<TimedOrder/>
</template>
<script setup>
import TimedOrder from "@/components/TimedOrderEntry.vue";
</script>

41
vite.config.js Normal file
View File

@@ -0,0 +1,41 @@
// Plugins
import vue from '@vitejs/plugin-vue'
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: { transformAssetUrls }
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
],
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
})

1335
yarn.lock Normal file

File diff suppressed because it is too large Load Diff