import {BrowserProvider, ethers} from "ethers"; import {useStore} from "@/store/store"; import {socket} from "@/socket.js"; import {contractOrNull, vaultAddress} from "@/blockchain/contract.js"; import {vaultAbi} from "@/blockchain/abi.js"; import {SingletonCoroutine, uuid} from "@/misc.js"; import {defineStore} from "pinia"; import {ref} from "vue"; import {metadataMap} from "@/version.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) // 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([]) return { chainId, pendingOrders, } }) export function onChainChanged(chainId) { console.log('onChainChanged', chainId) chainId = Number(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. 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 (chainId === store.chainId && accounts.length) { const addr = accounts[0] console.log('account logged in', addr) store.account = addr discoverVaults(addr) flushTransactions() socket.emit('address', chainId, addr) } else { console.log('account logged out') store.account = null store.vaults = [] store.vaultBalances = {} } } 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) { if (e.reason!=='rejected') { console.error(e, e.reason) throw e } } } 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 addTestnet() { const info = { "chainId": "0x539", "chainName": "Dexorder Alpha Testnet", "rpcUrls": ["https://rpc.alpha.dexorder.trade"], "nativeCurrency": { "name": "Test Ethereum", "symbol": "TETH", "decimals": 18 } }; await window.ethereum.request({ "method": "wallet_addEthereumChain", "params": [info] }); } 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, false) async function _discoverVaults(owner) { const result = [] const s = useStore() if( !owner || !s.chainId || !s.account) { s.vaults = [] return } // todo multi-vault scan // console.log('_discoverVaults',owner) const num = 0 const addr = vaultAddress(s.factory, s.vaultInitCodeHash, owner, num) // console.log(`vault ${num} at`, addr) if( addr === null ) { s.vaults = [] return } console.log('provider', provider) if (!provider) { console.log('No provider') return // do not change whatever was already found } const vault = new ethers.Contract(addr, vaultAbi, provider) let version = -1 try { version = await vault.version(); if( Number(version) === 1 ) { console.log(`found vault ${num} at ${addr}`) result.push(addr) } else console.error(`bad vault version ${version}`) } catch (e) { if( e.value==='0x' && e.code==='BAD_DATA' || e.revert===null && e.code==='CALL_EXCEPTION' ) 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? } if( s.account === owner ) { // double-check the account since it could have changed during our await s.vaults = result if( useWalletStore().pendingOrders.length ) { if (result.length) flushOrders(result[0]) else ensureVault2(s.chainId, owner, 0) } } } 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(s.vaults[num]) else { console.log(`requesting vault ${owner} ${num}`) socket.emit('ensureVault', chainId, owner, num) } } // await sleep(5000) // prevent this process from running more than once every 5 seconds const ensureVaultRoutine = new SingletonCoroutine(doEnsureVault, 100) export const PendingOrderState = { Submitted: -100, // user clicked Place Order but the tx isn't sent to the wallet yet Signing: 0, // tx is sent to the wallet Rejected: -101, // user refused to sign the tx Sent: -102, // tx is awaiting blockchain mining } export async function pendOrder(order) { const s = useStore() const pend = { id: uuid(), chainId: s.chainId, placementTime: Date.now()/1000, vault: s.vaults.length ? s.vaults[0] : null, state: PendingOrderState.Submitted, order }; useWalletStore().pendingOrders.splice(0,0, pend) console.log('pending order', pend.id, JSON.stringify(order)) ensureVault() } export async function cancelOrder(vault, orderIndex) { console.log('cancel order', vault, orderIndex) pendTransaction(async (signer)=> { const contract = contractOrNull(vault, vaultAbi, 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) { pendTransaction(async (signer)=> { const contract = contractOrNull(vault, vaultAbi, signer) if( contract === null ) { console.error('vault contract was null while canceling order', vault) return null } return await contract.cancelAllDexorders() }) } export function flushOrders(vault) { const ws = useWalletStore(); 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) pend.state = PendingOrderState.Signing needsFlush = true } } if (needsFlush) flushTransactions() } function pendOrderAsTransaction(pend) { pendTransaction(async (signer)=> { const contract = contractOrNull(pend.vault, vaultAbi, signer) if( contract === null ) { 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') pend.state = PendingOrderState.Rejected return null } else { console.error('Unknown error while switching chain to pend order', e) return null } } console.log('placing order', pend.id) const tx = await contract.placeDexorder(pend.order) // todo update status pend.tx = tx pend.state = 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) pend.state = PendingOrderState.Rejected return true // returning true means we handled the error. any other return value will dump to console. } }) } 0 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() 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) } }) }