646 lines
17 KiB
JavaScript
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)
|
|
}
|
|
} |