diff --git a/package.json b/package.json index c40540f..6ec5ca0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@magiceden-oss/runestone-lib", - "version": "0.4.1-alpha", + "version": "0.4.2-alpha", "description": "", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/indexer/updater.ts b/src/indexer/updater.ts index 09766bd..037dab2 100644 --- a/src/indexer/updater.ts +++ b/src/indexer/updater.ts @@ -350,7 +350,13 @@ export class RuneUpdater implements RuneBlockIndex { private async mint(id: RuneLocation, txid: string): Promise> { const runeLocation = RuneLocation.toString(id); - const etching = await this._storage.getEtching(runeLocation); + + const etchingByRuneId = new Map( + this.etchings.map((etching) => [RuneLocation.toString(etching.runeId), etching]) + ); + + const etching = + etchingByRuneId.get(runeLocation) ?? (await this._storage.getEtching(runeLocation)); if (etching === null || !etching.valid || !etching.terms) { return None; } @@ -405,12 +411,22 @@ export class RuneUpdater implements RuneBlockIndex { private async unallocated(tx: UpdaterTx) { const unallocated = new Map(); + const utxoBalancesByOutputLocation = new Map(); + for (const utxoBalance of this.utxoBalances) { + const location = `${utxoBalance.txid}:${utxoBalance.vout}`; + const balances = utxoBalancesByOutputLocation.get(location) ?? []; + balances.push(utxoBalance); + utxoBalancesByOutputLocation.set(location, balances); + } + for (const input of tx.vin) { if ('coinbase' in input) { continue; } - const utxoBalance = await this._storage.getUtxoBalance(input.txid, input.vout); + const utxoBalance = + utxoBalancesByOutputLocation.get(`${input.txid}:${input.vout}`) ?? + (await this._storage.getUtxoBalance(input.txid, input.vout)); this.spentOutputs.push({ txid: input.txid, vout: input.vout }); for (const additionalBalance of utxoBalance) { const runeId = additionalBalance.runeId; diff --git a/test/updater.test.ts b/test/updater.test.ts index 07b4b02..5319c7e 100644 --- a/test/updater.test.ts +++ b/test/updater.test.ts @@ -685,6 +685,59 @@ describe('mint', () => { }); }); +test('mint is valid for etching in same block', async () => { + const { runeUpdater, storage } = getDefaultRuneUpdaterContext(); + + storage.getValidMintCount.mockResolvedValue(0); + + const tx1: UpdaterTx = { + txid: 'txid1', + vin: [{ txid: 'parenttxid', vout: 1, txinwitness: [] }], + vout: [ + { + scriptPubKey: { + hex: getDeployRunestoneHex({ + etching: { terms: { amount: 100, cap: 1 } }, + }), + }, + }, + MAGIC_EDEN_OUTPUT, + ], + }; + await runeUpdater.indexRunes(tx1, 21); + + const tx2: UpdaterTx = { + txid: 'txid2', + vin: [{ txid: 'parenttxid', vout: 2, txinwitness: [] }], + vout: [ + { + scriptPubKey: { + hex: getDeployRunestoneHex({ + edicts: [{ id: [100000, 21], amount: 100, output: 1 }], + mint: [100000, 21], + }), + }, + }, + MAGIC_EDEN_OUTPUT, + ], + }; + + await runeUpdater.indexRunes(tx2, 88); + + expect(runeUpdater.etchings.length).toBe(1); + expect(runeUpdater.utxoBalances.length).toBe(1); + expect(runeUpdater.utxoBalances[0]).toMatchObject({ + txid: 'txid2', + vout: 1, + rune: 'AAAAAAAAAAAAAAAADBCSMALNGAF', + runeId: { + block: 100000, + tx: 21, + }, + amount: 100n, + }); +}); + describe('edict', () => { test('edicts successfully moves runes', async () => { const { runeUpdater, storage } = getDefaultRuneUpdaterContext(); @@ -765,6 +818,76 @@ describe('edict', () => { }); }); + test('edicts chained successfully moves runes', async () => { + const { runeUpdater, storage } = getDefaultRuneUpdaterContext(); + const tx1: UpdaterTx = { + txid: 'txid', + vin: [{ txid: 'parenttxid', vout: 0, txinwitness: [] }], + vout: [ + { + scriptPubKey: { + hex: getDeployRunestoneHex({}), + }, + }, + MAGIC_EDEN_OUTPUT, + ], + }; + const tx2: UpdaterTx = { + txid: 'childtxid', + vin: [{ txid: 'txid', vout: 1, txinwitness: [] }], + vout: [MAGIC_EDEN_OUTPUT], + }; + + storage.getUtxoBalance.mockResolvedValueOnce([ + { + txid: 'parenttxid', + vout: 0, + amount: 400n, + rune: 'TESTRUNE', + runeId: { block: 888, tx: 8 }, + scriptPubKey: Buffer.from('a914ea6b832a05c6ca578baa3836f3f25553d41068a587', 'hex'), + address: '3P4WqXDbSLRhzo2H6MT6YFbvBKBDPLbVtQ', + }, + ]); + + storage.getEtching.mockResolvedValue({ + valid: true, + txid: 'txid', + rune: 'TESTRUNE', + runeId: { block: 888, tx: 8 }, + terms: { amount: 500n, cap: 1n }, + }); + + await runeUpdater.indexRunes(tx1, 88); + await runeUpdater.indexRunes(tx2, 89); + expect(runeUpdater.etchings.length).toBe(0); + expect(runeUpdater.utxoBalances.length).toBe(2); + expect(runeUpdater.utxoBalances[0]).toMatchObject({ + txid: 'txid', + vout: 1, + rune: 'TESTRUNE', + runeId: { + block: 888, + tx: 8, + }, + amount: 400n, + }); + expect(runeUpdater.utxoBalances[1]).toMatchObject({ + txid: 'childtxid', + vout: 0, + rune: 'TESTRUNE', + runeId: { + block: 888, + tx: 8, + }, + amount: 400n, + }); + expect(runeUpdater.spentOutputs).toEqual([ + { txid: 'parenttxid', vout: 0 }, + { txid: 'txid', vout: 1 }, + ]); + }); + test('edict with invalid output is cenotaph', async () => { const { runeUpdater, storage } = getDefaultRuneUpdaterContext(); const tx: UpdaterTx = {