Files
web/src/blockchain/wallet.js

646 lines
17 KiB
JavaScript

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