order placement works, including automatic vault creation

This commit is contained in:
Tim Olson
2023-10-06 19:49:31 -04:00
parent c206607547
commit c637e82ac3
8 changed files with 279 additions and 56 deletions

View File

@@ -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,
}

View File

@@ -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)
}

114
src/blockchain/orderlib.js Normal file
View File

@@ -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
// }

View File

@@ -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()
})

View File

@@ -2,9 +2,8 @@
<NeedsQueryHelper>
<PhoneCard>
<v-card-title class="big">DCA / TWAP</v-card-title>
<v-card-subtitle>Split order across time</v-card-subtitle>
<v-card-subtitle>Multiple tranches over a time range</v-card-subtitle>
<v-card-text>
<token-choice v-model="tokenA" class="token-choice mb-1">
<template v-slot:prepend>
<v-btn :text="buy ? 'Buy' : 'Sell'" :color="buy ? 'green' : 'red'"
@@ -35,15 +34,15 @@
<v-text-field label='Amount' type="number" step="1" variant="outlined" aria-valuemin="0" min="0"
v-model="amount" :rules="[validateRequired,validateAmount]" v-auto-select>
<template v-slot:append-inner>
<v-btn @click="amountIsBase=!amountIsBase" variant="outlined" class="mr-2">
{{ amountIsBase ? tokenA.symbol : tokenB.symbol }}
<v-btn @click="amountIsTokenA=!amountIsTokenA" variant="outlined" class="mr-2">
{{ amountIsTokenA ? tokenA.symbol : tokenB.symbol }}
</v-btn>
<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" max="255"
:model-value="tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
v-model="tranches" :rules="[validateRequired,validateTranches]" v-auto-select>
<!-- <template v-slot:prepend-inner>-->
<!-- <div>{{ amountIsTotal ? 'Split into' : 'Times' }}</div>-->
<!-- </template>-->
@@ -60,6 +59,7 @@
<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]" v-auto-select>
@@ -69,13 +69,14 @@
</v-btn>
</template>
</v-text-field>
-->
</div>
</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" :disabled="!validOrder">Place Order</v-btn>
<v-btn variant="flat" color="green" :disabled="!validOrder" @click="placeOrder">Place Order</v-btn>
</v-card-actions>
</PhoneCard>
</NeedsQueryHelper>
@@ -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)
}
</script>

View File

@@ -38,4 +38,6 @@ export const vAutoSelect = {
const input = el.querySelector('input')
input.onfocus = () => setTimeout(() => input.select(), 0)
}
}
}
export const uint64max = 18446744073709551615n
export const uint32max = 4294967295n

View File

@@ -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
})

View File

@@ -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!',