import {BrowserProvider, ethers} from "ethers"; import {useStore} from "@/store/store"; import {socket} from "@/socket.js"; import {errorSuggestsMissingVault, SingletonCoroutine} from "@/misc.js"; import {newContract, vaultAddress, vaultContract} from "@/blockchain/contract.js"; import {defineStore} from "pinia"; import {computed, ref} from "vue"; import {metadataMap, version} from "@/version.js"; import {CancelAllTransaction, TransactionState, TransactionType} from "@/blockchain/transaction.js"; import {track} from "@/track.js"; export let provider = null 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) // OLD Pending Order Format // { // chainId: 31337, // must never be null, even if no wallet plugin exists. chosen by app, not wallet. // placementTime: Date.now(), // state: PendingOrderState.Submitted // tx: null // transaction ID // vault: '0x...', // or null if account not logged in yet // order: {tokenIn:..., tokenOut:..., ...} // blockchain binary order object // } const pendingOrders = ref([]) // NEW Format is a single Transaction class const _tx = ref(null) const transaction = computed({ get() {return _tx.value}, set(v) { _tx.value = v; if (v===null) { if (progressionInvoker!==null) { clearTimeout(progressionInvoker) progressionInvoker = null } } else { transactionProgressor.invoke(); if (progressionInvoker===null) progressionInvoker = setInterval(()=>transactionProgressor.invoke(), 1000) } }, }) return { chainId, pendingOrders, transaction, } }) export function onChainChanged(chainId) { console.log('onChainChanged', chainId) chainId = Number(chainId) socket.emit('chain', chainId) const store = useStore() const ws = useWalletStore() if( chainId !== ws.chainId ) { console.log('wallet chain changed', chainId) ws.chainId = chainId if (chainId.toString() in metadataMap) { console.log('app chain changed', chainId) store.chainId = chainId store.account = null provider = new BrowserProvider(window.ethereum, chainId) updateAccounts(chainId, provider) } else { console.log('app chain NOT changed') } } } export function updateAccounts(chainId, provider) { provider.listAccounts().then((accounts) => changeAccounts(chainId, accounts.map((a) => a.address))) } function 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. console.log('changeAccounts', chainId, accounts) const store = useStore() if (chainId === store.chainId && accounts.length) { const addr = accounts[0] if (addr !== store.account) { console.log('account logged in', addr) track('login', {chainId, address: addr}) store.account = addr store.vaults = [] // one of these two methods will call flushTransactions() if (useWalletStore().transaction!==null) ensureVault() else discoverVaults(addr) socket.emit('address', chainId, addr) } } else { console.log('account logged out') store.account = null store.vaults = [] } } function onAccountsChanged(accounts) { console.log('onAccountsChanged', accounts) const store = useStore() const ws = useWalletStore() if (accounts.length === 0 || accounts[0] !== store.account) changeAccounts(ws.chainId, accounts); } export function detectChain() { try { window.ethereum.on('chainChanged', onChainChanged); window.ethereum.on('accountsChanged', onAccountsChanged); } catch (e) { console.log('Could not connect change hooks to wallet', e) return } new ethers.BrowserProvider(window.ethereum).getNetwork().then((network)=>{ const chainId = network.chainId onChainChanged(chainId) }) } const errorHandlingProxy = { get(target, prop, proxy) { const got = Reflect.get(target, prop, proxy); if( typeof got !== 'function' ) { return got } else { return async function (...args) { try { return await got.apply(target, args) } catch (x) { target._connected(false) target._enabled = false if( x.code === 'NETWORK_ERROR' ) { // todo available chain names // store.error('Wrong Blockchain', 'Your wallet is connected to a different blockchain. Please select Arbitrum in your wallet.') // todo hardcoded arb console.error('wallet chain error', x) // store.chainId = store.chainInfo[] throw x } else { console.error('wallet error') throw x } } } } } } export async function connectWallet(chainId) { console.log('connectWallet', chainId) try { await switchChain(chainId) console.log('getSigner') const p = new BrowserProvider(window.ethereum, chainId) await p.getSigner() await updateAccounts(chainId, p) } catch (e) { console.log('connectWallet error', e.reason, e) if (e.reason==='rejected') { const ws = useWalletStore(); const tx = ws.transaction if (tx) { tx.state = TransactionState.Rejected ws.transaction = null } } else { console.error(e, e.reason) throw e } } } function discoverVaults(owner) { const s = useStore() console.log('discoverVaults', owner) if( owner === null ) s.vaults = [] else doDiscoverVaults.invoke(owner) } const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50) async function _discoverVaults(owner) { const result = [] const versions = [] const s = useStore() if( !owner || !s.chainId || !s.account) { s.vaults = [] return } // todo multi-vault scan // console.log('_discoverVaults',owner) for (let num=0; num < 1; num++) { const num = 0 const addr = vaultAddress(s.factory, s.vaultInitCodeHash, owner, num) // console.log(`vault ${num} at`, addr) if (addr === null) // no more vaults break if (!provider) { console.log('No provider') return // do not change whatever was already found } const vault = await vaultContract(addr, provider) try { const version = Number(await vault.version()) console.log(`found vault #${num} v${version} at ${addr}`) result.push(addr) versions.push(version) } catch (e) { if (errorSuggestsMissingVault(e)) console.log(`no vault ${num} at ${addr}`) else console.error(`discoverVaults failed`, e) return // do not change what was already found todo is this correct? } } console.log('new account === owner?', s.account, owner) if( s.account === owner ) { // double-check the account since it could have changed during our await s.vaults = result s.vaultVersions = versions if( useWalletStore().transaction ) { const num = 0 // todo multiple vaults if (result.length) flushOrders(s.chainId, owner, num, result[0]) else ensureVault2(s.chainId, owner, num) } } } export function ensureVault() { ensureVaultFunc.invoke() } const ensureVaultFunc = new SingletonCoroutine(ensureVault1,1) async function ensureVault1() { const s = useStore() const owner = s.account; if (owner===null) await connectWallet(s.chainId) ensureVault2(s.chainId, owner, 0) } export function ensureVault2(chainId, owner, num) { console.log('ensureVault2', chainId, owner, num) if( !chainId ) { console.log('cannot create vault: no chain selected') return } if( !owner ) { console.log('cannot create vault: no account logged in') return } ensureVaultRoutine.invoke(chainId, owner, num) } async function doEnsureVault(chainId, owner, num) { const s = useStore(); if (s.vaults.length <= num) await _discoverVaults(owner) if( s.vaults[num] ) flushOrders(chainId, owner, num, s.vaults[num]) else { console.log(`requesting vault ${owner} ${num}`) socket.emit('ensureVault', chainId, owner, num) } } const ensureVaultRoutine = new SingletonCoroutine(doEnsureVault, 100) export async function cancelOrder(vault, orderIndex) { console.log('cancel order', vault, orderIndex) pendTransaction(async (signer)=> { const contract = await vaultContract(vault, signer) if( contract === null ) { console.error('vault contract was null while canceling order', vault, orderIndex) return null } return await contract.cancelDexorder(orderIndex) }) } export async function cancelAll(vault) { new CancelAllTransaction(useStore().chainId, vault).submit() } async function progressTransactions() { const s = useStore() const ws = useWalletStore(); if( ws.transaction===null ) return if( s.account === null ) { let signer = null try { signer = await provider.getSigner() } catch (e) { console.log('signer error', e.code, e.info.error.code) if (e?.info?.error?.code === 4001) { console.log('signer rejected') signer = null } else throw e } if (signer === null) { console.log('setting tx state to rejected') ws.transaction.state = TransactionState.Rejected ws.transaction = null return } } if( s.vault === null ) { ensureVault() return } if( ws.transaction.type === TransactionType.PlaceOrder ) { flushOrders(s.chainId, s.account, 0, s.vault) } } const transactionProgressor = new SingletonCoroutine(progressTransactions, 10) let progressionInvoker = null export function flushOrders(chainId, owner, num, vault) { const ws = useWalletStore(); console.log('flushOrders', chainId, owner, num, vault) if (ws.transaction!==null && ws.transaction.type === TransactionType.PlaceOrder && ws.transaction.state < TransactionState.Proposed) ws.transaction.propose(owner, vault) let needsFlush = false for( const pend of ws.pendingOrders ) { if (pend.vault === null) pend.vault = vault if (pend.state === PendingOrderState.Submitted) { console.log('flushing order', pend.id) pendOrderAsTransaction(pend) setPendState(pend, PendingOrderState.Signing) needsFlush = true } } if (needsFlush) flushTransactions() } function pendOrderAsTransaction(pend) { pendTransaction(async (signer)=> { // 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 } try { await switchChain(pend.chainId) } catch (e) { if(e.code===4001) { console.log('user refused chain switch') setPendState(pend, PendingOrderState.Rejected) return null } else { console.error('Unknown error while switching chain to pend order', e) return null } } if (pend.fee === null) { const [orderFee, gasFee] = await placementFee(pend.vault, pend.order) pend.fee = orderFee + gasFee } console.log('placing order', pend.id, pend.fee, pend.order) const tx = await contract.placeDexorder(pend.order, {value:pend.fee}) pend.tx = tx setPendState(pend, PendingOrderState.Sent) console.log(`order ${pend.id} sent transaction`, tx) tx.wait().then((txReceipt)=>{ console.log('mined order', pend.id, txReceipt) pend.receipt = txReceipt const ws = useWalletStore(); ws.pendingOrders = ws.pendingOrders.filter((p)=>p!==pend) // remove pend since order was mined }) return tx }, (e) => { if( e.info?.error?.code === 4001 ) { console.log(`wallet refused order`, pend.id) setPendState(pend, PendingOrderState.Rejected) return true // returning true means we handled the error. any other return value will dump to console. } }) } export function pendTransaction(sender, errHandler) { const s = useStore() s.transactionSenders.push([sender,errHandler]) flushTransactions() } const flushTransactionsRoutine = new SingletonCoroutine(asyncFlushTransactions,1) export function flushTransactions() { flushTransactionsRoutine.invoke() } async function asyncFlushTransactions() { const s = useStore() const ws = useWalletStore() console.log('flushTransactions', ws.transaction, s.vault) if (ws.transaction !== null) { if (s.vault === null) { await ensureVault() if (s.vault === null) { console.error('vault could not be created') const tx = ws.transaction if (tx) { tx.state = TransactionState.Error ws.transaction = null } return } } } if( provider === null ) { console.log('warning: asyncFlushOrders() cancelled due to null provider') return } const senders = s.transactionSenders if (!senders.length) return console.log(`flushing ${senders.length} transactions`) let signer try { signer = await provider.getSigner(); } catch (e) { // { // "code": -32002, // "message": "Already processing eth_requestAccounts. Please wait." // } console.log('signer denied') return } for (const [sender,errHandler] of senders) doSendTransaction(sender, signer, errHandler) s.transactionSenders = [] } function doSendTransaction(sender, signer, errHandler) { const s = useStore(); s.removeTransactionSender(sender) sender(signer).then((tx)=>{ if (tx!==null) { console.log('sent transaction', tx) tx.wait().then((tr)=>{ console.log('tx receipt',tr) }) } }).catch(async (e)=>{ let dumpErr = true if (errHandler!==undefined) dumpErr = await errHandler(e) !== true if (dumpErr) { if( e.reason && e.info ) console.error('error sending transaction', e.reason, e.info) else console.error('error sending transaction', e) } }) } export async function detectUpgrade() { if (!provider) { console.log('no provider!') return null } const s = useStore() if (!s.vault) { console.log('no vault logged in') return null } const info = version.chainInfo[s.chainId] if (!info) { console.log(`couldn't get chainInfo for ${s.chainId}`) return null } try { console.log('factory', info.factory) const [factory, vault] = await Promise.all([ newContract(info.factory, 'IVaultFactory', provider), newContract(s.vault, 'IVault', provider), ]) const vaultImpl = await vault.implementation() const latestImpl = await factory.implementation() console.log('vaultImpl / latestImpl', vaultImpl, latestImpl) if ( vaultImpl !== latestImpl ) { s.upgrade = latestImpl const impl = await newContract(latestImpl, 'IVault', provider) const version = await impl.version() console.log(`found vault version ${version}`) return version } } catch (e) { console.log('ignorable error while querying for an upgrade', e) } return null } function upgradeSender(vault, impl) { return async function (signer) { const v = await vaultContract(vault, signer) v.upgrade(impl) } } function upgradeError(e) { console.error('error while upgrading vault', e) } export async function upgradeVault(vault, impl) { pendTransaction(upgradeSender(vault, impl), upgradeError) } const _chainInfos = { // Arbitrum One 42161: { "chainId": "0xa4b1", "chainName": "Arbitrum One", "rpcUrls": ["https://arbitrum-mainnet.infura.io"], "blockExplorerUrls": ['https://explorer.arbitrum.io'], "nativeCurrency": { "name": "Ethereum", "symbol": "ETH", "decimals": 18 } }, 1337: { "chainId": "0x539", "chainName": "Dexorder Alpha Testnet", "rpcUrls": ["https://rpc.alpha.dexorder.trade"], "nativeCurrency": { "name": "Test Ethereum", "symbol": "TETH", "decimals": 18 } }, 31337: { "chainId": "0x7a69", "chainName": "Mockchain", "rpcUrls": ["http://localhost:8545"], "nativeCurrency": { "name": "Test Ethereum", "symbol": "TETH", "decimals": 18 } }, } export async function switchChain(chainId) { if (useWalletStore().chainId === chainId) return await window.ethereum.request({ "method": "wallet_switchEthereumChain", "params": [{"chainId": '0x' + chainId.toString(16)}] }) } export async function addNetwork(chainId) { const info = _chainInfos[chainId] if (!info) { console.log(`No info to add chain ${chainId}`) return } await window.ethereum.request({ "method": "wallet_addEthereumChain", "params": [info] }); } export async function addNetworkAndConnectWallet(chainId) { try { await switchChain(chainId) } catch (e) { if (e.code === 4001) { // explicit user rejection return } else if (e.code === 4902) { try { await addNetwork(chainId) } catch (e) { console.log(`Could not add network ${chainId}`) } } else console.log('switchChain() failure', e) } try { await connectWallet(chainId) } catch (e) { if (e.code !== 4001) console.log('connectWallet() failed', e) } }