ABI's from //contract/out URLs; arbsep; placement fee considers upcoming fee changes; vault detection bugfixes; order placement bugfixes; BETA

This commit is contained in:
Tim
2024-07-03 16:18:29 -04:00
parent 104b798d4f
commit d38baccd49
18 changed files with 200 additions and 170 deletions

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# build.sh sets these env vars as output:
# build sets these env vars as output:
# DEXORDER_WEB_VERSION
# DEXORDER_WEB_IMAGE

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
source ./build.sh $@
# build.sh sets these env vars:
source ./build.sh "$@"
# build sets these env vars:
# DEXORDER_WEB_VERSION
# DEXORDER_WEB_IMAGE

View File

@@ -1,109 +0,0 @@
export const factoryAbi = [
'function deployVault() returns (address payable vault)',
'function deployVault(uint8 num) returns (address payable vault)',
'function deployVault(address owner) returns (address payable vault)',
'function deployVault(address owner, uint8 num) returns (address payable vault)',
'function logic() view returns (address)',
'function upgrader() view returns (address)',
'function proposedLogicActivationTimestamp() view returns (uint32)',
'function proposedLogic() view returns (address)',
'function upgradeLogic(address newLogic)',
]
export const queryHelperAbi = [
'function getRoutes(address tokenA,address tokenB) view returns((uint8,uint24,address)[])',
]
export const uniswapV3PoolAbi = [
// {
// // the current price
// uint160 sqrtPriceX96;
// // the current tick
// int24 tick;
// // the most-recently updated index of the observations array
// uint16 observationIndex;
// // the current maximum number of observations that are being stored
// uint16 observationCardinality;
// // the next maximum number of observations to store, triggered in observations.write
// uint16 observationCardinalityNext;
// // the current protocol fee as a percentage of the swap fee taken on withdrawal
// // represented as an integer denominator (1/x)%
// uint8 feeProtocol;
// // whether the pool is locked
// bool unlocked;
// }
'function slot0() view returns(uint160,int24,uint16,uint16,uint16,uint8,bool)',
'function token0() view returns(address)',
'function token1() view returns(address)',
]
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)',
]
export const mockErc20Abi = [...erc20Abi,
'function mint(address,uint256)',
]
const Route = '(uint8 exchange, uint24 fee)'
const Tranche = `(
uint16 fraction,
bool startTimeIsRelative,
bool endTimeIsRelative,
bool minIsBarrier,
bool maxIsBarrier,
bool marketOrder,
bool minIsRatio,
bool maxIsRatio,
bool _reserved7,
uint16 rateLimitFraction,
uint24 rateLimitPeriod,
uint32 startTime,
uint32 endTime,
uint32 minIntercept,
uint32 minSlope,
uint32 maxIntercept,
uint32 maxSlope
)`
const SwapOrder = `(
address tokenIn,
address tokenOut,
${Route} route,
uint256 amount,
uint256 minFillAmount,
bool amountIsInput,
bool outputDirectlyToOwner,
uint64 conditionalOrder,
${Tranche}[] tranches
)`
export const vaultAbi = [
'function version() pure returns (uint8)',
'function logic() view returns (address)',
'function upgrade(address)',
'function feeManager() view returns (address)',
'function withdraw(uint256 amount)',
'function withdrawTo(address payable recipient, uint256 amount)',
'function withdraw(address token, uint256 amount)',
'function withdrawTo(address token, address recipient, uint256 amount)',
'function numSwapOrders() view returns (uint64 num)',
`function placementFee(${SwapOrder} order) view returns (uint256 orderFee, uint256 gasFee)`,
`function placeDexorder(${SwapOrder}) payable`,
`function placeDexorders(${SwapOrder}[], uint8 ocoMode) payable`,
'function cancelDexorder(uint64 orderIndex)',
'function cancelAllDexorders()',
// `function swapOrderStatus(uint64 orderIndex) view returns (${SwapOrderStatus} memory status)`,
'function orderCanceled(uint64 orderIndex) view returns (bool)',
]

View File

@@ -1,5 +1,7 @@
import {ethers} from "ethers";
import {queryHelperAbi} from "@/blockchain/abi.js";
import {AbiURLCache} from "../common.js";
export const abiCache = new AbiURLCache('/contract/out/')
export function vaultAddress( factory, vaultInitCodeHash, owner, num=0) {
@@ -20,6 +22,23 @@ export function contractOrNull(addr,abi,provider) {
}
export async function queryHelperContract(helper, provider) {
return contractOrNull(helper, queryHelperAbi, provider)
return newContract(helper, 'QueryHelper', provider)
}
// do not supply extensions with name or file: e.g.
// use newContract(addr, 'IVaultLogic', provider, 'IVault') to get the ABI from IVault.sol/IVaultLogic.json
export async function newContract(addr, name, provider) {
const abi = await abiCache.get(name)
return new ethers.Contract(addr, abi, provider)
}
export async function erc20Contract(addr, provider) {
return newContract(addr, 'IERC20Metadata', provider)
}
export async function vaultContract(addr, provider) {
return await newContract(addr, 'IVault', provider)
}

View File

@@ -70,8 +70,8 @@ export function newTranche({
endTime = DISTANT_FUTURE,
minIsBarrier = false,
minIsRatio = false,
minIntercept = 0,
slippage = 0, // may also set minIntercept instead
minIntercept = 0,
minSlope = 0,
maxIsBarrier = false,
maxIsRatio = false,
@@ -200,3 +200,14 @@ export function parseTranche(tranche) {
startTime, endTime, minIntercept, minSlope, maxIntercept, maxSlope,
}
}
export function parseFeeSchedule(sched) {
const [orderFee, orderExp, gasFee, gasExp, fillFeeHalfBps] = sched
return {
orderFee: orderFee << orderExp, // orderFee is in native (ETH) currency
gasFee: gasFee << gasExp, // gasFee is in native (ETH) currency
fillFee: fillFeeHalfBps/1_000_000 // fillFee is a multiplier on the filled volume. 0.0001 = 0.1% of the output token taken as a fee
}
}

View File

@@ -2,10 +2,9 @@ import {socket} from "@/socket.js";
import {useStore} from "@/store/store.js";
import {Exchange} from "@/blockchain/orderlib.js";
import {uniswapV3PoolAddress} from "@/blockchain/uniswap.js";
import {ethers, FixedNumber} from "ethers";
import {uniswapV3PoolAbi} from "@/blockchain/abi.js";
import {subOHLCs} from "@/blockchain/ohlcs.js";
import {FixedNumber} from "ethers";
import {provider} from "@/blockchain/wallet.js";
import {newContract} from "@/blockchain/contract.js";
const subscriptionCounts = {} // key is route and value is a subscription counter
export const WIDE_PRICE_FORMAT = {decimals:38, width:512, signed:false}; // 38 decimals is 127 bits
@@ -84,7 +83,7 @@ async function getPriceForRoute(route) {
console.error('provider was null during getPriceForRoute')
return null
}
const pool = new ethers.Contract(addr, uniswapV3PoolAbi, provider)
const pool = newContract(addr, 'IUniswapV3Pool', provider)
const got = await pool.slot0()
const [sqrtPrice,,,,,,] = got
const spn = BigInt(sqrtPrice)

View File

@@ -1,9 +1,8 @@
import {socket} from "@/socket.js";
import {useStore} from "@/store/store.js";
import {erc20Abi} from "@/blockchain/abi.js";
import {ethers} from "ethers";
import {metadataMap} from "@/version.js";
import {provider} from "@/blockchain/wallet.js";
import {newContract} from "@/blockchain/contract.js";
// synchronous version may return null but will trigger a lookup
@@ -64,7 +63,7 @@ export async function addExtraToken(chainId, addr) {
resolve(null)
}
else {
const token = new ethers.Contract(addr, erc20Abi, provider)
const token = newContract(addr, 'IERC20Metadata', provider)
Promise.all( [token.name(), token.symbol(), token.decimals()] ).then((name,symbol,decimals)=>{
info = {
a: addr,

View File

@@ -2,13 +2,16 @@ import {ethers} from "ethers";
const UNISWAPV3_POOL_INIT_CODE_HASH = '0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54'
const uniswapV3Addresses = {
1337: {
31337: { // Mockchain
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
},
42161: {
1337: { // Dexorder Alpha
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
},
31337: {
421614: { // Arbitrum Sepolia
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
},
42161: { // Arbitrum
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
},
}

View File

@@ -1,9 +1,8 @@
import {BrowserProvider, ethers} from "ethers";
import {useStore} from "@/store/store";
import {socket} from "@/socket.js";
import {contractOrNull, vaultAddress} from "@/blockchain/contract.js";
import {SingletonCoroutine, uuid} from "@/misc.js";
import {factoryAbi, vaultAbi} from "@/blockchain/abi.js";
import {newContract, vaultAddress, vaultContract} from "@/blockchain/contract.js";
import {SingletonCoroutine, timestamp, uuid} from "@/misc.js";
import {defineStore} from "pinia";
import {ref} from "vue";
import {metadataMap, version} from "@/version.js";
@@ -217,7 +216,7 @@ async function _discoverVaults(owner) {
console.log('No provider')
return // do not change whatever was already found
}
const vault = new ethers.Contract(addr, vaultAbi, provider)
const vault = await vaultContract(addr, provider)
try {
const version = Number(await vault.version())
console.log(`found vault #${num} v${version} at ${addr}`)
@@ -295,11 +294,24 @@ export const PendingOrderState = {
Sent: -102, // tx is awaiting blockchain mining
}
const placementFeeSelector = 'placementFee((address,address,(uint8,uint24),uint256,uint256,bool,bool,uint64,(uint16,bool,bool,bool,bool,bool,bool,bool,bool,uint16,uint24,uint32,uint32,uint32,uint32,uint32,uint32)[]),(uint8,uint8,uint8,uint8,uint8))'
export async function placementFee(vault, order) {
const v = new ethers.Contract(vault, vaultAbi, provider)
const [orderFee, gasFee] = await v.placementFee(order)
console.log('computed fees', orderFee, gasFee)
export async function placementFee(vault, order, window=300) {
// If the fees are about to change within `window` seconds of now, we send the higher native amount of the two fees.
// If the fees sent are too much, the vault will refund the sender.
const v = await vaultContract(vault, provider)
const feeManagerAddr = await v.feeManager()
const feeManager = await newContract(feeManagerAddr, 'IFeeManager', provider)
const [sched, changeTimestamp] = await Promise.all([feeManager.fees(), feeManager.proposedFeeActivationTime()])
console.log('sched', order, sched)
let [orderFee, gasFee] = await v[placementFeeSelector](order, [...sched])
console.log('placementFee', orderFee, gasFee)
if (Number(changeTimestamp) - timestamp() < window) {
const nextSched = await feeManager.proposedFees()
const [nextOrderFee, nextGasFee] = await v[placementFeeSelector](order, [...nextSched])
if (nextOrderFee + nextGasFee > orderFee + gasFee)
[orderFee, gasFee] = [nextOrderFee, nextGasFee]
}
return [orderFee, gasFee]
}
@@ -324,7 +336,7 @@ export async function pendOrder(order, fee=null) {
export async function cancelOrder(vault, orderIndex) {
console.log('cancel order', vault, orderIndex)
pendTransaction(async (signer)=> {
const contract = contractOrNull(vault, vaultAbi, signer)
const contract = await vaultContract(vault, signer)
if( contract === null ) {
console.error('vault contract was null while canceling order', vault, orderIndex)
return null
@@ -335,7 +347,7 @@ export async function cancelOrder(vault, orderIndex) {
export async function cancelAll(vault) {
pendTransaction(async (signer)=> {
const contract = contractOrNull(vault, vaultAbi, signer)
const contract = await vaultContract(vault, signer)
if( contract === null ) {
console.error('vault contract was null while canceling order', vault)
return null
@@ -364,8 +376,12 @@ export function flushOrders(vault) {
function pendOrderAsTransaction(pend) {
pendTransaction(async (signer)=> {
const contract = contractOrNull(pend.vault, vaultAbi, signer)
if( contract === null ) {
// console.log('pendTransaction')
let contract
try {
contract = await vaultContract(pend.vault, signer)
}
catch (e) {
console.error('vault contract was null while sending order transaction', pend.vault)
return null
}
@@ -491,15 +507,17 @@ export async function detectUpgrade() {
}
try {
console.log('factory', info.factory)
const factory = new ethers.Contract(info.factory, factoryAbi, provider)
const vault = new ethers.Contract(s.vault, vaultAbi, provider)
const [factory, vault] = await Promise.all([
newContract(info.factory, 'IVaultFactory', provider),
newContract(s.vault, 'IVault', provider),
])
const vaultLogic = await vault.logic()
const latestLogic = await factory.logic()
// const [vaultLogic, latestLogic] = await Promise.all( vault.logic(), factory.logic() )
console.log('vaultLogic / latestLogic', vaultLogic, latestLogic)
if ( vaultLogic !== latestLogic ) {
s.upgrade = latestLogic
const logic = new ethers.Contract(latestLogic, vaultAbi, provider)
const logic = await newContract(latestLogic, 'IVault', provider)
const version = await logic.version()
console.log(`found vault version ${version}`)
return version
@@ -514,7 +532,7 @@ export async function detectUpgrade() {
function upgradeSender(vault, logic) {
return async function (signer) {
const v = new ethers.Contract(vault, vaultAbi, signer)
const v = await vaultContract(vault, signer)
v.upgrade(logic)
}
}

View File

@@ -1,3 +1,5 @@
import * as fs from "node:fs";
export function mixin(child, ...parents) {
// child is modified directly, assigning fields from parents that are missing in child. parents fields are
// assigned by parents order, highest priority first
@@ -41,3 +43,99 @@ export function decodeIEE754(value) {
view.setUint32(0, value, false)
return view.getFloat32(0, false)
}
//
// AsyncCache
//
export class AsyncCache {
// fetch(key) returns a value
constructor(fetch) {
this.cache = {}
this.fetchLocks = {}
this.fetch = fetch
}
async get(key) {
if (this.cache[key]) {
return this.cache[key]
}
if (this.fetchLocks[key]) {
return await this.fetchLocks[key]
}
const fetchPromise = this.fetch(key)
this.fetchLocks[key] = fetchPromise
const result = await fetchPromise
this.cache[key] = result
delete this.fetchLocks[key]
return result
}
}
export class AsyncAbiCache extends AsyncCache {
constructor(fetch) {
super(async (key)=>{
const result = await fetch(key)
return result.abi
});
}
}
export class AsyncURLCache extends AsyncAbiCache {
constructor(urlForKey) {
super(async (key) => {
const URL = this.urlForKey(key)
const response = await fetch(URL)
if (!response.ok)
throw new Error(`Could not fetch ${URL} (status ${response.status})`)
return await response.json()
})
this.urlForKey = urlForKey
}
}
export class AsyncFileCache extends AsyncAbiCache {
constructor(pathForKey) {
super(async (key) => {
const path = this.pathForKey(key)
const data = fs.readFileSync(path, 'utf8');
return JSON.parse(data);
})
this.pathForKey = pathForKey
}
}
export class AbiURLCache extends AsyncURLCache {
constructor(baseUrl) {
super((name)=>{
return this.baseUrl+abiPath(name)
})
this.baseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'
}
}
export class AbiFileCache extends AsyncFileCache {
constructor(basePath) {
super((name)=>{
return this.basePath+abiPath(name)
})
this.basePath = basePath.endsWith('/') ? basePath : basePath + '/'
}
}
const files = {
// If a contract is in a file different than its name, put the exception here
// 'IVaultLogic' : 'IVault', // for example
}
function abiPath(name) {
const file = files[name]
return `${file?file:name}.sol/${name}.json`
}

View File

@@ -1,8 +0,0 @@
<template>
<v-chip text="ALPHA" size='x-small' color="red" class="align-self-start" variant="text"/>
</template>
<script>
export default {
name: 'alpha'
}
</script>

8
src/components/Beta.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<v-chip text="BETA" size='x-small' color="red" class="align-self-start" variant="text"/>
</template>
<script>
export default {
name: 'beta'
}
</script>

View File

@@ -2,16 +2,16 @@
<span class="d-inline-flex align-center">
<v-icon icon="mdi-arrow-up-bold" color="primary" class="arrow"/>
<span class="logo">dexorder</span>
<alpha v-if="alpha"/>
<beta v-if="showTag"/>
</span>
</template>
<script setup>
import Alpha from "@/components/Alpha.vue";
import Beta from "@/components/Beta.vue";
const props = defineProps({
alpha: {type: Boolean, default: false}
showTag: {type: Boolean, default: false}
})
</script>

View File

@@ -3,7 +3,7 @@
<v-card v-if="!loggedIn" rounded="0">
<v-card-title>
<!-- <v-icon icon="mdi-hand-wave" color="grey"/>-->
Welcome to Dexorder Alpha!
Welcome to Dexorder Beta!
</v-card-title>
<v-card-text>
This alpha test runs on the Dexorder Testnet blockchain, which gives you free testnet tokens to trade.

View File

@@ -27,8 +27,7 @@
import {useStore} from "@/store/store";
import {computed, ref} from "vue";
import {tokenFloat} from "@/misc.js";
import {contractOrNull} from "@/blockchain/contract.js"
import {vaultAbi} from "@/blockchain/abi.js";
import {vaultContract} from "@/blockchain/contract.js"
import {pendTransaction} from "@/blockchain/wallet.js";
import {FixedNumber} from "ethers";
@@ -50,7 +49,7 @@ function withdraw() {
if( amount === 0n )
return
pendTransaction(async (signer)=>{
const vault = contractOrNull(vaultAddr, vaultAbi, signer)
const vault = await vaultContract(vaultAddr, signer)
return await vault['withdraw(address,uint256)'](props.token.a, amount)
})
floatAmount.value = 0

View File

@@ -1,6 +1,6 @@
<template>
<div class="d-flex mb-1 align-center w-100">
<logo class="d-flex align-end clickable logo-large" @click="nav('Order')" :alpha="true"/>
<logo class="d-flex align-end clickable logo-large" @click="nav('Order')" :show-tag="true"/>
<slot/>
<div class="ml-auto d-flex align-center">
<span class="title mr-4">{{title}}</span>
@@ -19,7 +19,7 @@ import {useStore} from "@/store/store.js";
import {useChartOrderStore} from "@/orderbuild.js";
import {useTheme} from "vuetify";
import ToolbarButton from "@/components/chart/ToolbarButton.vue";
import Alpha from "@/components/Alpha.vue";
import beta from "@/components/Beta.vue";
import Logo from "@/components/Logo.vue";
import {nav} from "@/misc.js";

View File

@@ -12,11 +12,11 @@
<v-list>
<v-list-item>
<uniswap-logo class="mr-2"/>
Supports Uniswap v3 <alpha/>
Supports Uniswap v3 <beta/>
</v-list-item>
<v-list-item><v-icon icon="mdi-ray-vertex" class="mr-2"/> Limit Orders <alpha/></v-list-item>
<v-list-item><v-icon icon="mdi-clock-outline" class="mr-2"/> DCA and TWAP Orders <alpha/></v-list-item>
<v-list-item><v-icon icon="mdi-vector-line" class="mr-2"/> Diagonal Limit Orders <alpha/></v-list-item>
<v-list-item><v-icon icon="mdi-ray-vertex" class="mr-2"/> Limit Orders <beta/></v-list-item>
<v-list-item><v-icon icon="mdi-clock-outline" class="mr-2"/> DCA and TWAP Orders <beta/></v-list-item>
<v-list-item><v-icon icon="mdi-vector-line" class="mr-2"/> Diagonal Limit Orders <beta/></v-list-item>
<v-list-item><v-icon icon="mdi-chart-timeline-variant-shimmer" class="mr-2"/> Stop Loss / Take Profit <soon/></v-list-item>
<v-list-item><v-icon icon="mdi-format-list-checks" class="mr-2"/> One-Cancels-the-Other (OCO) Groups <soon/></v-list-item>
<v-list-item><v-icon icon="mdi-cancel" class="mr-2"/> Single-transaction Cancel All <soon/></v-list-item>
@@ -33,17 +33,14 @@
<script setup>
import {useTheme} from "vuetify";
import Alpha from "@/components/Alpha.vue";
import beta from "@/components/Beta.vue";
import Soon from "@/components/Soon.vue";
import HowItWorks from "@/corp/HowItWorks.vue";
import HomeActions from "@/corp/HomeActions.vue";
import UniswapLogo from "@/corp/UniswapLogo.vue";
import Logo from "@/components/Logo.vue";
import {nav} from "@/misc.js";
import AppBtn from "@/corp/AppBtn.vue";
const theme = useTheme().current
</script>
<style scoped lang="scss">

View File

@@ -71,11 +71,7 @@
<script setup>
import Alpha from "@/components/Alpha.vue";
import Soon from "@/components/Soon.vue";
import UniswapLogo from "@/corp/UniswapLogo.vue";
import Logo from "@/components/Logo.vue";
import HomeActions from "@/corp/HomeActions.vue";
import {nav} from "@/misc.js";
import AppBtn from "@/corp/AppBtn.vue";
</script>