diff --git a/src/blockchain/route.js b/src/blockchain/route.js index 06e612d..21d127f 100644 --- a/src/blockchain/route.js +++ b/src/blockchain/route.js @@ -36,7 +36,7 @@ async function componentFindRoute() { os.routes = [] if (!tokenA || !tokenB) return - console.log('finding route', s.chainId.value, tokenA, tokenB) + console.log('finding route', s.chainId, tokenA, tokenB) os.routesPending = true try { console.log('getting query helper') @@ -44,7 +44,7 @@ async function componentFindRoute() { if (!helper) { console.log('no helper') } else { - const result = await findRoute(helper, s.chainId.value, tokenA, tokenB) + const result = await findRoute(helper, s.chainId, tokenA, tokenB) console.log('found route', result) os.routes = result } diff --git a/src/blockchain/token.js b/src/blockchain/token.js index 6ae3213..e6c07fe 100644 --- a/src/blockchain/token.js +++ b/src/blockchain/token.js @@ -6,6 +6,8 @@ import {ethers} from "ethers"; // synchronous version may return null but will trigger a lookup export function token(addr) { + // todo deprecated. use metadataMap[chainId][addr] + console.warn('token() is deprecated') console.log('token', addr) if( !addr ) { // console.log('ignoring call to token', addr) @@ -23,6 +25,8 @@ export function token(addr) { // async version doesnt return until it has a token value export async function getToken(addr) { + // todo deprecated. use metadataMap[chainId][addr] + console.warn('getToken() is deprecated') const s = useStore() if (!(addr in s.tokens)) await addExtraToken(addr) diff --git a/src/blockchain/wallet.js b/src/blockchain/wallet.js index 8d43a48..d5977f8 100644 --- a/src/blockchain/wallet.js +++ b/src/blockchain/wallet.js @@ -6,20 +6,26 @@ import {vaultAbi} from "@/blockchain/abi.js"; import {SingletonCoroutine} from "@/misc.js"; import {defineStore} from "pinia"; import {ref} from "vue"; +import {metadata, metadataMap} from "@/version.js"; export const useWalletStore = defineStore('wallet', ()=>{ + // this is what the wallet is logged into. it could be different than the application's store.chainId. + const chainId = ref(0) + // Pending Order Format // { // chainId: 31337, // must never be null, even if no wallet plugin exists. chosen by app, not wallet. // placementTime: Date.now(), + // submitted: false // true after the order has started to be sent to the wallet + // tx: null // transaction ID // vault: '0x...', // or null if account not logged in yet // order: {tokenIn:..., tokenOut:..., ...} // blockchain binary order object // } const pendingOrders = ref([]) return { - pendingOrders, + chainId, pendingOrders, } }) @@ -27,27 +33,31 @@ export const useWalletStore = defineStore('wallet', ()=>{ export function onChainChanged(chainId) { chainId = Number(chainId) const store = useStore() - if( chainId !== store.chainId.value ) { - // todo check pending orders and confirm cancellation + const ws = useWalletStore() + if( chainId !== ws.chainId ) { console.log('chain changed', chainId) - store.chainId.value = chainId - store.account = null - const provider = new ethers.BrowserProvider(window.ethereum, chainId); - store.provider = provider - provider.listAccounts().then((accounts)=>changeAccounts(chainId, accounts.map((a)=>a.address))) + ws.chainId = chainId + if (chainId.toString() in metadataMap) { + console.log('app chain changed', chainId) + store.chainId = chainId + store.account = null + } } } + +export function updateAccounts(chainId, provider) { + provider.listAccounts().then((accounts) => changeAccounts(chainId, accounts.map((a) => a.address))) +} + + function changeAccounts(chainId, accounts) { - // console.log('changeAccounts', chainId, accounts) + // this is a notification from the wallet that the user selected a different blockchain. that chain may or may not + // be supported. we store this value in walletStore.chainId, which may or may not be the same as + // the application's useStore().chainId + console.log('changeAccounts', chainId, accounts) const store = useStore() - if( accounts.length === 0 ) { - console.log('account logged out') - store.account = null - store.vaults = [] - store.vaultBalances = {} - } - else { + if (chainId === store.chainId && accounts.length) { const addr = accounts[0] console.log('account logged in', addr) store.account = addr @@ -55,12 +65,19 @@ function changeAccounts(chainId, accounts) { flushTransactions() socket.emit('address', chainId, addr) } + else { + console.log('account logged out') + store.account = null + store.vaults = [] + store.vaultBalances = {} + } } function onAccountsChanged(accounts) { const store = useStore() + const ws = useWalletStore() if (accounts.length === 0 || accounts[0] !== store.account) - changeAccounts(store.chainId.value, accounts); + changeAccounts(ws.chainId, accounts); } export function detectChain() { @@ -105,8 +122,49 @@ const errorHandlingProxy = { } +export async function connectProvider(chainId) { + console.log('connecting provider to chainId', chainId) + try { + return new ethers.BrowserProvider(window.ethereum, chainId) + } + catch (e) { + console.log('provider error', e) + } + return null +} + + export async function connectWallet(chainId) { - await new ethers.BrowserProvider(window.ethereum, chainId).getSigner(); + console.log('connectWallet', chainId) + const ws = useWalletStore() + if (ws.chainId !== chainId && !await switchChain(chainId)) + return null + const p = await connectProvider(chainId) + if (p!==null) { + try { + return await p.getSigner(); + } + catch (e) { + if (e.reason!=='rejected') + console.error(e, e.reason) + return null + } + } +} + + +export async function switchChain(chainId) { + if (useWalletStore().chainId === chainId) + return true + try { + await window.ethereum.request({ + "method": "wallet_switchEthereumChain", + "params": [{"chainId": '0x' + chainId.toString(16)}] + }) + return true + } catch (e) { + return false + } } @@ -123,7 +181,7 @@ const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50, false) async function _discoverVaults(owner) { const result = [] const s = useStore() - if( !owner || !s.chainId.value || !s.account) { + if( !owner || !s.chainId || !s.account) { s.vaults = [] return } @@ -159,7 +217,7 @@ async function _discoverVaults(owner) { if( result.length ) flushOrders(result[0]) else - ensureVault2(s.chainId.value, owner, 0) + ensureVault2(s.chainId, owner, 0) } } @@ -176,8 +234,8 @@ async function ensureVault1() { const s = useStore() const owner = s.account; if (owner===null) - await connectWallet(s.chainId.value) - ensureVault2(s.chainId.value, owner, 0) + await connectWallet(s.chainId) + ensureVault2(s.chainId, owner, 0) } export function ensureVault2(chainId, owner, num) { @@ -209,9 +267,10 @@ export async function pendOrder(order) { console.log('order', JSON.stringify(order)) const s = useStore() useWalletStore().pendingOrders.push({ - chainId: s.chainId.value, + chainId: s.chainId, placementTime: new Date(), vault: s.vaults.length ? s.vaults[0] : null, + submitted: false, order }) ensureVault() @@ -243,13 +302,18 @@ export async function cancelAll(vault) { export function flushOrders(vault) { const ws = useWalletStore(); + let needsFlush = false for( const order of ws.pendingOrders ) { if (order.vault === null) order.vault = vault - pendOrderAsTransaction(order) + if (!order.submitted) { + pendOrderAsTransaction(order) + order.submitted = true + needsFlush = true + } } - ws.pendingOrders = [] - flushTransactions() + if (needsFlush) + flushTransactions() } @@ -260,8 +324,18 @@ function pendOrderAsTransaction(order) { console.error('vault contract was null while sending order transaction', order.vault) return null } + if (!await switchChain(order.chainId)) { + console.log('user refused chain switch') + return null + } console.log('placing order', order) - return await contract.placeOrder(order.order) // todo update status + const tx = await contract.placeOrder(order.order) // todo update status + order.tx = tx + tx.wait().then((txReceipt)=>{ + const ws = useWalletStore(); + ws.pendingOrders = ws.pendingOrders.filter((o)=>o!==order) + }) + return tx }) } @@ -273,26 +347,15 @@ export function pendTransaction(sender) { } +const flushTransactionsRoutine = new SingletonCoroutine(asyncFlushTransactions,1) + let flushing = 0 // semaphore export function flushTransactions() { - flushing++ - if( flushing === 1 ) - // noinspection JSIgnoredPromiseFromCall - asyncFlushTransactions() + flushTransactionsRoutine.invoke() } -export async function asyncFlushTransactions() { - let counter - do { - counter = flushing - await asyncFlushTransactions2() - } while( flushing > counter) - flushing = 0 -} - -export async function asyncFlushTransactions2() { - // todo rework into flushTransactions() +async function asyncFlushTransactions() { const s = useStore() if( s.provider === null ) { console.log('warning: asyncFlushOrders() cancelled due to null provider') @@ -319,22 +382,21 @@ export async function asyncFlushTransactions2() { function doSendTransaction(sender, signer) { const s = useStore(); + s.removeTransactionSender(sender) sender(signer).then((tx)=>{ - console.log('sent transaction', tx) - s.removeTransactionSender(sender) - tx.wait().then((tr)=>console.log('tx receipt',tr)) + if (tx!==null) { + console.log('sent transaction', tx) + tx.wait().then((tr)=>console.log('tx receipt',tr)) + } }).catch((e)=>{ if( e.info?.error?.code === 4001 ) { console.log(`user rejected transaction`) - s.removeTransactionSender(sender) } else { if( e.reason && e.info ) console.error('error sending transaction', e.reason, e.info) else console.error('error sending transaction', e) - s.removeTransactionSender(sender) - // todo retry? } }) } diff --git a/src/charts/datafeed.js b/src/charts/datafeed.js index b66a819..9f6b76f 100644 --- a/src/charts/datafeed.js +++ b/src/charts/datafeed.js @@ -152,7 +152,7 @@ function addSymbol(p, base, quote, inverted) { async function getAllSymbols() { if (_symbols===null) { - const chainId = useStore().chainId.value; + const chainId = useStore().chainId; const md = metadata[chainId] if(!md) { console.log('could not get metadata for chain', chainId) @@ -340,7 +340,7 @@ export const DataFeed = { onResetCacheNeededCallback, ) => { console.log('[subscribeBars]', symbolInfo, resolution, subscriberUID); - const chainId = useStore().chainId.value; + const chainId = useStore().chainId; const poolAddr = useChartOrderStore().selectedPool[0]; const period = tvResolutionToPeriodString(resolution); subscriptions[subscriberUID] = [chainId, poolAddr, period, onRealtimeCallback, onResetCacheNeededCallback] diff --git a/src/components/Btn.vue b/src/components/Btn.vue index db169cc..308946a 100644 --- a/src/components/Btn.vue +++ b/src/components/Btn.vue @@ -1,9 +1,9 @@ @@ -12,7 +12,12 @@ import {useStore} from "@/store/store"; import {useAttrs} from "vue"; const s = useStore() -const props = defineProps(['icon', 'color', 'text']) +const props = defineProps({ + icon: {default:null}, + color: {default:null}, + text: {default:null}, + variant: {default:'text'}, +}) const attrs = useAttrs() diff --git a/src/components/Faucet.vue b/src/components/Faucet.vue index 0545578..8cd11b2 100644 --- a/src/components/Faucet.vue +++ b/src/components/Faucet.vue @@ -1,9 +1,11 @@ \ No newline at end of file diff --git a/src/components/chart/ChartOrderPane.vue b/src/components/chart/ChartPlaceOrder.vue similarity index 100% rename from src/components/chart/ChartOrderPane.vue rename to src/components/chart/ChartPlaceOrder.vue diff --git a/src/components/chart/ChartVault.vue b/src/components/chart/ChartVault.vue new file mode 100644 index 0000000..e5af88c --- /dev/null +++ b/src/components/chart/ChartVault.vue @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/src/components/chart/Toolbar.vue b/src/components/chart/Toolbar.vue index 95bfa0e..290f6da 100644 --- a/src/components/chart/Toolbar.vue +++ b/src/components/chart/Toolbar.vue @@ -1,12 +1,21 @@ @@ -19,21 +28,17 @@ import {timestamp} from "@/misc.js"; import {ShapeType} from "@/charts/shape.js"; import {computed, ref} from "vue"; import {useTheme} from "vuetify"; +import ToolbarButton from "@/components/chart/ToolbarButton.vue"; + +const props = defineProps(['title', 'icon']) const s = useStore() const co = useChartOrderStore() -const showCancel = ref(false) - const theme = useTheme().current -function cancelOrder() { - showCancel.value = true -} - - -function placeOrder() { - +function showCorp() { + window.open('https://dexorder.trade', 'dexorder') } diff --git a/src/components/chart/ToolbarButton.vue b/src/components/chart/ToolbarButton.vue new file mode 100644 index 0000000..bed88e0 --- /dev/null +++ b/src/components/chart/ToolbarButton.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index b543f1e..5e407a8 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -11,7 +11,7 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "chartorder" */ '@/components/chart/ChartOrderPane.vue'), + component: () => import(/* webpackChunkName: "chartorder" */ '@/components/chart/ChartPlaceOrder.vue'), }, { path: '/place', @@ -19,7 +19,7 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "chartorder" */ '@/components/chart/ChartOrderPane.vue'), + component: () => import(/* webpackChunkName: "chartorder" */ '@/components/chart/ChartPlaceOrder.vue'), }, { path: '/vault', @@ -27,7 +27,7 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "vaultview" */ '@/views/VaultView.vue'), + component: () => import(/* webpackChunkName: "vaultview" */ '@/components/chart/ChartVault.vue'), }, { path: '/orders', @@ -35,13 +35,13 @@ const routes = [ // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "ordersview" */ '@/views/OrdersView.vue'), + component: () => import(/* webpackChunkName: "ordersview" */ '@/components/chart/ChartOrders.vue'), }, /* { path: '/create', name: 'Create', - component: () => import(/!* webpackChunkName: "chartorder" *!/ '@/components/chart/ChartOrderPane.vue'), + component: () => import(/!* webpackChunkName: "chartorder" *!/ '@/components/chart/ChartPlaceOrder.vue'), }, { path: '/twap', diff --git a/src/socket.js b/src/socket.js index d333a6b..840ca90 100644 --- a/src/socket.js +++ b/src/socket.js @@ -17,7 +17,7 @@ socket.on('disconnect', () => { socket.on('p', async (chainId, pool, price) => { console.log('pool price from message', chainId, pool, price) const s = useStore() - if( s.chainId.value !== chainId ) + if( s.chainId !== chainId ) return s.poolPrices[[chainId,pool]] = price }) @@ -29,7 +29,7 @@ socket.on('ohlc', async (chainId, pool, ohlcs) => { socket.on('vb', async (chainId, vault, balances) => { const s = useStore() - if( s.chainId.value !== chainId ) + if( s.chainId !== chainId ) return console.log('vb', vault, balances) s.vaultBalances[vault] = JSON.parse(balances) @@ -39,7 +39,7 @@ socket.on('vb', async (chainId, vault, balances) => { socket.on('vaults', (chainId, owner, vaults)=>{ const s = useStore() console.log('vaults', vaults) - if( s.chainId.value !== chainId || s.account !== owner ) + if( s.chainId !== chainId || s.account !== owner ) return if( vaults.length > s.vaults.length ) { s.vaults = vaults @@ -53,7 +53,7 @@ socket.on('vaults', (chainId, owner, vaults)=>{ function handleOrderStatus(chainId, vault, orderIndex, status) { const s = useStore() - if( s.chainId.value !== chainId ) + if( s.chainId !== chainId ) return // message 'o' is a single order status const parsed = parseOrderStatus(status); @@ -74,7 +74,7 @@ socket.on( 'o', handleOrderStatus) socket.on( 'of', (chainId, vault, orderIndex, filled)=>{ const s = useStore() - if( s.chainId.value !== chainId ) + if( s.chainId !== chainId ) return console.log('of', chainId, vault, orderIndex, filled) if( !(vault in s.orders) ) { diff --git a/src/store/store.js b/src/store/store.js index 52f6fa8..d8e21d5 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -3,6 +3,8 @@ import {defineStore} from 'pinia' import {knownTokens} from "@/knownTokens.js"; import {computed, ref} from "vue"; import {version} from "@/version.js"; +import {ethers} from "ethers"; +import {updateAccounts} from "@/blockchain/wallet.js"; // USING THE STORE: // @@ -31,11 +33,15 @@ function timestamp() { return Math.round(new Date().getTime() / 1000) } + +const UNKNOWN_PROVIDER = {} + + export const useStore = defineStore('app', ()=> { const clock = ref(timestamp()) const nav = ref(false) // controls opening navigation drawer - const _chainId = ref(Object.keys(version.chainInfo)[0]) + const _chainId = ref(Number(Object.keys(version.chainInfo)[0])) const _chainInfo = ref(version.chainInfo) function getTokenList() { @@ -54,25 +60,42 @@ export const useStore = defineStore('app', ()=> { result[token.address] = token return result } - function refreshChain() { - useOrderStore().setDefaultTokens(getTokenList()) - } const chainId = computed({ - get() {return _chainId}, - set(v) {_chainId.value=v; refreshChain()} + get() {return _chainId.value}, + set(v) { + console.log('setting chainid',_chainId.value, v) + if (_chainId.value!==v) { + console.log('do set') + _chainId.value = v + account.value = null + } + if (_chainId.value!==v || _provider.value === null) { + console.log('update provider') + _provider.value = UNKNOWN_PROVIDER + } + } }) const chainInfo = computed({ - get() {return _chainInfo}, - set(v) {_chainInfo.value=v; refreshChain()} + get() {return _chainInfo.value}, + set(v) {_chainInfo.value=v} }) const chain = computed(() => !_chainId.value ? null : (_chainInfo.value[_chainId.value] || null)) - // making the provider directly reactive causes exceptions (calling private method...) when calling provider - // functions, so we use a separate ref to signal changes - let _provider = null - const _providerTouch = ref(false) - const provider = computed({ - get() {_providerTouch.value; return _provider}, - set(v) {_provider=v; _providerTouch.value = !_providerTouch.value} + // this provider is for the app's chainId not the wallet's chainId. + let _provider = UNKNOWN_PROVIDER // null indicates a known-unavailable provider whereas undefined means provider status is unknown + const provider = computed(()=>{ + if (_provider===UNKNOWN_PROVIDER) { + // unknown provider status + try { + _provider = new ethers.BrowserProvider(window.ethereum, _chainId.value); + updateAccounts(_chainId.value, _provider) + } + catch (e) { + console.error('could not get provider', _chainId.value, e) + _provider = null + } + } + // if _provider is null then it is known to be an unavailable chain + return _provider }) const vaultInitCodeHash = computed(() => !chain.value ? null : chain.value.vaultInitCodeHash) const account = ref(null) diff --git a/src/styles/style.scss b/src/styles/style.scss index 156252a..b87e785 100644 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -7,6 +7,12 @@ font-size: 24px; } + .title { + font-family: v.$heading-font-family; + font-weight: 500; + font-size: 21px; + } + .clickable { :hover { cursor: pointer; diff --git a/src/version.js b/src/version.js index 06116e2..aa7b299 100644 --- a/src/version.js +++ b/src/version.js @@ -25,6 +25,6 @@ for (const [chain, info] of Object.entries(metadata)) { map[poolMeta.a] = poolMeta for (const tokenMeta of info.t) map[tokenMeta.a] = tokenMeta - metadataMap[chain] = map + metadataMap[Number(chain)] = map } console.log('metadataMap', metadataMap) diff --git a/src/views/VaultView.vue b/src/views/VaultView.vue index 21c1c9c..3f68fd8 100644 --- a/src/views/VaultView.vue +++ b/src/views/VaultView.vue @@ -1,8 +1,11 @@ @@ -14,6 +17,7 @@ import NeedsSigner from "@/components/NeedsSigner.vue"; import Faucet from "@/components/Faucet.vue"; import NeedsProvider from "@/components/NeedsProvider.vue"; import NewOrder from "@/components/NewOrder.vue"; +import PhoneCard from "@/components/PhoneCard.vue"; const s = useStore()