Files
web/src/blockchain/wallet.js
2024-04-11 20:01:50 -04:00

463 lines
13 KiB
JavaScript

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