diff --git a/src/blockchain/abi.js b/src/blockchain/abi.js index 89be73b..aee6a63 100644 --- a/src/blockchain/abi.js +++ b/src/blockchain/abi.js @@ -43,32 +43,16 @@ export const erc20Abi = [ 'event Approval(address indexed,address indexed,uint256)', ] +const Route = '(uint8,uint24)' +const Constraint = '(uint8,bytes)' +const Tranche = `(uint64,${Constraint}[])` +const SwapOrder = `(address,address,${Route},uint256,bool,bool,uint64,${Tranche}[])` -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 vaultAbi = [ + 'function withdraw(uint256) public', + 'function withdrawTo(address payable,uint256) public', + 'function withdraw(address,uint256) public', + 'function withdrawTo(address,address,uint256) public', + `function placeOrder(${SwapOrder}) public`, + `function placeOrders(${SwapOrder}[],uint8) public`, ] - - -export const abi = { - 'ERC20': erc20Abi, - 'TimedOrder': timedOrderAbi, - 'QueryHelper': queryHelperAbi, -} - diff --git a/src/blockchain/contract.js b/src/blockchain/contract.js index 6983e1f..3639b65 100644 --- a/src/blockchain/contract.js +++ b/src/blockchain/contract.js @@ -1,20 +1,48 @@ import {ethers} from "ethers"; -import {factoryAbi, queryHelperAbi} from "@/blockchain/abi.js"; +import {factoryAbi, queryHelperAbi, vaultAbi} from "@/blockchain/abi.js"; import {useStore} from "@/store/store.js"; import {provider} from "@/blockchain/wallet.js"; +export function vaultAddress( owner, num ) { + const s = useStore() + if( s.vaultInitCodeHash === null || s.factory === null ) + return null + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + console.log('vaultAddress owner', owner) + const salt = ethers.keccak256(abiCoder.encode(['address','uint8'],[owner,num])) + const result = ethers.getCreate2Address(s.factory, salt, s.vaultInitCodeHash) + console.log('vaultAddress', result, s.factory, salt, s.vaultInitCodeHash) + return result +} + + +function contractOrNull(addr,abi,provider) { + try { + return new ethers.Contract(addr,abi,provider) + } + catch (e) { + return null + } +} + export async function factoryContract() { const s = useStore() - return new ethers.Contract(s.helper, factoryAbi, provider) + return contractOrNull(s.factory, factoryAbi, provider) } export async function queryHelperContract() { const s = useStore() - return new ethers.Contract(s.helper, queryHelperAbi, provider) + return contractOrNull(s.helper, queryHelperAbi, provider) } export async function poolContract(addr) { - const s = useStore() - return new ethers.Contract(addr, poolAbi, provider) + return contractOrNull(addr, poolAbi, provider) +} + +export async function vaultContract(num, signer) { + const s = useStore() + if( num >= s.vaults.length ) + return null + return contractOrNull(s.vaults[num], vaultAbi, signer) } diff --git a/src/blockchain/orderlib.js b/src/blockchain/orderlib.js new file mode 100644 index 0000000..2ce976e --- /dev/null +++ b/src/blockchain/orderlib.js @@ -0,0 +1,114 @@ + +import {uint32max, uint64max} from "@/misc.js"; +import {ethers} from "ethers"; + +export const NO_CHAIN = uint64max; +export const NO_OCO = uint64max; + +// struct SwapOrder { +// address tokenIn; +// address tokenOut; +// Route route; +// uint256 amount; +// bool amountIsInput; +// bool outputDirectlyToOwner; +// uint64 chainOrder; // use NO_CHAIN for no chaining. chainOrder index must be < than this order's index for safety (written first) and chainOrder state must be Template +// Tranche[] tranches; +// } +// struct Route { +// Exchange exchange; +// uint24 fee; +// } +export function newOrder(tokenIn, tokenOut, exchange, fee, amount, amountIsInput, tranches, + outputToOwner=false, chainOrder=NO_CHAIN) { + if(!tranches) + tranches = [newTranche(1,[])] // todo this is just a swap: issue warning? + return [ + tokenIn, tokenOut, [exchange,fee], amount, amountIsInput, outputToOwner, chainOrder, tranches + ] +} + +// struct Tranche { +// uint64 fraction; // 18-decimal fraction of the order amount which is available to this tranche. must be <= 1 +// Constraint[] constraints; +// } +export function newTranche(amountRatio, constraints) { + return [ + BigInt(Math.ceil(amountRatio * 10**18)), // we use ceil to make sure the sum of tranche fractions doesn't round below 1 + constraints + ] +} + +// enum Exchange { +// UniswapV2, +// UniswapV3 +// } +export const Exchange = { + UniswapV2: 0, + UniswapV3: 1, +} + +// enum ConstraintMode { +// Time, // 0 +// Limit, // 1 +// Trailing, // 2 +// Barrier, // 3 +// Line // 4 +// } +export const ConstraintMode = { + Time: 0, + Limit: 1, + Trailing: 2, + Barrier: 3, + Line: 4, +} + +// struct Constraint { +// ConstraintMode mode; +// bytes constraint; // abi-encoded constraint struct +// } + +function encodeConstraint( constraintMode, types, values ) { + return [constraintMode, ethers.AbiCoder.defaultAbiCoder().encode(types,values)] +} + + +export const TimeMode = { + Timestamp:0, + SinceOrderStart:1, +} + +export const DISTANT_PAST = 0 +export const DISTANT_FUTURE = uint32max + +// struct Time { +// TimeMode mode; +// uint32 time; +// } +// struct TimeConstraint { +// Time earliest; +// Time latest; +// } +export function newTimeConstraint(startMode, start, endMode, end) { + // absolute time + return encodeConstraint( + ConstraintMode.Time, + ['uint8', 'uint32', 'uint8', 'uint32'], + [startMode, start, endMode, end] + ) +} + +// struct PriceConstraint { +// bool isAbove; +// bool isRatio; +// uint160 valueSqrtX96; +// } + +// struct LineConstraint { +// bool isAbove; +// bool isRatio; +// uint32 time; +// uint160 valueSqrtX96; +// int160 slopeSqrtX96; // price change per second +// } + diff --git a/src/blockchain/wallet.js b/src/blockchain/wallet.js index 95738ba..289e11b 100644 --- a/src/blockchain/wallet.js +++ b/src/blockchain/wallet.js @@ -1,14 +1,18 @@ import {ethers} from "ethers"; import {useStore} from "@/store/store"; +import {socket} from "@/socket.js"; +import {vaultContract} from "@/blockchain/contract.js"; export let provider = null -function onChainChanged(chainId) { +export function onChainChanged(chainId) { chainId = Number(chainId) // console.log('chain changed', chainId) const store = useStore() store.chainId = chainId + store.account = null provider = new ethers.BrowserProvider(window.ethereum, chainId) + provider.listAccounts().then(onAccountsChanged) } function onAccountsChanged(accounts) { @@ -18,8 +22,10 @@ function onAccountsChanged(accounts) { store.account = null } else if (accounts[0] !== store.account) { - store.account = accounts[0] + store.account = accounts[0].address + flushOrders() } + socket.emit('address', store.chainId, accounts[0].address) } export async function watchWallet() { @@ -62,4 +68,48 @@ const errorHandlingProxy = { } -// const wallet = new Proxy(new Wallet(), errorHandlingProxy); +export async function connectWallet() { + return provider.getSigner() +} + + +export async function pendOrder(order) { + console.log('order', order) + const s = useStore() + s.pendingOrders.push(order) + const signer = await connectWallet() + if (!s.vaults.length) + socket.emit('ensureVault', s.chainId, await signer.getAddress(), 0) + else + flushOrders() +} + + +export function flushOrders() { + // noinspection JSIgnoredPromiseFromCall + asyncFlushOrders() +} + +export async function asyncFlushOrders() { + const s = useStore() + const orders = s.pendingOrders + if(!orders.length || !s.account) + return + const contract = await vaultContract(0, await provider.getSigner()) + if( !contract ) + return + const proms = [] + for (const order of orders) + proms.push(contract.placeOrder(order)) + s.pendingOrders = [] + const txs = await Promise.all(proms) + for( const tx of txs ) + console.log('placed order', tx) +} + +socket.on('vaults', (vaults)=>{ + const s = useStore() + console.log('vaults', vaults) + s.vaults = vaults + flushOrders() +}) diff --git a/src/components/TimedOrderEntry.vue b/src/components/TimedOrderEntry.vue index 3aae516..5384a8e 100644 --- a/src/components/TimedOrderEntry.vue +++ b/src/components/TimedOrderEntry.vue @@ -2,9 +2,8 @@ DCA / TWAP - Split order across time + Multiple tranches over a time range - + Cancel - Place Order + Place Order @@ -87,13 +88,15 @@ import {computed, ref} from "vue"; import TokenChoice from "@/components/TokenChoice.vue" import PhoneCard from "@/components/PhoneCard.vue"; import {queryHelperContract} from "@/blockchain/contract.js"; -import {SingletonCoroutine} from "@/misc.js"; -import NeedsQueryHelper from "@/components/NeedsQueryHelper.vue"; // noinspection ES6UnusedImports -import {vAutoSelect} from "@/misc.js"; +import {SingletonCoroutine, vAutoSelect} from "@/misc.js"; +import NeedsQueryHelper from "@/components/NeedsQueryHelper.vue"; +import {Exchange, newOrder, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js"; +import {FixedNumber} from "ethers"; +import {pendOrder} from "@/blockchain/wallet.js"; const s = useStore() -const buy = ref(false) +const buy = ref(true) let _tokenA = ref(s.tokens && s.tokens.length >= 1 ? s.tokens[0] : null) let _tokenB = ref(s.tokens && s.tokens.length >= 2 ? s.tokens[1] : null) const tokenA = computed({ @@ -129,8 +132,8 @@ const quote = computed(()=>{ return !token?{}:token }) const routes = ref([]) -const amount = ref(0) -const amountIsBase = ref(false) +const amount = ref(100) // todo 0 +const amountIsTokenA = ref(false) const amountIsTotal = ref(true) const tranches = ref(3) const inverted = ref(false) @@ -169,7 +172,7 @@ async function findRoute() { case 0: // UniswapV2 break case 1: // UniswapV3 - result = {exchange: 'UniswapV3', pool, fee,} + result = {exchange: Exchange.UniswapV3, pool, fee,} break } } @@ -234,6 +237,41 @@ function validateMin(v) { return true } +async 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 amountToken = amountIsTokenA ? ta : tb + const amt = FixedNumber.fromString(amount.value.toString(), {decimals: amountToken.decimals}).value + const amountIsInput = !buy.value ^ amountIsTokenA.value + + // build tranches + const n = tranches.value // num tranches + const ts = [] + let duration = timeUnitIndex === 0 ? interval.value * 60 : // minutes + timeUnitIndex === 1 ? interval.value * 60 * 60 : // hours + interval.value * 24 * 60 * 60; // days + let window + if (!intervalIsTotal.value) { + window = duration + duration *= n // duration is the total time for all tranches + } else { + window = duration / n + } + const ceil = 10n ** 18n % BigInt(n) ? 1n : 0n + const amtPerTranche = 10n ** 18n / BigInt(n) + ceil + duration -= 15 // subtract 15 seconds so the last tranche completes before the deadline + for (let i = 0; i < n; i++) { + const start = Math.floor(i * (duration / n)) + const end = start + window + const cs = [newTimeConstraint(TimeMode.SinceOrderStart, start, TimeMode.SinceOrderStart, end)] + ts.push([amtPerTranche, cs]) + } + const order = newOrder(tokenIn, tokenOut, route.exchange, route.fee, amt, amountIsInput, ts) + await pendOrder(order) +} diff --git a/src/misc.js b/src/misc.js index 7144681..6115c3b 100644 --- a/src/misc.js +++ b/src/misc.js @@ -38,4 +38,6 @@ export const vAutoSelect = { const input = el.querySelector('input') input.onfocus = () => setTimeout(() => input.select(), 0) } -} \ No newline at end of file +} +export const uint64max = 18446744073709551615n +export const uint32max = 4294967295n \ No newline at end of file diff --git a/src/socket.js b/src/socket.js index 7d4a619..cec92a7 100644 --- a/src/socket.js +++ b/src/socket.js @@ -1,5 +1,7 @@ import {io} from "socket.io-client"; import {useStore} from "@/store/store.js"; +import {onChainChanged} from "@/blockchain/wallet.js"; +import {ethers} from "ethers"; export const socket = io(import.meta.env.VITE_WS_URL || undefined, { transports: ["websocket"] }) @@ -11,11 +13,14 @@ socket.on('disconnect', ()=>{ console.log('ws disconnected') }) -socket.on('welcome', (data)=>{ +socket.on('welcome', async (data)=>{ console.log('welcome',data) + const s = useStore() + s.chainInfo = data.chainInfo + s.vaultInitCodeHash = data.vaultInitCodeHash + const p = new ethers.BrowserProvider(window.ethereum) + const network = await p.getNetwork() + if( network !== null ) + onChainChanged(network.chainId) }) -socket.on('chainInfo', async (chainInfo)=>{ - const s = useStore() - s.chainInfo = chainInfo -}) diff --git a/src/store/store.js b/src/store/store.js index 18c0b9a..d0cc727 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -4,10 +4,12 @@ import {knownTokens} from "@/tokens.js"; export const useStore = defineStore('app', { state: () => ({ - chainInfo: null, chainId: null, + chainInfo: null, + vaultInitCodeHash: null, account: null, - vault: null, + vaults: [], + pendingOrders: [], // created but not yet sent to metamask. maybe waiting on vault creation. errors: [{ title: 'DANGER!', text: 'This is early development (alpha) software, which could have severe bugs that lose all your money. Thank you for testing a SMALL amount!',