diff --git a/src-vue/lib/MyVault.ts b/src-vue/lib/MyVault.ts index 38980349..8eb2c1dd 100644 --- a/src-vue/lib/MyVault.ts +++ b/src-vue/lib/MyVault.ts @@ -56,6 +56,10 @@ type ICollectOrphanCosignMetadata = { vout: number; vaultSignatureHex: string; }; +type IPendingCosignUtxo = { + marketValue: bigint; + dueFrame?: number; +}; export class MyVault { public data: { @@ -64,7 +68,7 @@ export class MyVault { metadata: IVaultRecord | null; stats: IVaultStats | null; pendingCollectRevenue: bigint; - pendingCosignUtxosById: Map; + pendingCosignUtxosById: Map; nextCollectDueDate: number; expiringCollectAmount: bigint; finalizeMyBitcoinError?: { lockUtxoId: number; error: string }; @@ -113,6 +117,8 @@ export class MyVault { #singleActiveBitcoinQueue = new SingleFileQueue(); // Serialize cosign submissions (collect + individual cosign) and track in-flight intent per UTXO. #cosignQueue = new SingleFileQueue(); + #collectFrames: { frameId: number; uncollectedEarnings: bigint }[] = []; + #pendingCosignUpdateSeq = 0; constructor( private readonly dbPromise: Promise, @@ -167,6 +173,7 @@ export class MyVault { if (this.#waitForLoad && !reload) return this.#waitForLoad.promise; this.#waitForLoad = createDeferred(); + this.#collectFrames = []; try { console.log('Loading MyVault...'); await this.miningFrames.load(); @@ -261,24 +268,26 @@ export class MyVault { const client = await getMainchainClient(false); // update stats live - const sub = await client.query.vaults.vaultsById(vaultId, async vault => { + const sub = await client.query.vaults.vaultsById(vaultId, vault => { if (vault.isSome) this.createdVault?.load(vault.unwrap()); - await this.updateRevenueStats(); }); const sub2 = await client.query.vaults.revenuePerFrameByVault(vaultId, async x => { await this.updateRevenueStats(x); - await this.updateCollectDueDate(); + this.updateCollectDueDate(); }); - const sub3 = await client.query.vaults.pendingCosignByVaultId(vaultId, x => this.recordPendingCosignUtxos(x)); - const sub4 = await client.query.vaults.lastCollectFrameByVaultId(vaultId, x => - this.updateCollectDueDate(x.isSome ? x.unwrap().toNumber() : undefined), - ); + const sub3 = await client.query.vaults.pendingCosignByVaultId(vaultId, async x => { + const updateSeq = ++this.#pendingCosignUpdateSeq; + await this.recordPendingCosignUtxos(x, updateSeq); + }); + const sub4 = await client.query.vaults.lastCollectFrameByVaultId(vaultId, () => { + this.updateCollectDueDate(); + }); const sub5 = await client.query.miningSlot.nextFrameId(frameId => { this.data.currentFrameId = frameId.toNumber() - 1; - void this.updateCollectDueDate(); + this.updateCollectDueDate(); }); const sub6 = await this.subscribeToTreasuryAllocated(); @@ -286,12 +295,12 @@ export class MyVault { this.#subscriptions.push(sub, sub2, sub3, sub4, sub5, sub6); } - private async updateCollectDueDate(_lastCollectFrameId?: number) { + private updateCollectDueDate() { const framesToCollect = this.#configs!.timeToCollectFrames; let nextCollectFrame = this.data.currentFrameId + framesToCollect; this.data.expiringCollectAmount = 0n; const oldestToCollectFrame = this.data.currentFrameId - framesToCollect; - for (const frameChange of this.data.stats?.changesByFrame ?? []) { + for (const frameChange of this.#collectFrames) { if (frameChange.uncollectedEarnings > 0n) { this.data.expiringCollectAmount = frameChange.uncollectedEarnings; // descending order @@ -299,19 +308,46 @@ export class MyVault { } if (frameChange.frameId < oldestToCollectFrame) break; } + if (this.data.pendingCosignUtxosById.size > 0) { + let earliestCosignDueFrame = Number.MAX_SAFE_INTEGER; + for (const pendingCosign of this.data.pendingCosignUtxosById.values()) { + if (pendingCosign.dueFrame === undefined) { + continue; + } + earliestCosignDueFrame = Math.min(earliestCosignDueFrame, pendingCosign.dueFrame); + } + if (earliestCosignDueFrame < Number.MAX_SAFE_INTEGER) { + nextCollectFrame = Math.min(nextCollectFrame, earliestCosignDueFrame); + } + } nextCollectFrame = Math.max(this.data.currentFrameId + 1, nextCollectFrame); this.data.nextCollectDueDate = this.miningFrames.getFrameDate(nextCollectFrame).getTime(); } - private async recordPendingCosignUtxos(rawUtxoIds: Iterable) { - this.data.pendingCosignUtxosById.clear(); + private async recordPendingCosignUtxos(rawUtxoIds: Iterable, updateSeq: number) { + const previousPendingCosignsById = new Map(this.data.pendingCosignUtxosById); + const pendingCosignUtxosById = new Map(); const client = await getMainchainClient(false); for (const utxoId of rawUtxoIds) { const id = utxoId.toNumber(); const lock = await BitcoinLock.get(client, id); - this.data.pendingCosignUtxosById.set(id, lock?.lockedMarketRate ?? 0n); + const previousPending = previousPendingCosignsById.get(id); + const pendingReleaseRaw = await client.query.bitcoinLocks.lockReleaseRequestsByUtxoId(id); + const dueFrame = pendingReleaseRaw.isSome + ? pendingReleaseRaw.unwrap().cosignDueFrame.toNumber() + : previousPending?.dueFrame; + const marketValue = lock?.lockedMarketRate ?? previousPending?.marketValue ?? 0n; + if (marketValue === 0n && !lock && previousPending?.marketValue === undefined) { + console.warn(`Pending cosign UTXO ${id} has no lock data; using 0 as fallback market value.`); + } + pendingCosignUtxosById.set(id, { marketValue, dueFrame }); + } + if (updateSeq !== this.#pendingCosignUpdateSeq) { + return; } + this.data.pendingCosignUtxosById = pendingCosignUtxosById; + this.updateCollectDueDate(); } public async cosignMyLock( @@ -560,7 +596,7 @@ export class MyVault { if (!this.metadata) { throw new Error('No metadata available to collect revenue'); } - const toCosign = [...this.data.pendingCosignUtxosById]; + const toCosign = [...this.data.pendingCosignUtxosById.keys()]; const cosignedUtxoIds: number[] = []; const cosignedOrphanUtxos: ICollectOrphanCosignMetadata[] = []; const expectedCollectRevenue = this.data.pendingCollectRevenue; @@ -570,7 +606,7 @@ export class MyVault { const argonKeyring = await this.walletKeys.getVaultingKeypair(); const txs: SubmittableExtrinsic[] = []; try { - for (const [utxoId, _amount] of toCosign) { + for (const utxoId of toCosign) { if (this.findPendingCosignTxInfo(utxoId)) { continue; } @@ -931,13 +967,19 @@ export class MyVault { const client = await getMainchainClient(false); const vaultId = this.createdVault.vaultId; frameRevenues ??= await client.query.vaults.revenuePerFrameByVault(vaultId); + this.#collectFrames = frameRevenues + .map(frameRevenue => ({ + frameId: frameRevenue.frameId.toNumber(), + uncollectedEarnings: frameRevenue.uncollectedRevenue.toBigInt(), + })) + .sort((a, b) => b.frameId - a.frameId); this.vaults.vaultsById[vaultId] = this.createdVault; await this.vaults.updateVaultRevenue(vaultId, frameRevenues); - this.data.pendingCollectRevenue = 0n; - for (const frameRevenue of frameRevenues) { - this.data.pendingCollectRevenue += frameRevenue.uncollectedRevenue.toBigInt(); - } + this.data.pendingCollectRevenue = this.#collectFrames.reduce( + (total, frame) => total + frame.uncollectedEarnings, + 0n, + ); const data = this.vaults.stats?.vaultsById?.[vaultId]; if (data) { this.data.stats = { ...data }; diff --git a/src-vue/overlays-operations/VaultCollectOverlay.vue b/src-vue/overlays-operations/VaultCollectOverlay.vue index 854e3679..d52cf702 100644 --- a/src-vue/overlays-operations/VaultCollectOverlay.vue +++ b/src-vue/overlays-operations/VaultCollectOverlay.vue @@ -161,7 +161,10 @@ const collectRevenue = Vue.ref(myVault.data.pendingCollectRevenue); const { microgonToMoneyNm } = createNumeralHelpers(currency); const pendingCosignSum = Vue.computed(() => { - const sum = Array.from(myVault.data.pendingCosignUtxosById.values()).reduce((acc, utxo) => acc + utxo, 0n); + const sum = Array.from(myVault.data.pendingCosignUtxosById.values()).reduce( + (acc, utxo) => acc + utxo.marketValue, + 0n, + ); return bigIntMin(sum, myVault.createdVault?.securitization ?? 0n); }); diff --git a/src-vue/screens-operations/vaulting-screen/Dashboard.vue b/src-vue/screens-operations/vaulting-screen/Dashboard.vue index 42a2c951..29351e5b 100644 --- a/src-vue/screens-operations/vaulting-screen/Dashboard.vue +++ b/src-vue/screens-operations/vaulting-screen/Dashboard.vue @@ -458,7 +458,10 @@ const revenueMicrogons = Vue.computed(() => { }); const pendingCosignPenalty = Vue.computed(() => { - const sum = Array.from(myVault.data.pendingCosignUtxosById.values()).reduce((acc, amount) => acc + amount, 0n); + const sum = Array.from(myVault.data.pendingCosignUtxosById.values()).reduce( + (acc, entry) => acc + entry.marketValue, + 0n, + ); return bigIntMin(sum, myVault.createdVault?.securitization ?? 0n); });