From d38baccd49e5f208f8d39003cd6b368ec82e3802 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 3 Jul 2024 16:18:29 -0400 Subject: [PATCH] ABI's from //contract/out URLs; arbsep; placement fee considers upcoming fee changes; vault detection bugfixes; order placement bugfixes; BETA --- build.sh | 2 +- deploy.sh | 4 +- src/blockchain/abi.js | 109 ------------------------------- src/blockchain/contract.js | 23 ++++++- src/blockchain/orderlib.js | 13 +++- src/blockchain/prices.js | 7 +- src/blockchain/token.js | 5 +- src/blockchain/uniswap.js | 9 ++- src/blockchain/wallet.js | 50 +++++++++----- src/common.js | 98 +++++++++++++++++++++++++++ src/components/Alpha.vue | 8 --- src/components/Beta.vue | 8 +++ src/components/Logo.vue | 6 +- src/components/NeedsSigner.vue | 2 +- src/components/Withdraw.vue | 5 +- src/components/chart/Toolbar.vue | 4 +- src/corp/Home.vue | 13 ++-- src/corp/HowItWorks.vue | 4 -- 18 files changed, 200 insertions(+), 170 deletions(-) delete mode 100644 src/blockchain/abi.js delete mode 100644 src/components/Alpha.vue create mode 100644 src/components/Beta.vue diff --git a/build.sh b/build.sh index 55ab52f..f8cf7fd 100755 --- a/build.sh +++ b/build.sh @@ -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 diff --git a/deploy.sh b/deploy.sh index 7cbe000..2388075 100755 --- a/deploy.sh +++ b/deploy.sh @@ -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 diff --git a/src/blockchain/abi.js b/src/blockchain/abi.js deleted file mode 100644 index b0bf605..0000000 --- a/src/blockchain/abi.js +++ /dev/null @@ -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)', -] diff --git a/src/blockchain/contract.js b/src/blockchain/contract.js index 2550454..7dac57a 100644 --- a/src/blockchain/contract.js +++ b/src/blockchain/contract.js @@ -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) +} diff --git a/src/blockchain/orderlib.js b/src/blockchain/orderlib.js index 207cd51..5dd9e81 100644 --- a/src/blockchain/orderlib.js +++ b/src/blockchain/orderlib.js @@ -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 + } +} + diff --git a/src/blockchain/prices.js b/src/blockchain/prices.js index bb76c25..f42d289 100644 --- a/src/blockchain/prices.js +++ b/src/blockchain/prices.js @@ -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) diff --git a/src/blockchain/token.js b/src/blockchain/token.js index f6257b5..90a575f 100644 --- a/src/blockchain/token.js +++ b/src/blockchain/token.js @@ -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, diff --git a/src/blockchain/uniswap.js b/src/blockchain/uniswap.js index e37ccdf..2ae1f67 100644 --- a/src/blockchain/uniswap.js +++ b/src/blockchain/uniswap.js @@ -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', }, } diff --git a/src/blockchain/wallet.js b/src/blockchain/wallet.js index 6a75965..bee12a3 100644 --- a/src/blockchain/wallet.js +++ b/src/blockchain/wallet.js @@ -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) } } diff --git a/src/common.js b/src/common.js index be40122..d125339 100644 --- a/src/common.js +++ b/src/common.js @@ -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` +} diff --git a/src/components/Alpha.vue b/src/components/Alpha.vue deleted file mode 100644 index c1c5eec..0000000 --- a/src/components/Alpha.vue +++ /dev/null @@ -1,8 +0,0 @@ - - \ No newline at end of file diff --git a/src/components/Beta.vue b/src/components/Beta.vue new file mode 100644 index 0000000..fb845c0 --- /dev/null +++ b/src/components/Beta.vue @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/src/components/Logo.vue b/src/components/Logo.vue index 3572d36..309ab34 100644 --- a/src/components/Logo.vue +++ b/src/components/Logo.vue @@ -2,16 +2,16 @@ - + diff --git a/src/components/NeedsSigner.vue b/src/components/NeedsSigner.vue index 6f3fc42..3c68620 100644 --- a/src/components/NeedsSigner.vue +++ b/src/components/NeedsSigner.vue @@ -3,7 +3,7 @@ - Welcome to Dexorder Alpha! + Welcome to Dexorder Beta! This alpha test runs on the Dexorder Testnet blockchain, which gives you free testnet tokens to trade. diff --git a/src/components/Withdraw.vue b/src/components/Withdraw.vue index 9d7d9a2..336288f 100644 --- a/src/components/Withdraw.vue +++ b/src/components/Withdraw.vue @@ -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 diff --git a/src/components/chart/Toolbar.vue b/src/components/chart/Toolbar.vue index 809bc2a..47d3f18 100644 --- a/src/components/chart/Toolbar.vue +++ b/src/components/chart/Toolbar.vue @@ -1,6 +1,6 @@