Files
web/src/blockchain/wallet.js
2024-02-27 22:20:23 -04:00

314 lines
8.2 KiB
JavaScript

import {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, sleep} from "@/misc.js";
export function onChainChanged(chainId) {
chainId = Number(chainId)
const store = useStore()
if( chainId !== store.chainId ) {
console.log('chain changed', chainId)
store.chainId = chainId
store.account = null
const provider = new ethers.BrowserProvider(window.ethereum, chainId);
store.provider = provider
provider.listAccounts().then((accounts)=>changeAccounts(chainId, accounts.map((a)=>a.address)))
}
}
function changeAccounts(chainId, accounts) {
// console.log('changeAccounts', chainId, accounts)
const store = useStore()
if( accounts.length === 0 ) {
console.log('account logged out')
store.account = null
store.vaults = []
store.vaultBalances = {}
}
else {
const addr = accounts[0]
console.log('account logged in', addr)
store.account = addr
discoverVaults(addr)
flushTransactions()
socket.emit('address', chainId, addr)
}
}
function onAccountsChanged(accounts) {
const store = useStore()
if (accounts.length === 0 || accounts[0] !== store.account)
changeAccounts(store.chainId.value, accounts);
}
export function detectChain() {
new ethers.BrowserProvider(window.ethereum).getNetwork().then((network)=>{
const chainId = network.chainId
onChainChanged(chainId)
window.ethereum.on('chainChanged', onChainChanged);
window.ethereum.on('accountsChanged', onAccountsChanged);
})
}
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() {
await new ethers.BrowserProvider(window.ethereum).getSigner();
}
let pendingOrders = []
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
}
const vault = new ethers.Contract(addr, vaultAbi, s.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' )
console.log(`no vault ${num} at ${addr}`)
else
console.error(`routeFinder failed`, e)
}
if( s.account == owner ) { // double-check the account since it could have changed during our await
s.vaults = result
if( pendingOrders.length )
if( result.length )
flushOrders(result[0])
else
ensureVault2(s.chainId.value, owner, 0)
}
}
export function ensureVault() {
const s = useStore()
const owner = s.account;
console.log('ensureVault', s.chainId.value, owner)
if( !owner )
return
ensureVault2(s.chainId.value, 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) {
await _discoverVaults(owner)
if( !useStore().vaults[num] ) {
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, false)
export async function pendOrder(order) {
console.log('order', JSON.stringify(order))
const s = useStore()
if (!s.vaults.length) {
pendingOrders.push(order)
ensureVault()
}
else {
const vault = s.vaults[0];
pendOrderAsTransaction(vault, order)
}
}
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.cancelOrder(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.cancelAll()
})
}
export function flushOrders(vault) {
for( const order of pendingOrders )
pendOrderAsTransaction(vault, order)
pendingOrders = []
flushTransactions()
}
function pendOrderAsTransaction(vault, order) {
pendTransaction(async (signer)=> {
const contract = contractOrNull(vault, vaultAbi, signer)
if( contract === null ) {
console.error('vault contract was null while sending order transaction', vault)
return null
}
return await contract.placeOrder(order)
})
}
export function pendTransaction(sender) {
const s = useStore()
s.transactionSenders.push(sender)
flushTransactions()
}
let flushing = 0 // semaphore
export function flushTransactions() {
flushing++
if( flushing === 1 )
// noinspection JSIgnoredPromiseFromCall
asyncFlushTransactions()
}
export async function asyncFlushTransactions() {
let counter
do {
counter = flushing
await asyncFlushTransactions2()
} while( flushing > counter)
flushing = 0
}
export async function asyncFlushTransactions2() {
// todo rework into flushTransactions()
const s = useStore()
if( s.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 s.provider.getSigner();
} catch (e) {
// {
// "code": -32002,
// "message": "Already processing eth_requestAccounts. Please wait."
// }
console.log('signer denied')
return
}
for (const sender of senders)
doSendTransaction(sender, signer)
}
function doSendTransaction(sender, signer) {
const s = useStore();
sender(signer).then((tx)=>{
console.log('sent transaction', tx)
s.removeTransactionSender(sender)
tx.wait().then((tr)=>console.log('tx receipt',tr))
}).catch((e)=>{
if( e.info?.error?.code === 4001 ) {
console.log(`user rejected transaction`)
s.removeTransactionSender(sender)
}
else {
if( e.reason && e.info )
console.error('error sending transaction', e.reason, e.info)
else
console.error('error sending transaction', e)
s.removeTransactionSender(sender)
// todo retry?
}
})
}