From cd381a95b4d0d3f4bb08e447500153c3f652eff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Wed, 31 Aug 2022 12:07:00 -0500 Subject: [PATCH] fix: detect name transfers and renewals in special circumstances (#1303) * fix: take new owner from nft event * fix: remove check on nft_custody view as it is no longer required * fix: name-renewal with no zonefile hash * fix: import v1 data during replay * fix: headers already sent in redirect * fix: import names first, subdomains last --- src/api/routes/bns/names.ts | 1 - src/datastore/postgres-store.ts | 109 +++++----- src/event-stream/bns/bns-helpers.ts | 73 +++++-- src/event-stream/core-node-message.ts | 2 +- src/event-stream/event-server.ts | 65 ++++-- src/tests-bns/event-server-tests.ts | 289 ++++++++++++++++++++++++++ 6 files changed, 456 insertions(+), 83 deletions(-) diff --git a/src/api/routes/bns/names.ts b/src/api/routes/bns/names.ts index dc416fb8..67bd12be 100644 --- a/src/api/routes/bns/names.ts +++ b/src/api/routes/bns/names.ts @@ -98,7 +98,6 @@ export function createBnsNamesRouter(db: DataStore, chainId: ChainID): express.R return; } res.redirect(`${resolverResult.result}/v1/names${req.url}`); - next(); return; } res.status(404).json({ error: `cannot find subdomain ${name}` }); diff --git a/src/datastore/postgres-store.ts b/src/datastore/postgres-store.ts index c275f7e8..03b507df 100644 --- a/src/datastore/postgres-store.ts +++ b/src/datastore/postgres-store.ts @@ -1491,12 +1491,12 @@ export class PgDataStore for (const smartContract of entry.smartContracts) { await this.updateSmartContract(client, entry.tx, smartContract); } - for (const bnsName of entry.names) { - await this.updateNames(client, entry.tx, bnsName); - } for (const namespace of entry.namespaces) { await this.updateNamespaces(client, entry.tx, namespace); } + for (const bnsName of entry.names) { + await this.updateNames(client, entry.tx, bnsName); + } } await this.refreshNftCustody(client, batchedTxData); await this.refreshMaterializedView(client, 'chain_tip'); @@ -1701,12 +1701,12 @@ export class PgDataStore for (const smartContract of entry.smartContracts) { await this.updateSmartContract(client, entry.tx, smartContract); } - for (const bnsName of entry.names) { - await this.updateNames(client, entry.tx, bnsName); - } for (const namespace of entry.namespaces) { await this.updateNamespaces(client, entry.tx, namespace); } + for (const bnsName of entry.names) { + await this.updateNames(client, entry.tx, bnsName); + } } } @@ -6896,8 +6896,59 @@ export class PgDataStore status, canonical, } = bnsName; - // inserting remaining names information in names table - const validZonefileHash = this.validateZonefileHash(zonefile_hash); + // Try to figure out the name's expiration block based on its namespace's lifetime. However, if + // the name was only transferred, keep the expiration from the last register/renewal we had. + let expireBlock = expire_block; + if (status === 'name-transfer') { + const prevExpiration = await client.query<{ expire_block: number }>( + `SELECT expire_block + FROM names + WHERE name = $1 + AND canonical = TRUE AND microblock_canonical = TRUE + ORDER BY registered_at DESC, microblock_sequence DESC, tx_index DESC + LIMIT 1`, + [name] + ); + if (prevExpiration.rowCount > 0) { + expireBlock = prevExpiration.rows[0].expire_block; + } + } else { + const namespaceLifetime = await client.query<{ lifetime: number }>( + `SELECT lifetime + FROM namespaces + WHERE namespace_id = $1 + AND canonical = true AND microblock_canonical = true + ORDER BY namespace_id, ready_block DESC, microblock_sequence DESC, tx_index DESC + LIMIT 1`, + [namespace_id] + ); + if (namespaceLifetime.rowCount > 0) { + expireBlock = registered_at + namespaceLifetime.rows[0].lifetime; + } + } + // If we didn't receive a zonefile, keep the last valid one. + let finalZonefile = zonefile; + let finalZonefileHash = zonefile_hash; + if (finalZonefileHash === '') { + const lastZonefile = await client.query<{ zonefile: string; zonefile_hash: string }>( + ` + SELECT z.zonefile, z.zonefile_hash + FROM zonefiles AS z + INNER JOIN names AS n USING (name, tx_id, index_block_hash) + WHERE z.name = $1 + AND n.canonical = TRUE + AND n.microblock_canonical = TRUE + ORDER BY n.registered_at DESC, n.microblock_sequence DESC, n.tx_index DESC + LIMIT 1 + `, + [name] + ); + if (lastZonefile.rowCount > 0) { + finalZonefile = lastZonefile.rows[0].zonefile; + finalZonefileHash = lastZonefile.rows[0].zonefile_hash; + } + } + const validZonefileHash = this.validateZonefileHash(finalZonefileHash); await client.query( ` INSERT INTO zonefiles (name, zonefile, zonefile_hash, tx_id, index_block_hash) @@ -6907,26 +6958,12 @@ export class PgDataStore `, [ name, - zonefile, + finalZonefile, validZonefileHash, hexToBuffer(tx_id), hexToBuffer(blockData.index_block_hash), ] ); - // Try to figure out the name's expiration block based on its namespace's lifetime. - const namespaceLifetime = await client.query<{ lifetime: number }>( - `SELECT lifetime - FROM namespaces - WHERE namespace_id = $1 - AND canonical = true AND microblock_canonical = true - ORDER BY namespace_id, ready_block DESC, microblock_sequence DESC, tx_index DESC - LIMIT 1`, - [namespace_id] - ); - const expireBlock = - namespaceLifetime.rowCount > 0 - ? registered_at + namespaceLifetime.rows[0].lifetime - : expire_block; await client.query( ` INSERT INTO names( @@ -7221,32 +7258,6 @@ export class PgDataStore if (nameZonefile.rowCount === 0) { return; } - // The `names` and `zonefiles` tables only track latest zonefile changes. We need to check - // `nft_custody` for the latest name owner, but only for names that were NOT imported from v1 - // since they did not generate an NFT event for us to track. - if (nameZonefile.rows[0].registered_at !== 1) { - let value: Buffer; - try { - value = bnsNameCV(name); - } catch (error) { - return; - } - const nameCustody = await client.query<{ recipient: string }>( - ` - SELECT recipient - FROM ${includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody'} - WHERE asset_identifier = $1 AND value = $2 - `, - [getBnsSmartContractId(chainId), value] - ); - if (nameCustody.rowCount === 0) { - return; - } - return { - ...nameZonefile.rows[0], - address: nameCustody.rows[0].recipient, - }; - } return nameZonefile.rows[0]; }); if (queryResult) { diff --git a/src/event-stream/bns/bns-helpers.ts b/src/event-stream/bns/bns-helpers.ts index a73edfe7..485119f6 100644 --- a/src/event-stream/bns/bns-helpers.ts +++ b/src/event-stream/bns/bns-helpers.ts @@ -1,6 +1,11 @@ -import { ChainID, ClarityType, hexToCV } from '@stacks/transactions'; +import { BufferCV, ChainID, ClarityType, hexToCV, StringAsciiCV } from '@stacks/transactions'; import { hexToBuffer, hexToUtf8String } from '../../helpers'; -import { CoreNodeParsedTxMessage } from '../../event-stream/core-node-message'; +import { + CoreNodeEvent, + CoreNodeEventType, + CoreNodeParsedTxMessage, + NftTransferEvent, +} from '../../event-stream/core-node-message'; import { getCoreNodeEndpoint } from '../../core-rpc/client'; import { StacksMainnet, StacksTestnet } from '@stacks/network'; import { URIType } from 'zone-file/dist/zoneFile'; @@ -244,10 +249,48 @@ function isEventFromBnsContract(event: SmartContractEvent): boolean { ); } +export function parseNameRenewalWithNoZonefileHashFromContractCall( + tx: CoreNodeParsedTxMessage, + chainId: ChainID +): DbBnsName | undefined { + const payload = tx.parsed_tx.payload; + if ( + payload.type_id === TxPayloadTypeID.ContractCall && + payload.function_name === 'name-renewal' && + getBnsContractID(chainId) === `${payload.address}.${payload.contract_name}` && + payload.function_args.length === 5 && + hexToCV(payload.function_args[4].hex).type === ClarityType.OptionalNone + ) { + const namespace = (hexToCV(payload.function_args[0].hex) as BufferCV).buffer.toString('utf8'); + const name = (hexToCV(payload.function_args[1].hex) as BufferCV).buffer.toString('utf8'); + return { + name: `${name}.${namespace}`, + namespace_id: namespace, + // NOTE: We're not using the `new_owner` argument here because there's a bug in the BNS + // contract that doesn't actually transfer the name to the given principal: + // https://github.com/stacks-network/stacks-blockchain/issues/2680, maybe this will be fixed + // in Stacks 2.1 + address: tx.sender_address, + // expire_block will be calculated upon DB insert based on the namespace's lifetime. + expire_block: 0, + registered_at: tx.block_height, + // Since we received no zonefile_hash, the previous one will be reused when writing to DB. + zonefile_hash: '', + zonefile: '', + tx_id: tx.parsed_tx.tx_id, + tx_index: tx.core_tx.tx_index, + status: 'name-renewal', + canonical: true, + }; + } +} + export function parseNameFromContractEvent( event: SmartContractEvent, tx: CoreNodeParsedTxMessage, - blockHeight: number + txEvents: CoreNodeEvent[], + blockHeight: number, + chainId: ChainID ): DbBnsName | undefined { if (!isEventFromBnsContract(event)) { return; @@ -259,19 +302,21 @@ export function parseNameFromContractEvent( return; } let name_address = attachment.attachment.metadata.tx_sender.address; - // Is this a `name-transfer` contract call? If so, record the new owner. - if ( - attachment.attachment.metadata.op === 'name-transfer' && - tx.parsed_tx.payload.type_id === TxPayloadTypeID.ContractCall && - tx.parsed_tx.payload.function_args.length >= 3 && - tx.parsed_tx.payload.function_args[2].type_id === ClarityTypeID.PrincipalStandard - ) { - const decoded = decodeClarityValue(tx.parsed_tx.payload.function_args[2].hex); - const principal = decoded as ClarityValuePrincipalStandard; - name_address = principal.address; + // Is this a `name-transfer`? If so, look for the new owner in an `nft_transfer` event bundled in + // the same transaction. + if (attachment.attachment.metadata.op === 'name-transfer') { + for (const txEvent of txEvents) { + if ( + txEvent.type === CoreNodeEventType.NftTransferEvent && + txEvent.nft_transfer_event.asset_identifier === `${getBnsContractID(chainId)}::names` + ) { + name_address = txEvent.nft_transfer_event.recipient; + break; + } + } } const name: DbBnsName = { - name: attachment.attachment.metadata.name.concat('.', attachment.attachment.metadata.namespace), + name: `${attachment.attachment.metadata.name}.${attachment.attachment.metadata.namespace}`, namespace_id: attachment.attachment.metadata.namespace, address: name_address, // expire_block will be calculated upon DB insert based on the namespace's lifetime. diff --git a/src/event-stream/core-node-message.ts b/src/event-stream/core-node-message.ts index e8dd76a8..1a7452a5 100644 --- a/src/event-stream/core-node-message.ts +++ b/src/event-stream/core-node-message.ts @@ -76,7 +76,7 @@ export interface StxLockEvent extends CoreNodeEventBase { }; } -interface NftTransferEvent extends CoreNodeEventBase { +export interface NftTransferEvent extends CoreNodeEventBase { type: CoreNodeEventType.NftTransferEvent; nft_transfer_event: { /** Fully qualified asset ID, e.g. "ST2ZRX0K27GW0SP3GJCEMHD95TQGJMKB7G9Y0X1MH.contract-name.asset-name" */ diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 18f1acd0..306b3298 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -63,7 +63,11 @@ import { } from 'stacks-encoding-native-js'; import { ChainID } from '@stacks/transactions'; import { BnsContractIdentifier } from './bns/bns-constants'; -import { parseNameFromContractEvent, parseNamespaceFromContractEvent } from './bns/bns-helpers'; +import { + parseNameFromContractEvent, + parseNameRenewalWithNoZonefileHashFromContractCall, + parseNamespaceFromContractEvent, +} from './bns/bns-helpers'; async function handleRawEventRequest( eventPath: string, @@ -199,10 +203,15 @@ async function handleMicroblockMessage( }); const updateData: DataStoreMicroblockUpdateData = { microblocks: dbMicroblocks, - txs: parseDataStoreTxEventData(parsedTxs, msg.events, { - block_height: -1, // TODO: fill during initial db insert - index_block_hash: '', - }), + txs: parseDataStoreTxEventData( + parsedTxs, + msg.events, + { + block_height: -1, // TODO: fill during initial db insert + index_block_hash: '', + }, + chainId + ), }; await db.updateMicroblocks(updateData); } @@ -299,7 +308,7 @@ async function handleBlockMessage( block: dbBlock, microblocks: dbMicroblocks, minerRewards: dbMinerRewards, - txs: parseDataStoreTxEventData(parsedTxs, msg.events, msg), + txs: parseDataStoreTxEventData(parsedTxs, msg.events, msg, chainId), }; await db.update(dbData); @@ -311,7 +320,8 @@ function parseDataStoreTxEventData( blockData: { block_height: number; index_block_hash: string; - } + }, + chainId: ChainID ): DataStoreTxEventData[] { const dbData: DataStoreTxEventData[] = parsedTxs.map(tx => { const dbTx: DataStoreBlockUpdateData['txs'][number] = { @@ -325,16 +335,29 @@ function parseDataStoreTxEventData( names: [], namespaces: [], }; - if (tx.parsed_tx.payload.type_id === TxPayloadTypeID.SmartContract) { - const contractId = `${tx.sender_address}.${tx.parsed_tx.payload.contract_name}`; - dbTx.smartContracts.push({ - tx_id: tx.core_tx.txid, - contract_id: contractId, - block_height: blockData.block_height, - source_code: tx.parsed_tx.payload.code_body, - abi: JSON.stringify(tx.core_tx.contract_abi), - canonical: true, - }); + switch (tx.parsed_tx.payload.type_id) { + case TxPayloadTypeID.SmartContract: + const contractId = `${tx.sender_address}.${tx.parsed_tx.payload.contract_name}`; + dbTx.smartContracts.push({ + tx_id: tx.core_tx.txid, + contract_id: contractId, + block_height: blockData.block_height, + source_code: tx.parsed_tx.payload.code_body, + abi: JSON.stringify(tx.core_tx.contract_abi), + canonical: true, + }); + break; + case TxPayloadTypeID.ContractCall: + // Name renewals can happen without a zonefile_hash. In that case, the BNS contract does NOT + // emit a `name-renewal` contract log, causing us to miss this event. This function catches + // those cases. + const name = parseNameRenewalWithNoZonefileHashFromContractCall(tx, chainId); + if (name) { + dbTx.names.push(name); + } + break; + default: + break; } return dbTx; }); @@ -372,7 +395,13 @@ function parseDataStoreTxEventData( if (!parsedTx) { throw new Error(`Unexpected missing tx during BNS parsing by tx_id ${event.txid}`); } - const name = parseNameFromContractEvent(event, parsedTx, blockData.block_height); + const name = parseNameFromContractEvent( + event, + parsedTx, + events, + blockData.block_height, + chainId + ); if (name) { dbTx.names.push(name); } diff --git a/src/tests-bns/event-server-tests.ts b/src/tests-bns/event-server-tests.ts index 1596da79..c8ea8c61 100644 --- a/src/tests-bns/event-server-tests.ts +++ b/src/tests-bns/event-server-tests.ts @@ -129,6 +129,295 @@ describe('BNS event server tests', () => { expect(namespace.result?.ready_block).toBe(2); }); + test('name-transfer called by a contract other than BNS', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + block_hash: '0x09458029b7c0e43e015bd3202c0f9512c2b394e0481bfd2bdd096ae7b5b862f2', + index_block_hash: '0xad9403fc8d8eaef47816555cac51dca9d934384aa9b2581f9b9085509b2af915', + burn_block_height: 743853, + burn_block_hash: '0x00000000000000000008b9d65609c6b39bb89d7da35433e4b287835d7112d6d4', + burn_block_time: 1657123396, + }) + .addTx({ + tx_id: '0x1234', + sender_address: 'SPP117ENNNDQVQ1G3E0N1AP178GXBTC2YNQ3H7J' + }) + .addTxBnsNamespace({ + namespace_id: 'btc', + lifetime: 1000 + }) + .addTxBnsName({ + name: 'dayslikewater.btc', + namespace_id: 'btc', + zonefile_hash: 'b472a266d0bd89c13706a4132ccfb16f7c3b9fcb', + address: 'SPP117ENNNDQVQ1G3E0N1AP178GXBTC2YNQ3H7J' + }) + .addTxNftEvent({ + asset_event_type_id: DbAssetEventTypeId.Mint, + value: bnsNameCV('dayslikewater.btc'), + asset_identifier: 'SP000000000000000000002Q6VF78.bns::names', + recipient: 'SPP117ENNNDQVQ1G3E0N1AP178GXBTC2YNQ3H7J', + }) + .build(); + await db.update(block); + const microblock = new TestMicroblockStreamBuilder() + .addMicroblock({ + microblock_hash: '0xccdd11fef1792979bc54a9b686e9cc4fc3d64f2a9b2d8ee9d34fe27bfab783a4', + microblock_sequence: 0, + parent_index_block_hash: '0xad9403fc8d8eaef47816555cac51dca9d934384aa9b2581f9b9085509b2af915' + }) + .build(); + await db.updateMicroblocks(microblock); + + const name1 = await db.getName({ + name: 'dayslikewater.btc', + includeUnanchored: true, + chainId: ChainID.Mainnet + }); + expect(name1.found).toBe(true); + expect(name1.result?.namespace_id).toBe('btc'); + expect(name1.result?.tx_id).toBe('0x1234'); + expect(name1.result?.status).toBe('name-register'); + expect(name1.result?.expire_block).toBe(1001); + expect(name1.result?.address).toBe('SPP117ENNNDQVQ1G3E0N1AP178GXBTC2YNQ3H7J'); + + const payload = { + "events": [ + { + "txid": "0xa75ebee2c824c4943bf8494b101ea7ee7d44191b4a8f761582ce99ef28befb19", + "type": "contract_event", + "committed": true, + "event_index": 74, + "contract_event": { + "topic": "print", + "raw_value": "0x0c000000010a6174746163686d656e740c00000003106174746163686d656e742d696e646578010000000000000000000000000000e52b04686173680200000014b472a266d0bd89c13706a4132ccfb16f7c3b9fcb086d657461646174610c00000004046e616d65020000000d646179736c696b657761746572096e616d6573706163650200000003627463026f700d0000000d6e616d652d7472616e736665720974782d73656e6465720516016084eead6adbeee180dc0a855609d10eaf4c17", + "contract_identifier": "SP000000000000000000002Q6VF78.bns" + } + }, + { + "txid": "0xa75ebee2c824c4943bf8494b101ea7ee7d44191b4a8f761582ce99ef28befb19", + "type": "nft_transfer_event", + "committed": true, + "event_index": 73, + "nft_transfer_event": { + "sender": "SPP117ENNNDQVQ1G3E0N1AP178GXBTC2YNQ3H7J", + "raw_value": "0x0c00000002046e616d65020000000d646179736c696b657761746572096e616d6573706163650200000003627463", + "recipient": "SP1TY00PDWJVNVEX7H7KJGS2K2YXHTQMY8C0G1NVP", + "asset_identifier": "SP000000000000000000002Q6VF78.bns::names" + } + }, + { + "txid": "0xa75ebee2c824c4943bf8494b101ea7ee7d44191b4a8f761582ce99ef28befb19", + "type": "stx_transfer_event", + "committed": true, + "event_index": 71, + "stx_transfer_event": { + "amount": "2500", + "sender": "SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.bns-marketplace-v3", + "recipient": "SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C" + } + } + ], + "block_hash": "0x7d18920cc47f731f186fb9f731d2e8d5029bbab6d73fd012ac3e10637a8e4a37", + "miner_txid": "0xbed35e9e7eb7f98583c87743d3860ab63f2506f7f1efe24740cd37f7708de0b4", + "block_height": 2, + "transactions": [ + { + "txid": "0xa75ebee2c824c4943bf8494b101ea7ee7d44191b4a8f761582ce99ef28befb19", + "raw_tx": "0x00000000010400016084eead6adbeee180dc0a855609d10eaf4c1700000000000000020000000000000bb80000e452e9d87e94a2a4364e89af3ab44b3ce1117afb6505721ff5b801294e1280f0616ee4d21a6ef9bcca1ea15ac65477e79df3427f7fd6c41c80938f8cca6d2cd0030200000002000316a6a7a70f41adbe8eae708ed7ec2cbf41a272182012626e732d6d61726b6574706c6163652d76330500000000000186a0020216016084eead6adbeee180dc0a855609d10eaf4c1716000000000000000000000000000000000000000003626e73056e616d65730c00000002046e616d65020000000d646179736c696b657761746572096e616d6573706163650200000003627463100216a6a7a70f41adbe8eae708ed7ec2cbf41a272182012626e732d6d61726b6574706c6163652d76330a6163636570742d626964000000030200000003627463020000000d646179736c696b6577617465720a0200000014b472a266d0bd89c13706a4132ccfb16f7c3b9fcb", + "status": "success", + "tx_index": 25, + "raw_result": "0x0703", + "contract_abi": null, + "execution_cost": { + "runtime": 381500, + "read_count": 42, + "read_length": 96314, + "write_count": 9, + "write_length": 359 + }, + "microblock_hash": null, + "microblock_sequence": null, + "microblock_parent_hash": null + } + ], + "anchored_cost": { + "runtime": 44194708, + "read_count": 4105, + "read_length": 11476905, + "write_count": 546, + "write_length": 47312 + }, + "burn_block_hash": "0x00000000000000000005e28a41cdb7461953b9424b4fd44a9211a145a1c0346d", + "burn_block_time": 1657125225, + "index_block_hash": "0xb70205d38a8666cbd071239b4ec28ae7d12a2c32341118d7c6d4d1e22f56014e", + "burn_block_height": 743854, + "parent_block_hash": "0x09458029b7c0e43e015bd3202c0f9512c2b394e0481bfd2bdd096ae7b5b862f2", + "parent_microblock": "0xccdd11fef1792979bc54a9b686e9cc4fc3d64f2a9b2d8ee9d34fe27bfab783a4", + "matured_miner_rewards": [], + "parent_burn_block_hash": "0x00000000000000000008b9d65609c6b39bb89d7da35433e4b287835d7112d6d4", + "parent_index_block_hash": "0xad9403fc8d8eaef47816555cac51dca9d934384aa9b2581f9b9085509b2af915", + "parent_burn_block_height": 743853, + "confirmed_microblocks_cost": { + "runtime": 48798, + "read_count": 10, + "read_length": 40042, + "write_count": 3, + "write_length": 19 + }, + "parent_microblock_sequence": 0, + "parent_burn_block_timestamp": 1657123396 + }; + + await httpPostRequest({ + host: '127.0.0.1', + port: eventServer.serverAddress.port, + path: '/new_block', + headers: { 'Content-Type': 'application/json' }, + body: Buffer.from(JSON.stringify(payload), 'utf8'), + throwOnNotOK: true, + }); + + const name2 = await db.getName({ + name: 'dayslikewater.btc', + includeUnanchored: true, + chainId: ChainID.Mainnet + }); + expect(name2.found).toBe(true); + expect(name2.result?.namespace_id).toBe('btc'); + expect(name2.result?.tx_id).toBe('0xa75ebee2c824c4943bf8494b101ea7ee7d44191b4a8f761582ce99ef28befb19'); + expect(name2.result?.status).toBe('name-transfer'); + expect(name2.result?.expire_block).toBe(1001); // Unchanged as it was not renewed + expect(name2.result?.address).toBe('SP1TY00PDWJVNVEX7H7KJGS2K2YXHTQMY8C0G1NVP'); + }); + + test('name-renewal called with no zonefile_hash', async () => { + const block = new TestBlockBuilder({ + block_height: 1, + block_hash: '0xf81ef7f114213b9034a4378345a931a97c781fab398c3d7a2053f0d0bf48d311', + index_block_hash: '0xaec282925b5096c0bd98588d25a97e134bcc4f19b6600859fa267cf0ee4eaf2d', + burn_block_height: 726955, + burn_block_hash: '0x00000000000000000001523f01cb4304d39527454d2eec79817b50c033a5c5d9', + burn_block_time: 1647068146, + }) + .addTx({ + tx_id: '0x1234', + sender_address: 'SP3GWTV1SMF9HDS4VY5NMM833CHH266W4YBASVYMZ' + }) + .addTxBnsNamespace({ + namespace_id: 'id', + lifetime: 1000 + }) + .addTxBnsName({ + name: 'friedger.id', + namespace_id: 'id', + zonefile_hash: 'b472a266d0bd89c13706a4132ccfb16f7c3b9fcb', + address: 'SP3GWTV1SMF9HDS4VY5NMM833CHH266W4YBASVYMZ' + }) + .addTxNftEvent({ + asset_event_type_id: DbAssetEventTypeId.Mint, + value: bnsNameCV('friedger.id'), + asset_identifier: 'SP000000000000000000002Q6VF78.bns::names', + recipient: 'SP3GWTV1SMF9HDS4VY5NMM833CHH266W4YBASVYMZ', + }) + .build(); + await db.update(block); + const microblock = new TestMicroblockStreamBuilder() + .addMicroblock({ + microblock_hash: '0x640362ec47c40de3337491993e42efe60d05187431633ab03c3f5d33e70d1f8e', + microblock_sequence: 0, + parent_index_block_hash: '0xaec282925b5096c0bd98588d25a97e134bcc4f19b6600859fa267cf0ee4eaf2d' + }) + .build(); + await db.updateMicroblocks(microblock); + + const name1 = await db.getName({ + name: 'friedger.id', + includeUnanchored: true, + chainId: ChainID.Mainnet + }); + expect(name1.found).toBe(true); + expect(name1.result?.namespace_id).toBe('id'); + expect(name1.result?.tx_id).toBe('0x1234'); + expect(name1.result?.status).toBe('name-register'); + expect(name1.result?.expire_block).toBe(1001); + expect(name1.result?.address).toBe('SP3GWTV1SMF9HDS4VY5NMM833CHH266W4YBASVYMZ'); + + const payload = { + "events": [], + "block_hash": "0xaaee893667244adcb8581abac372f1f8c385d402b71e8e8b4ac91e8066024fd5", + "miner_txid": "0x6ff493c6b98b9cff0638c7c5276af8e627b8ed779965a5f1c11bbc0810834b3e", + "block_height": 2, + "transactions": [ + { + "txid": "0xf037c8da8210e2a348bbecd3bc44901de875d3774c5fce49cb75d95f2dc2ca4d", + "raw_tx": "0x00000000010500e1cd6c39a3d316e49bf16b4a20636462231b84f200000000000000000000000000000000000094f2c8529dcb8a55a5cfd4434c68cae9cd54f26f01c656369585db3ba364150a4fead679adf35cf5ba1026656b3873daf3380f48ec6dcc175ada868e531decf5001d04c185cad28a3f5299d3fcbcbcbe66b2e1e227000000000000000000000000000186a0000064cc0eb565e85c0d4110c9a760c8fdad21999409f89320e355f326c144b8ada4268244f80734170cea96f683d2431b59f07f276a10efc80793d4dceef8feb2310302000000000216000000000000000000000000000000000000000003626e730c6e616d652d72656e6577616c000000050200000002696402000000086672696564676572010000000000000000000000000001a72a0909", + "status": "success", + "tx_index": 2, + "raw_result": "0x0703", + "contract_abi": null, + "execution_cost": { + "runtime": 184253, + "read_count": 11, + "read_length": 43250, + "write_count": 1, + "write_length": 143 + }, + "microblock_hash": null, + "microblock_sequence": null, + "microblock_parent_hash": null + } + ], + "anchored_cost": { + "runtime": 28375070, + "read_count": 8888, + "read_length": 1085153, + "write_count": 593, + "write_length": 156284 + }, + "burn_block_hash": "0x0000000000000000000552fb5fd8c08ad8f1ef30c239369a8a3380ec1566047a", + "burn_block_time": 1647068392, + "index_block_hash": "0x9ff46918054b1aa94571a60e14921a56977f26af2adcbf4a7f64138566feba48", + "burn_block_height": 726956, + "parent_block_hash": "0xf81ef7f114213b9034a4378345a931a97c781fab398c3d7a2053f0d0bf48d311", + "parent_microblock": "0x640362ec47c40de3337491993e42efe60d05187431633ab03c3f5d33e70d1f8e", + "matured_miner_rewards": [], + "parent_burn_block_hash": "0x00000000000000000001523f01cb4304d39527454d2eec79817b50c033a5c5d9", + "parent_index_block_hash": "0xaec282925b5096c0bd98588d25a97e134bcc4f19b6600859fa267cf0ee4eaf2d", + "parent_burn_block_height": 726955, + "confirmed_microblocks_cost": { + "runtime": 360206, + "read_count": 38, + "read_length": 95553, + "write_count": 8, + "write_length": 378 + }, + "parent_microblock_sequence": 0, + "parent_burn_block_timestamp": 1647068146 + }; + + await httpPostRequest({ + host: '127.0.0.1', + port: eventServer.serverAddress.port, + path: '/new_block', + headers: { 'Content-Type': 'application/json' }, + body: Buffer.from(JSON.stringify(payload), 'utf8'), + throwOnNotOK: true, + }); + + const name2 = await db.getName({ + name: 'friedger.id', + includeUnanchored: true, + chainId: ChainID.Mainnet + }); + expect(name2.found).toBe(true); + expect(name2.result?.namespace_id).toBe('id'); + expect(name2.result?.tx_id).toBe('0xf037c8da8210e2a348bbecd3bc44901de875d3774c5fce49cb75d95f2dc2ca4d'); + expect(name2.result?.status).toBe('name-renewal'); + expect(name2.result?.expire_block).toBe(1002); // Updated correctly + expect(name2.result?.address).toBe('SP3GWTV1SMF9HDS4VY5NMM833CHH266W4YBASVYMZ'); + }); + test('/attachments/new with re-orged zonefiles', async () => { const block1 = new TestBlockBuilder({ block_height: 1,