price subscriptions

This commit is contained in:
Tim Olson
2023-11-02 23:30:43 -04:00
parent 3f15985bf5
commit b483974268
10 changed files with 212 additions and 40 deletions

View File

@@ -6,7 +6,7 @@ export const queryHelperAbi = [
'function getRoutes(address tokenA,address tokenB) view returns((uint8,uint24,address)[])', 'function getRoutes(address tokenA,address tokenB) view returns((uint8,uint24,address)[])',
] ]
export const poolAbi = [ export const uniswapV3PoolAbi = [
// { // {
// // the current price // // the current price
// uint160 sqrtPriceX96; // uint160 sqrtPriceX96;

View File

@@ -4,6 +4,8 @@ import {useStore} from "@/store/store.js";
export function vaultAddress( owner, num=0) { export function vaultAddress( owner, num=0) {
if( !owner )
return null
const s = useStore() const s = useStore()
const salt = ethers.solidityPackedKeccak256(['address','uint8'],[owner,num]) const salt = ethers.solidityPackedKeccak256(['address','uint8'],[owner,num])
return ethers.getCreate2Address(s.factory, salt, s.vaultInitCodeHash) return ethers.getCreate2Address(s.factory, salt, s.vaultInitCodeHash)
@@ -31,7 +33,7 @@ export async function queryHelperContract() {
export async function poolContract(addr) { export async function poolContract(addr) {
const s = useStore() const s = useStore()
return contractOrNull(addr, poolAbi, s.provider) return contractOrNull(addr, uniswapV3PoolAbi, s.provider)
} }
export async function vaultContract(num, signer) { export async function vaultContract(num, signer) {

84
src/blockchain/prices.js Normal file
View File

@@ -0,0 +1,84 @@
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 {erc20Abi, uniswapV3PoolAbi} from "@/blockchain/abi.js";
const subs = {} // key is route and value is a subscription counter
export function subPrices( routes ) {
const subRoutes = []
let chainId = null
for( const route of routes ) {
if( !(route in subRoutes) || subRoutes[route] === 0 ) {
subRoutes[route] = 1
console.log('subscribing to pool', route.pool)
subRoutes.push(route)
}
else {
subRoutes[route]++
if( chainId !== null && route.chainId !== chainId )
throw Error('cannot mix chainIds in a subscription list')
chainId = route.chainId
}
}
if( subRoutes.length ) {
socket.emit('subPools', chainId, routes.map((r)=>r.address) )
// perform a local query if necessary
for( const route of subRoutes ) {
const s = useStore()
if( !(route.address in s.poolPrices) ) {
getPriceForRoute(route).then((price)=>s.poolPrices[route.address]=price)
}
}
}
}
export function unsubPrices( routes ) {
let chainId = null
const unsubAddrs = []
for( const route of routes ) {
if( !(route in subs) ) {
console.error('unsubscribed to a nonexistent route', route)
}
else {
subs[route]--
if( subs[route] === 0 ) {
unsubAddrs.push(route.pool)
if( chainId !== null && route.chainId !== chainId )
throw Error('cannot mix chainIds in a subscription list')
console.log('unsubscribing from pool', route.pool)
chainId = route.chainId
}
else if( subs[route] < 0 ) {
console.error('unsubscribed to an already unsubbed route', route)
subs[route] = 0 // fix
}
}
}
if( unsubAddrs.length )
socket.emit('unsubPool', chainId, unsubAddrs )
}
async function getPriceForRoute(route) {
console.log('route is',route)
if( route.exchange === Exchange.UniswapV3 ) {
const addr = uniswapV3PoolAddress(route.chainId, route.token0.address, route.token1.address, route.fee)
const provider = useStore().provider;
if( provider === null ) {
console.error('provider was null during getPriceForRoute')
return null
}
const pool = new ethers.Contract(addr, uniswapV3PoolAbi, provider)
const got = await pool.slot0()
const [sqrtPrice,,,,,,] = got
const spn = Number(sqrtPrice)
const price = spn*spn/2**(96*2) * 10**(route.token0.decimals-route.token1.decimals)
console.log(`price for ${route.token0.symbol}/${route.token1.symbol}`,price)
}
else
throw Error(`Unsupported exchange ${route.exchange}`)
}

29
src/blockchain/route.js Normal file
View File

@@ -0,0 +1,29 @@
import {queryHelperContract} from "@/blockchain/contract.js";
import {Exchange} from "@/blockchain/orderlib.js";
import {useStore} from "@/store/store.js";
export async function findRoute(tokenA, tokenB) {
const helper = await queryHelperContract()
if (!helper)
throw Error('no helper')
const chainId = useStore().chainId
const rawRoutes = await helper.getRoutes(tokenA.address, tokenB.address)
// todo expose all available pools
let result = {} // we actually only find a single pool for now
for (let [exchange, fee, pool] of rawRoutes) {
exchange = Number(exchange)
fee = Number(fee)
if (result.fee === undefined || result.fee > fee) {
switch (exchange) {
case 0: // UniswapV2
break
case 1: // UniswapV3
const [token0, token1] = tokenA.address < tokenB.address ? [tokenA, tokenB] : [tokenB, tokenA]
result = {chainId, exchange: Exchange.UniswapV3, pool, fee, token0, token1}
break
}
}
}
return [result]
}

24
src/blockchain/uniswap.js Normal file
View File

@@ -0,0 +1,24 @@
import {ethers} from "ethers";
const UNISWAPV3_POOL_INIT_CODE_HASH = '0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54'
const uniswapV3Addresses = {
42161: {
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
},
31337: {
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
},
}
export function uniswapV3PoolAddress(chainId, tokenAddrA, tokenAddrB, fee) {
const [addr0, addr1] = tokenAddrA < tokenAddrB ? [tokenAddrA, tokenAddrB] : [tokenAddrB, tokenAddrA]
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(['address', 'address', 'uint24'], [addr0, addr1, fee]);
const salt = ethers.keccak256(encoded)
const factory = uniswapV3Addresses[chainId]?.factory
console.log('uni3addr', addr0, addr1, fee, salt, factory)
if (!factory) {
console.log('no uniswap factory for chain', chainId)
return null
}
return ethers.getCreate2Address(factory, salt, UNISWAPV3_POOL_INIT_CODE_HASH)
}

View File

@@ -81,8 +81,9 @@ const errorHandlingProxy = {
export async function connectWallet() { export async function connectWallet() {
console.log('TODO connect wallet')
// eth_getaccounts // eth_getaccounts
const s = useStore()
await s.provider.getSigner()
} }
const pendingOrders = [] const pendingOrders = []

View File

@@ -0,0 +1,32 @@
<template>
<NeedsProvider>
<slot v-if="ok"/>
<phone-card v-if="!ok">
<v-card-title><v-icon icon="mdi-reload-alert" color="warning"/> Connect Wallet</v-card-title>
<v-card-text>
Please select an account to use from your wallet.
</v-card-text>
<v-card-actions>
<v-btn @click="connectWallet">Connect Wallet</v-btn>
</v-card-actions>
</phone-card>
</NeedsProvider>
</template>
<script setup>
import {useStore} from "@/store/store";
import NeedsProvider from "@/components/NeedsProvider.vue";
import {computed} from "vue";
import PhoneCard from "@/components/PhoneCard.vue";
import {connectWallet} from "@/blockchain/wallet.js";
const s = useStore()
const ok = computed(()=>s.address!==null)
</script>
<style scoped lang="scss">
@use "src/styles/vars" as *;
</style>

View File

@@ -84,16 +84,17 @@
<script setup> <script setup>
import {useStore} from "@/store/store"; import {useStore} from "@/store/store";
import {computed, ref} from "vue"; import {computed, onBeforeUnmount, ref} from "vue";
import TokenChoice from "@/components/TokenChoice.vue" import TokenChoice from "@/components/TokenChoice.vue"
import PhoneCard from "@/components/PhoneCard.vue"; import PhoneCard from "@/components/PhoneCard.vue";
import {queryHelperContract} from "@/blockchain/contract.js";
// noinspection ES6UnusedImports // noinspection ES6UnusedImports
import {SingletonCoroutine, vAutoSelect} from "@/misc.js"; import {SingletonCoroutine, vAutoSelect} from "@/misc.js";
import {Exchange, newOrder, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js"; import {newOrder, newTimeConstraint, TimeMode} from "@/blockchain/orderlib.js";
import {FixedNumber} from "ethers"; import {FixedNumber} from "ethers";
import {pendOrder} from "@/blockchain/wallet.js"; import {pendOrder} from "@/blockchain/wallet.js";
import NeedsProvider from "@/components/NeedsProvider.vue"; import NeedsProvider from "@/components/NeedsProvider.vue";
import {subPrices, unsubPrices} from "@/blockchain/prices.js";
import {findRoute} from "@/blockchain/route.js";
const s = useStore() const s = useStore()
const buy = ref(false) const buy = ref(false)
@@ -131,7 +132,18 @@ const quote = computed(()=>{
const token = inverted.value ? _tokenA.value : _tokenB.value const token = inverted.value ? _tokenA.value : _tokenB.value
return !token?{}:token return !token?{}:token
}) })
const routes = ref([]) const _routes = ref([])
const routes = computed({
get() {
return _routes.value
},
set(value) {
console.log('setting new routes', value)
subPrices(value)
unsubPrices(_routes.value)
_routes.value = value
}
})
const routesPending = ref(false) const routesPending = ref(false)
const amount = ref(100) // todo 0 const amount = ref(100) // todo 0
const amountIsTokenA = ref(false) const amountIsTokenA = ref(false)
@@ -148,47 +160,33 @@ const timeUnitIndex = ref(1)
const limitIsMinimum = computed(() => !(buy.value ^ inverted.value)) const limitIsMinimum = computed(() => !(buy.value ^ inverted.value))
const validOrder = computed(()=>amount.value > 0 && routes.value.length > 0 ) const validOrder = computed(()=>amount.value > 0 && routes.value.length > 0 )
async function findRoute() {
onBeforeUnmount(() => {
unsubPrices(_routes.value)
})
async function componentFindRoute() {
console.log('finding route', _tokenA.value, _tokenB.value) console.log('finding route', _tokenA.value, _tokenB.value)
routes.value = [] routes.value = []
if( !_tokenA.value || !_tokenB.value ) if (!_tokenA.value || !_tokenB.value)
return return
const helper = await queryHelperContract()
if( !helper ) {
console.log('no helper')
return
}
routesPending.value = true routesPending.value = true
let rawRoutes
try { try {
rawRoutes = await helper.getRoutes(tokenA.value.address, tokenB.value.address) const result = await findRoute(tokenA.value, tokenB.value)
console.log('found route', result)
routes.value = result
} }
catch (e) { catch (e) {
console.log('routes exception', e) console.log('ignoring routes exception', e)
}
finally {
routesPending.value = false routesPending.value = false
return
} }
// todo expose all available pools
let result = {} // we actually only find a single pool for now
for (let [exchange, fee, pool] of rawRoutes) {
exchange = Number(exchange)
fee = Number(fee)
if (result.fee === undefined || result.fee > fee) {
switch (exchange) {
case 0: // UniswapV2
break
case 1: // UniswapV3
result = {exchange: Exchange.UniswapV3, pool, fee,}
break
}
}
}
routes.value = [result]
routesPending.value = false
console.log('found route', result)
} }
const routeFinder = new SingletonCoroutine(findRoute,10)
const routeFinder = new SingletonCoroutine(componentFindRoute,10)
routeFinder.invoke() routeFinder.invoke()
function toggleTimeUnits() { function toggleTimeUnits() {

View File

@@ -59,13 +59,12 @@
<script setup> <script setup>
import {useStore} from "@/store/store.js"; import {useStore} from "@/store/store.js";
import PhoneCard from "@/components/PhoneCard.vue";
import {computed, defineAsyncComponent, ref} from "vue"; import {computed, defineAsyncComponent, ref} from "vue";
import {vaultAddress} from "@/blockchain/contract.js"; import {vaultAddress} from "@/blockchain/contract.js";
import CopyButton from "@/components/CopyButton.vue"; import CopyButton from "@/components/CopyButton.vue";
import NeedsProvider from "@/components/NeedsProvider.vue"; import NeedsProvider from "@/components/NeedsProvider.vue";
import Withdraw from "@/components/Withdraw.vue"; import Withdraw from "@/components/Withdraw.vue";
console.log('vault setup')
const TokenRow = defineAsyncComponent(()=>import('./TokenRow.vue')) const TokenRow = defineAsyncComponent(()=>import('./TokenRow.vue'))
const s = useStore() const s = useStore()

View File

@@ -1,11 +1,14 @@
<template> <template>
<!-- todo needs account --> <!-- todo needs account -->
<Vault v-if="s.account" :owner="s.account" :num="0"/> <needs-signer>
<Vault :owner="s.account" :num="0"/>
</needs-signer>
</template> </template>
<script setup> <script setup>
import {useStore} from "@/store/store"; import {useStore} from "@/store/store";
import Vault from "@/components/Vault.vue"; import Vault from "@/components/Vault.vue";
import NeedsSigner from "@/components/NeedsSigner.vue";
const s = useStore() const s = useStore()