diff --git a/src/blockchain/orderlib.js b/src/blockchain/orderlib.js index f019451..5d8c929 100644 --- a/src/blockchain/orderlib.js +++ b/src/blockchain/orderlib.js @@ -1,6 +1,5 @@ import {uint32max, uint64max} from "@/misc.js"; -import {decodeIEE754, encodeIEE754} from "@/common.js"; -import order from "@/components/Order.vue"; +import {encodeIEE754} from "@/common.js"; export const MAX_FRACTION = 65535; export const NO_CONDITIONAL_ORDER = uint64max; diff --git a/src/blockchain/transaction.js b/src/blockchain/transaction.js new file mode 100644 index 0000000..c2d2b63 --- /dev/null +++ b/src/blockchain/transaction.js @@ -0,0 +1,279 @@ +import {nav, timestamp, uuid} from "@/misc.js"; +import {newContract, vaultContract} from "@/blockchain/contract.js"; +import {ensureVault, provider, switchChain, useWalletStore} from "@/blockchain/wallet.js"; +import {toRaw} from "vue"; +import {useChartOrderStore} from "@/orderbuild.js"; + +export const TransactionState = { + Created: 0, // user requested a transaction + Proposed: 1, // tx is sent to the wallet + Signed: 2, // tx is awaiting blockchain mining + Rejected: 3, // user refused to sign the tx + Error: 3, // unknown error sending the tx to the wallet + Mined: 4, // transaction has been confirmed on-chain +} + +export const TransactionType = { + PlaceOrder: 1, + CancelOrder: 2, + CancelAll: 3, + Wrap: 4, + Unwrap: 5, + WithdrawNative: 6, + Withdraw: 7, +} + +export class Transaction { + constructor(chainId, type) { + this.id = uuid() + this.type = type + this.state = TransactionState.Created + this.tx = null + this.chainId = chainId + this.owner = null + this.vault = null + this.error = null + } + + submit() { + useWalletStore().transaction = this + ensureVault() + } + + propose(owner, vault) { + if (this.vault !== null && this.vault !== vault) { + this.failed('proposed vault did not match withdrawl vault', vault, this.vault) + return + } + this.owner = owner + this.vault = vault + this.send().catch(this.catchSend.bind(this)) + this.state = TransactionState.Proposed + } + + async createTx(vaultContract) { + throw Error('unimplemented') + } + + signed(tx) { + this.tx = tx + this.state = TransactionState.Signed + } + + rejected() { + this.tx = null + this.chainId = null + this.owner = null + this.vault = null + this.end(TransactionState.Rejected) + console.log('transaction rejected', this.id) + } + + failed(e) { + this.error = e + this.end(TransactionState.Error) + console.log('transaction failed', this.id, e) + } + + mined(receipt) { + this.receipt = receipt + this.end(TransactionState.Mined) + console.log('mined transaction', this.id, receipt) + } + + isOpen() { + return this.state >= TransactionState.Rejected + } + + isClosed() { + return this.state < TransactionState.Rejected + } + + + end(state) { + this.state = state + useWalletStore().transaction = null + } + + + async send() { + console.log('sendTransaction', this) + try { + await switchChain(this.chainId) + } catch (e) { + if (e.code === 4001) { + this.rejected() + return null + } else { + this.failed(e) + return null + } + } + let signer + try { + signer = await provider.getSigner(); + } catch (e) { + // { + // "code": -32002, + // "message": "Already processing eth_requestAccounts. Please wait." + // } + this.rejected() + return null + } + let contract + try { + contract = await vaultContract(this.vault, signer) + } catch (e) { + this.failed('vault contract was null while sending order transaction') + return null + } + const tx = toRaw(await this.createTx(contract)) + this.signed(tx) + console.log(`sent transaction`, tx) + tx.wait().then(this.mined.bind(this)).catch(this.failed.bind(this)) + return this.tx + } + + + catchSend(e) { + this.error = e + if (e.info?.error?.code === 4001) { + console.log(`wallet refused transaction`, this.id) + this.rejected() + } else { + this.failed(e) + } + } + +} + + +export class PlaceOrderTransaction extends Transaction { + constructor(chainId, order) { + super(chainId, TransactionType.PlaceOrder) + this.order = order + this.placementTime = Date.now()/1000 + this.fee = null // dexorder place and gas fee total + } + + + async createTx(vaultContract) { + this.fee = await placementFee(this.vault, this.order) + console.log('placing order', this.id, this.fee, this.order) + return await vaultContract.placeDexorder(this.order, {value: this.fee.reduce((a, b) => a + b)}) + } + + + end(state) { + super.end(state) + if (state === TransactionState.Mined) { + useChartOrderStore().resetOrders() + nav('Status') + } + + } +} + + +// todo move to orderlib +async function placementFee(vault, order, window = 300) { + // If the fees are about to change within `window` seconds of now, we send the higher native amount of the two fees. + // If the fees sent are too much, the vault will refund the sender. + const v = await vaultContract(vault, provider) + const feeManagerAddr = await v.feeManager() + const feeManager = await newContract(feeManagerAddr, 'IFeeManager', provider) + const [sched, changeTimestamp] = await Promise.all([feeManager.fees(), feeManager.proposedFeeActivationTime()]) + console.log('sched', order, sched) + // single order placement selector + const placementFeeSelector = 'placementFee((address,address,(uint8,uint24),uint256,uint256,bool,bool,bool,uint64,(uint16,bool,bool,bool,bool,bool,bool,bool,bool,uint16,uint24,uint32,uint32,(uint32,uint32),(uint32,uint32))[]),(uint8,uint8,uint8,uint8,uint8))' + let [orderFee, gasFee] = await v[placementFeeSelector](order, [...sched]) + console.log('placementFee', orderFee, gasFee) + if (Number(changeTimestamp) - timestamp() < window) { + const nextSched = await feeManager.proposedFees() + const [nextOrderFee, nextGasFee] = await v[placementFeeSelector](order, [...nextSched]) + if (nextOrderFee + nextGasFee > orderFee + gasFee) + [orderFee, gasFee] = [nextOrderFee, nextGasFee] + } + return [orderFee, gasFee] +} + + +export class CancelOrderTransaction extends Transaction { + constructor(chainId, index) { + super(chainId, TransactionType.CancelOrder) + this.index = index + } + + + async createTx(vaultContract) { + return await vaultContract.cancelDexorder(this.index) + } +} + + +export class CancelAllTransaction extends Transaction { + constructor(chainId, vault) { + super(chainId, TransactionType.CancelAll) + this.vault = vault + } + + + async createTx(vaultContract) { + return await vaultContract.cancelAllDexorders() + } +} + + +export class WithdrawTransaction extends Transaction { + constructor(chainId, vault, token, amount) { + super(chainId, TransactionType.Withdraw) + this.token = token + this.amount = amount + this.vault = vault + } + + + async createTx(vaultContract) { + return await vaultContract['withdraw(address,uint256)'](this.token.a, this.amount) + } +} + + +export class WithdrawNativeTransaction extends Transaction { + constructor(chainId, vault, amount) { + super(chainId, TransactionType.WithdrawNative) + this.amount = amount + this.vault = vault + } + + + async createTx(vaultContract) { + return await vaultContract['withdraw(uint256)'](this.amount) + } +} + + +export class WrapTransaction extends Transaction { + constructor(chainId, vault, amount) { + super(chainId, TransactionType.Wrap) + this.vault = vault + this.amount = amount + } + + async createTx(vaultContract) { + return await vaultContract.wrap(this.amount) + } +} + + +export class UnwrapTransaction extends Transaction { + constructor(chainId, vault, amount) { + super(chainId, TransactionType.Unwrap) + this.amount = amount + } + + async createTx(vaultContract) { + return await vaultContract.unwrap(this.amount) + } +} + diff --git a/src/blockchain/wallet.js b/src/blockchain/wallet.js index 091f4d8..45d79c2 100644 --- a/src/blockchain/wallet.js +++ b/src/blockchain/wallet.js @@ -1,21 +1,23 @@ import {BrowserProvider, ethers} from "ethers"; import {useStore} from "@/store/store"; import {socket} from "@/socket.js"; -import {SingletonCoroutine, timestamp, uuid} from "@/misc.js"; +import {SingletonCoroutine, uuid} from "@/misc.js"; import {newContract, vaultAddress, vaultContract} from "@/blockchain/contract.js"; import {defineStore} from "pinia"; import {ref} from "vue"; import {metadataMap, version} from "@/version.js"; +import {CancelAllTransaction, TransactionState} from "@/blockchain/transaction.js"; export let provider = null +// DEPRECATED 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 + // OLD Pending Order Format // { // chainId: 31337, // must never be null, even if no wallet plugin exists. chosen by app, not wallet. // placementTime: Date.now(), @@ -26,8 +28,11 @@ export const useWalletStore = defineStore('wallet', ()=>{ // } const pendingOrders = ref([]) + // NEW Format is a single Transaction class + const transaction = ref(null) + return { - chainId, pendingOrders, + chainId, pendingOrders, transaction, } }) @@ -165,7 +170,7 @@ function discoverVaults(owner) { doDiscoverVaults.invoke(owner) } -const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50, false) +const doDiscoverVaults = new SingletonCoroutine(_discoverVaults, 50) async function _discoverVaults(owner) { const result = [] const versions = [] @@ -204,11 +209,12 @@ async function _discoverVaults(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().pendingOrders.length ) { + if( useWalletStore().transaction ) { + const num = 0 // todo multiple vaults if (result.length) - flushOrders(result[0]) + flushOrders(s.chainId, owner, num, result[0]) else - ensureVault2(s.chainId, owner, 0) + ensureVault2(s.chainId, owner, num) } } } @@ -248,7 +254,7 @@ async function doEnsureVault(chainId, owner, num) { if (s.vaults.length <= num) await _discoverVaults(owner) if( s.vaults[num] ) - flushOrders(s.vaults[num]) + flushOrders(chainId, owner, num, s.vaults[num]) else { console.log(`requesting vault ${owner} ${num}`) socket.emit('ensureVault', chainId, owner, num) @@ -258,53 +264,6 @@ async function doEnsureVault(chainId, owner, num) { 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 -} - -// single order placement selector -const placementFeeSelector = 'placementFee((address,address,(uint8,uint24),uint256,uint256,bool,bool,bool,uint64,(uint16,bool,bool,bool,bool,bool,bool,bool,bool,uint16,uint24,uint32,uint32,(uint32,uint32),(uint32,uint32))[]),(uint8,uint8,uint8,uint8,uint8))' - -export async function placementFee(vault, order, window=300) { - // If the fees are about to change within `window` seconds of now, we send the higher native amount of the two fees. - // If the fees sent are too much, the vault will refund the sender. - const v = await vaultContract(vault, provider) - const feeManagerAddr = await v.feeManager() - const feeManager = await newContract(feeManagerAddr, 'IFeeManager', provider) - const [sched, changeTimestamp] = await Promise.all([feeManager.fees(), feeManager.proposedFeeActivationTime()]) - console.log('sched', order, sched) - let [orderFee, gasFee] = await v[placementFeeSelector](order, [...sched]) - console.log('placementFee', orderFee, gasFee) - if (Number(changeTimestamp) - timestamp() < window) { - const nextSched = await feeManager.proposedFees() - const [nextOrderFee, nextGasFee] = await v[placementFeeSelector](order, [...nextSched]) - if (nextOrderFee + nextGasFee > orderFee + gasFee) - [orderFee, gasFee] = [nextOrderFee, nextGasFee] - } - return [orderFee, gasFee] -} - - -export async function pendOrder(order, fee=null) { - const s = useStore() - const pend = { - id: uuid(), - chainId: s.chainId, - placementTime: Date.now()/1000, - fee: fee, // dexorder place and gas fee total - vault: s.vaults.length ? s.vaults[0] : null, - state: PendingOrderState.Submitted, - order - }; - useWalletStore().pendingOrders.splice(0,0, pend) - console.log('pended order', pend.id, JSON.stringify(order)) - ensureVault() -} - - export async function cancelOrder(vault, orderIndex) { console.log('cancel order', vault, orderIndex) pendTransaction(async (signer)=> { @@ -318,18 +277,13 @@ export async function cancelOrder(vault, orderIndex) { } export async function cancelAll(vault) { - pendTransaction(async (signer)=> { - const contract = await vaultContract(vault, signer) - if( contract === null ) { - console.error('vault contract was null while canceling order', vault) - return null - } - return await contract.cancelAllDexorders() - }) + new CancelAllTransaction(useStore().chainId, vault).submit() } -export function flushOrders(vault) { +export function flushOrders(chainId, owner, num, vault) { const ws = useWalletStore(); + if (ws.transaction!==null && ws.transaction.state < TransactionState.Proposed) + ws.transaction.propose(owner, vault) let needsFlush = false for( const pend of ws.pendingOrders ) { if (pend.vault === null) @@ -337,7 +291,7 @@ export function flushOrders(vault) { if (pend.state === PendingOrderState.Submitted) { console.log('flushing order', pend.id) pendOrderAsTransaction(pend) - pend.state = PendingOrderState.Signing + setPendState(pend, PendingOrderState.Signing) needsFlush = true } } @@ -363,7 +317,7 @@ function pendOrderAsTransaction(pend) { catch (e) { if(e.code===4001) { console.log('user refused chain switch') - pend.state = PendingOrderState.Rejected + setPendState(pend, PendingOrderState.Rejected) return null } else { @@ -378,7 +332,7 @@ function pendOrderAsTransaction(pend) { console.log('placing order', pend.id, pend.fee, pend.order) const tx = await contract.placeDexorder(pend.order, {value:pend.fee}) pend.tx = tx - pend.state = PendingOrderState.Sent + setPendState(pend, PendingOrderState.Sent) console.log(`order ${pend.id} sent transaction`, tx) tx.wait().then((txReceipt)=>{ console.log('mined order', pend.id, txReceipt) @@ -391,7 +345,7 @@ function pendOrderAsTransaction(pend) { (e) => { if( e.info?.error?.code === 4001 ) { console.log(`wallet refused order`, pend.id) - pend.state = PendingOrderState.Rejected + setPendState(pend, PendingOrderState.Rejected) return true // returning true means we handled the error. any other return value will dump to console. } }) diff --git a/src/components/LinePrice.vue b/src/components/LinePrice.vue index c2a25bd..58e66ed 100644 --- a/src/components/LinePrice.vue +++ b/src/components/LinePrice.vue @@ -9,9 +9,9 @@ diff --git a/src/misc.js b/src/misc.js index 6827d06..af36b7e 100644 --- a/src/misc.js +++ b/src/misc.js @@ -144,7 +144,7 @@ export function inversionPreference(chainId, base, quote) { const inputInverted = base.a > quote.a const token0 = !inputInverted ? base.a : quote.a const token1 = inputInverted ? base.a : quote.a - const key = [chainId, token0, token1]; + const key = [chainId, token0, token1]; // todo chainId shouldn't matter const prefs = usePrefStore() if (!(key in prefs.inverted)) { // todo prefer stablecoins as the quote asset @@ -157,6 +157,7 @@ export function inversionPreference(chainId, base, quote) { break // definitely inverted } } + console.log('inverted?', base, quote, preferInverted) prefs.inverted[key] = preferInverted } // console.log('inversion preference', base, quote, prefs.inverted[key], inputInverted) diff --git a/src/notify.js b/src/notify.js index 7206361..00fd36e 100644 --- a/src/notify.js +++ b/src/notify.js @@ -40,8 +40,8 @@ export async function notifyFillEvent(chainId, status, trancheIndex, fill) { const quoteAddr = status.order.inverted ? low : high const buy = status.order.tokenIn === quoteAddr const [base, quote] = await Promise.all([getToken(chainId, baseAddr), getToken(chainId, quoteAddr)]); - const baseAmount = (buy ? fill.filledOut : fill.filledIn) * 10**-base.d - const quoteAmount = (buy ? fill.filledIn : fill.filledOut) * 10**-quote.d + const baseAmount = Number(buy ? fill.filledOut : fill.filledIn) * 10**-base.d + const quoteAmount = Number(buy ? fill.filledIn : fill.filledOut) * 10**-quote.d const average = quoteAmount / baseAmount const title = `${buy?"Bought":"Sold"} ${baseAmount.toPrecision(5)} ${base.s}`; const msg = title + diff --git a/src/socket.js b/src/socket.js index fbca795..ae6c4ee 100644 --- a/src/socket.js +++ b/src/socket.js @@ -53,7 +53,7 @@ socket.on('vaults', (chainId, owner, vaults)=>{ s.vaults = vaults if( vaults.length ) { const vault = vaults[0] - flushOrders(vault) + flushOrders(chainId, owner, 0, vault) } } })