Skip to content

Commit f756308

Browse files
authored
Fixed chained rune actions in same block bugs (#30)
1 parent 88ed8fd commit f756308

File tree

3 files changed

+142
-3
lines changed

3 files changed

+142
-3
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@magiceden-oss/runestone-lib",
3-
"version": "0.4.1-alpha",
3+
"version": "0.4.2-alpha",
44
"description": "",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/indexer/updater.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,13 @@ export class RuneUpdater implements RuneBlockIndex {
350350

351351
private async mint(id: RuneLocation, txid: string): Promise<Option<bigint>> {
352352
const runeLocation = RuneLocation.toString(id);
353-
const etching = await this._storage.getEtching(runeLocation);
353+
354+
const etchingByRuneId = new Map(
355+
this.etchings.map((etching) => [RuneLocation.toString(etching.runeId), etching])
356+
);
357+
358+
const etching =
359+
etchingByRuneId.get(runeLocation) ?? (await this._storage.getEtching(runeLocation));
354360
if (etching === null || !etching.valid || !etching.terms) {
355361
return None;
356362
}
@@ -405,12 +411,22 @@ export class RuneUpdater implements RuneBlockIndex {
405411
private async unallocated(tx: UpdaterTx) {
406412
const unallocated = new Map<string, RuneBalance>();
407413

414+
const utxoBalancesByOutputLocation = new Map<string, RuneUtxoBalance[]>();
415+
for (const utxoBalance of this.utxoBalances) {
416+
const location = `${utxoBalance.txid}:${utxoBalance.vout}`;
417+
const balances = utxoBalancesByOutputLocation.get(location) ?? [];
418+
balances.push(utxoBalance);
419+
utxoBalancesByOutputLocation.set(location, balances);
420+
}
421+
408422
for (const input of tx.vin) {
409423
if ('coinbase' in input) {
410424
continue;
411425
}
412426

413-
const utxoBalance = await this._storage.getUtxoBalance(input.txid, input.vout);
427+
const utxoBalance =
428+
utxoBalancesByOutputLocation.get(`${input.txid}:${input.vout}`) ??
429+
(await this._storage.getUtxoBalance(input.txid, input.vout));
414430
this.spentOutputs.push({ txid: input.txid, vout: input.vout });
415431
for (const additionalBalance of utxoBalance) {
416432
const runeId = additionalBalance.runeId;

test/updater.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,59 @@ describe('mint', () => {
685685
});
686686
});
687687

688+
test('mint is valid for etching in same block', async () => {
689+
const { runeUpdater, storage } = getDefaultRuneUpdaterContext();
690+
691+
storage.getValidMintCount.mockResolvedValue(0);
692+
693+
const tx1: UpdaterTx = {
694+
txid: 'txid1',
695+
vin: [{ txid: 'parenttxid', vout: 1, txinwitness: [] }],
696+
vout: [
697+
{
698+
scriptPubKey: {
699+
hex: getDeployRunestoneHex({
700+
etching: { terms: { amount: 100, cap: 1 } },
701+
}),
702+
},
703+
},
704+
MAGIC_EDEN_OUTPUT,
705+
],
706+
};
707+
await runeUpdater.indexRunes(tx1, 21);
708+
709+
const tx2: UpdaterTx = {
710+
txid: 'txid2',
711+
vin: [{ txid: 'parenttxid', vout: 2, txinwitness: [] }],
712+
vout: [
713+
{
714+
scriptPubKey: {
715+
hex: getDeployRunestoneHex({
716+
edicts: [{ id: [100000, 21], amount: 100, output: 1 }],
717+
mint: [100000, 21],
718+
}),
719+
},
720+
},
721+
MAGIC_EDEN_OUTPUT,
722+
],
723+
};
724+
725+
await runeUpdater.indexRunes(tx2, 88);
726+
727+
expect(runeUpdater.etchings.length).toBe(1);
728+
expect(runeUpdater.utxoBalances.length).toBe(1);
729+
expect(runeUpdater.utxoBalances[0]).toMatchObject({
730+
txid: 'txid2',
731+
vout: 1,
732+
rune: 'AAAAAAAAAAAAAAAADBCSMALNGAF',
733+
runeId: {
734+
block: 100000,
735+
tx: 21,
736+
},
737+
amount: 100n,
738+
});
739+
});
740+
688741
describe('edict', () => {
689742
test('edicts successfully moves runes', async () => {
690743
const { runeUpdater, storage } = getDefaultRuneUpdaterContext();
@@ -765,6 +818,76 @@ describe('edict', () => {
765818
});
766819
});
767820

821+
test('edicts chained successfully moves runes', async () => {
822+
const { runeUpdater, storage } = getDefaultRuneUpdaterContext();
823+
const tx1: UpdaterTx = {
824+
txid: 'txid',
825+
vin: [{ txid: 'parenttxid', vout: 0, txinwitness: [] }],
826+
vout: [
827+
{
828+
scriptPubKey: {
829+
hex: getDeployRunestoneHex({}),
830+
},
831+
},
832+
MAGIC_EDEN_OUTPUT,
833+
],
834+
};
835+
const tx2: UpdaterTx = {
836+
txid: 'childtxid',
837+
vin: [{ txid: 'txid', vout: 1, txinwitness: [] }],
838+
vout: [MAGIC_EDEN_OUTPUT],
839+
};
840+
841+
storage.getUtxoBalance.mockResolvedValueOnce([
842+
{
843+
txid: 'parenttxid',
844+
vout: 0,
845+
amount: 400n,
846+
rune: 'TESTRUNE',
847+
runeId: { block: 888, tx: 8 },
848+
scriptPubKey: Buffer.from('a914ea6b832a05c6ca578baa3836f3f25553d41068a587', 'hex'),
849+
address: '3P4WqXDbSLRhzo2H6MT6YFbvBKBDPLbVtQ',
850+
},
851+
]);
852+
853+
storage.getEtching.mockResolvedValue({
854+
valid: true,
855+
txid: 'txid',
856+
rune: 'TESTRUNE',
857+
runeId: { block: 888, tx: 8 },
858+
terms: { amount: 500n, cap: 1n },
859+
});
860+
861+
await runeUpdater.indexRunes(tx1, 88);
862+
await runeUpdater.indexRunes(tx2, 89);
863+
expect(runeUpdater.etchings.length).toBe(0);
864+
expect(runeUpdater.utxoBalances.length).toBe(2);
865+
expect(runeUpdater.utxoBalances[0]).toMatchObject({
866+
txid: 'txid',
867+
vout: 1,
868+
rune: 'TESTRUNE',
869+
runeId: {
870+
block: 888,
871+
tx: 8,
872+
},
873+
amount: 400n,
874+
});
875+
expect(runeUpdater.utxoBalances[1]).toMatchObject({
876+
txid: 'childtxid',
877+
vout: 0,
878+
rune: 'TESTRUNE',
879+
runeId: {
880+
block: 888,
881+
tx: 8,
882+
},
883+
amount: 400n,
884+
});
885+
expect(runeUpdater.spentOutputs).toEqual([
886+
{ txid: 'parenttxid', vout: 0 },
887+
{ txid: 'txid', vout: 1 },
888+
]);
889+
});
890+
768891
test('edict with invalid output is cenotaph', async () => {
769892
const { runeUpdater, storage } = getDefaultRuneUpdaterContext();
770893
const tx: UpdaterTx = {

0 commit comments

Comments
 (0)