Skip to content

Commit e656520

Browse files
authored
fix: /v1/names/[:name] name resolution (#1159)
* chore: set up * fix: bns name custody resolution * fix: bns api tests * fix: bns tests * fix: datastore tests
1 parent 8543243 commit e656520

File tree

11 files changed

+453
-199
lines changed

11 files changed

+453
-199
lines changed

src/api/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export async function startApiServer(opts: {
244244
const router = express.Router();
245245
router.use(cors());
246246
router.use('/namespaces', createBnsNamespacesRouter(datastore));
247-
router.use('/names', createBnsNamesRouter(datastore));
247+
router.use('/names', createBnsNamesRouter(datastore, chainId));
248248
router.use('/addresses', createBnsAddressesRouter(datastore));
249249
return router;
250250
})()

src/api/routes/bns/names.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { parsePagingQueryInput } from '../../../api/pagination';
55
import { isUnanchoredRequest } from '../../query-helpers';
66
import { bnsBlockchain, BnsErrors } from '../../../bns-constants';
77
import { BnsGetNameInfoResponse } from '@stacks/stacks-blockchain-api-types';
8+
import { ChainID } from '@stacks/transactions';
89

9-
export function createBnsNamesRouter(db: DataStore): express.Router {
10+
export function createBnsNamesRouter(db: DataStore, chainId: ChainID): express.Router {
1011
const router = express.Router();
1112

1213
router.get(

src/datastore/postgres-store.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
has0xPrefix,
4040
isValidPrincipal,
4141
isSmartContractTx,
42+
bnsNameCV,
4243
} from '../helpers';
4344
import {
4445
DataStore,
@@ -103,7 +104,7 @@ import {
103104
} from '@stacks/stacks-blockchain-api-types';
104105
import { getTxTypeId } from '../api/controllers/db-controller';
105106
import { isProcessableTokenMetadata } from '../event-stream/tokens-contract-handler';
106-
import { ClarityAbi } from '@stacks/transactions';
107+
import { ChainID, ClarityAbi } from '@stacks/transactions';
107108
import {
108109
PgAddressNotificationPayload,
109110
PgBlockNotificationPayload,
@@ -2403,6 +2404,10 @@ export class PgDataStore
24032404
} else {
24042405
updatedEntities.markedNonCanonical.nftEvents += nftResult.rowCount;
24052406
}
2407+
if (nftResult.rowCount) {
2408+
await this.refreshMaterializedView(client, 'nft_custody');
2409+
await this.refreshMaterializedView(client, 'nft_custody_unanchored');
2410+
}
24062411

24072412
const contractLogResult = await client.query(
24082413
`
@@ -6991,13 +6996,17 @@ export class PgDataStore
69916996
async getName({
69926997
name,
69936998
includeUnanchored,
6999+
chainId,
69947000
}: {
69957001
name: string;
69967002
includeUnanchored: boolean;
7003+
chainId: ChainID;
69977004
}): Promise<FoundOrNot<DbBnsName & { index_block_hash: string }>> {
69987005
const queryResult = await this.queryTx(async client => {
69997006
const maxBlockHeight = await this.getMaxBlockHeight(client, { includeUnanchored });
7000-
return await client.query<DbBnsName & { tx_id: Buffer; index_block_hash: Buffer }>(
7007+
const nameZonefile = await client.query<
7008+
DbBnsName & { tx_id: Buffer; index_block_hash: Buffer }
7009+
>(
70017010
`
70027011
SELECT DISTINCT ON (names.name) names.name, names.*, zonefiles.zonefile
70037012
FROM names
@@ -7006,18 +7015,52 @@ export class PgDataStore
70067015
AND registered_at <= $2
70077016
AND canonical = true AND microblock_canonical = true
70087017
ORDER BY name, registered_at DESC, tx_index DESC
7009-
LIMIT 1
70107018
`,
70117019
[name, maxBlockHeight]
70127020
);
7021+
if (nameZonefile.rowCount === 0) {
7022+
return;
7023+
}
7024+
// The `names` and `zonefiles` tables only track latest zonefile changes. We need to check
7025+
// `nft_custody` for the latest name owner, but only for names that were NOT imported from v1
7026+
// since they did not generate an NFT event for us to track.
7027+
if (nameZonefile.rows[0].registered_at !== 0) {
7028+
let value: Buffer;
7029+
try {
7030+
value = bnsNameCV(name);
7031+
} catch (error) {
7032+
return;
7033+
}
7034+
const assetIdentifier =
7035+
chainId === ChainID.Mainnet
7036+
? 'SP000000000000000000002Q6VF78.bns::names'
7037+
: 'ST000000000000000000002AMW42H.bns::names';
7038+
const nftCustody = includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody';
7039+
const nameCustody = await client.query<{ recipient: string }>(
7040+
`
7041+
SELECT recipient
7042+
FROM ${nftCustody}
7043+
WHERE asset_identifier = $1 AND value = $2
7044+
`,
7045+
[assetIdentifier, value]
7046+
);
7047+
if (nameCustody.rowCount === 0) {
7048+
return;
7049+
}
7050+
return {
7051+
...nameZonefile.rows[0],
7052+
address: nameCustody.rows[0].recipient,
7053+
};
7054+
}
7055+
return nameZonefile.rows[0];
70137056
});
7014-
if (queryResult.rowCount > 0) {
7057+
if (queryResult) {
70157058
return {
70167059
found: true,
70177060
result: {
7018-
...queryResult.rows[0],
7019-
tx_id: bufferToHexPrefixString(queryResult.rows[0].tx_id),
7020-
index_block_hash: bufferToHexPrefixString(queryResult.rows[0].index_block_hash),
7061+
...queryResult,
7062+
tx_id: bufferToHexPrefixString(queryResult.tx_id),
7063+
index_block_hash: bufferToHexPrefixString(queryResult.index_block_hash),
70217064
},
70227065
};
70237066
}

src/helpers.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as winston from 'winston';
99
import { isValidStacksAddress, stacksToBitcoinAddress } from 'stacks-encoding-native-js';
1010
import * as btc from 'bitcoinjs-lib';
1111
import * as BN from 'bn.js';
12-
import { ChainID } from '@stacks/transactions';
12+
import { bufferCV, ChainID, cvToHex, tupleCV } from '@stacks/transactions';
1313
import BigNumber from 'bignumber.js';
1414
import {
1515
CliConfigSetColors,
@@ -944,6 +944,22 @@ export function parseDataUrl(
944944
}
945945
}
946946

947+
/**
948+
* Creates a Clarity tuple Buffer from a BNS name, just how it is stored in
949+
* received NFT events.
950+
*/
951+
export function bnsNameCV(name: string): Buffer {
952+
const components = name.split('.');
953+
return hexToBuffer(
954+
cvToHex(
955+
tupleCV({
956+
name: bufferCV(Buffer.from(components[0])),
957+
namespace: bufferCV(Buffer.from(components[1])),
958+
})
959+
)
960+
);
961+
}
962+
947963
export function getSendManyContract(chainId: ChainID) {
948964
const contractId =
949965
chainId === ChainID.Mainnet

src/migrations/1636130197558_nft_custody.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
2626
`);
2727

2828
pgm.createIndex('nft_custody', ['recipient', 'asset_identifier']);
29+
pgm.createIndex('nft_custody', 'asset_identifier');
2930
}

src/migrations/1640037852136_nft_custody_unanchored.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ export async function up(pgm: MigrationBuilder): Promise<void> {
3030
`);
3131

3232
pgm.createIndex('nft_custody_unanchored', ['recipient', 'asset_identifier']);
33+
pgm.createIndex('nft_custody_unanchored', 'asset_identifier');
3334
}

src/test-utils/test-builders.ts

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DataStoreTxEventData,
1313
DbAssetEventTypeId,
1414
DbBlock,
15+
DbBnsName,
1516
DbEventTypeId,
1617
DbFtEvent,
1718
DbMempoolTx,
@@ -73,12 +74,20 @@ const COINBASE_AMOUNT = 15_000_000_000_000n;
7374
const TX_FEES_ANCHORED = 1_000_000_000_000n;
7475
const TX_FEES_STREAMED_CONFIRMED = 2_000_000_000_000n;
7576
const TX_FEES_STREAMED_PRODUCED = 3_000_000_000_000n;
77+
const BNS_NAME = 'test.btc';
78+
const BNS_NAMESPACE_ID = 'btc';
79+
const ZONEFILE =
80+
'$ORIGIN test.btc\n$TTL 3600\n_http._tcp IN URI 10 1 "https://blockstack.s3.amazonaws.com/test.btc"\n';
81+
const ZONEFILE_HASH = 'b100a68235244b012854a95f9114695679002af9';
7682

7783
interface TestBlockArgs {
7884
block_height?: number;
7985
block_hash?: string;
8086
index_block_hash?: string;
8187
burn_block_hash?: string;
88+
burn_block_time?: number;
89+
burn_block_height?: number;
90+
miner_txid?: string;
8291
parent_index_block_hash?: string;
8392
parent_block_hash?: string;
8493
parent_microblock_hash?: string;
@@ -100,10 +109,10 @@ function testBlock(args?: TestBlockArgs): DbBlock {
100109
parent_microblock_hash: args?.parent_microblock_hash ?? '',
101110
parent_microblock_sequence: args?.parent_microblock_sequence ?? 0,
102111
block_height: args?.block_height ?? BLOCK_HEIGHT,
103-
burn_block_time: BURN_BLOCK_TIME,
112+
burn_block_time: args?.burn_block_time ?? BURN_BLOCK_TIME,
104113
burn_block_hash: args?.burn_block_hash ?? BURN_BLOCK_HASH,
105-
burn_block_height: BURN_BLOCK_HEIGHT,
106-
miner_txid: '0x4321',
114+
burn_block_height: args?.burn_block_height ?? BURN_BLOCK_HEIGHT,
115+
miner_txid: args?.miner_txid ?? '0x4321',
107116
canonical: args?.canonical ?? true,
108117
execution_cost_read_count: 0,
109118
execution_cost_read_length: 0,
@@ -118,6 +127,9 @@ interface TestMicroblockArgs {
118127
microblock_parent_hash?: string;
119128
microblock_sequence?: number;
120129
parent_index_block_hash?: string;
130+
parent_burn_block_time?: number;
131+
parent_burn_block_hash?: string;
132+
parent_burn_block_height?: number;
121133
}
122134

123135
/**
@@ -131,9 +143,9 @@ function testMicroblock(args?: TestMicroblockArgs): DbMicroblockPartial {
131143
microblock_sequence: args?.microblock_sequence ?? 0,
132144
microblock_parent_hash: args?.microblock_parent_hash ?? BLOCK_HASH,
133145
parent_index_block_hash: args?.parent_index_block_hash ?? INDEX_BLOCK_HASH,
134-
parent_burn_block_height: BURN_BLOCK_HEIGHT,
135-
parent_burn_block_hash: BURN_BLOCK_HASH,
136-
parent_burn_block_time: BURN_BLOCK_TIME,
146+
parent_burn_block_height: args?.parent_burn_block_height ?? BURN_BLOCK_HEIGHT,
147+
parent_burn_block_hash: args?.parent_burn_block_hash ?? BURN_BLOCK_HASH,
148+
parent_burn_block_time: args?.parent_burn_block_time ?? BURN_BLOCK_TIME,
137149
};
138150
}
139151

@@ -377,16 +389,6 @@ interface TestSmartContractLogEventArgs {
377389
tx_index?: number;
378390
}
379391

380-
interface TestStxEventLockArgs {
381-
tx_id?: string;
382-
block_height?: number;
383-
event_index?: number;
384-
tx_index?: number;
385-
locked_amount?: number;
386-
unlock_height?: number;
387-
locked_address?: string;
388-
}
389-
390392
/**
391393
* Generate a test contract log event.
392394
* @param args - Optional event data
@@ -406,6 +408,16 @@ function testSmartContractLogEvent(args?: TestSmartContractLogEventArgs): DbSmar
406408
};
407409
}
408410

411+
interface TestStxEventLockArgs {
412+
tx_id?: string;
413+
block_height?: number;
414+
event_index?: number;
415+
tx_index?: number;
416+
locked_amount?: number;
417+
unlock_height?: number;
418+
locked_address?: string;
419+
}
420+
409421
/**
410422
* Generate a test stx lock event.
411423
* @param args - Optional event data
@@ -482,6 +494,47 @@ function testMinerReward(args?: TestMinerRewardArgs): DbMinerReward {
482494
};
483495
}
484496

497+
interface TestBnsNameArgs {
498+
name?: string;
499+
address?: string;
500+
namespace_id?: string;
501+
registered_at?: number;
502+
expire_block?: number;
503+
grace_period?: number;
504+
renewal_deadline?: number;
505+
resolver?: string;
506+
zonefile?: string;
507+
zonefile_hash?: string;
508+
tx_id?: string;
509+
tx_index?: number;
510+
status?: string;
511+
canonical?: boolean;
512+
}
513+
514+
/**
515+
* Generate a test BNS name
516+
* @param args - Optional name data
517+
* @returns `DbBnsName`
518+
*/
519+
function testBnsName(args?: TestBnsNameArgs): DbBnsName {
520+
return {
521+
name: args?.name ?? BNS_NAME,
522+
address: args?.address ?? SENDER_ADDRESS,
523+
namespace_id: args?.namespace_id ?? BNS_NAMESPACE_ID,
524+
registered_at: args?.registered_at ?? BLOCK_HEIGHT,
525+
expire_block: args?.expire_block ?? 0,
526+
grace_period: args?.grace_period,
527+
renewal_deadline: args?.renewal_deadline,
528+
resolver: args?.resolver,
529+
zonefile: args?.zonefile ?? ZONEFILE,
530+
zonefile_hash: args?.zonefile_hash ?? ZONEFILE_HASH,
531+
tx_id: args?.tx_id ?? TX_ID,
532+
tx_index: args?.tx_index ?? 0,
533+
status: args?.status ?? 'name-register',
534+
canonical: args?.canonical ?? true,
535+
};
536+
}
537+
485538
/**
486539
* Builder that creates a test block with any number of transactions and events so populating
487540
* the DB for testing becomes easier.
@@ -597,6 +650,15 @@ export class TestBlockBuilder {
597650
return this;
598651
}
599652

653+
addTxBnsName(args?: TestBnsNameArgs): TestBlockBuilder {
654+
const defaultArgs: TestBnsNameArgs = {
655+
tx_id: this.txData.tx.tx_id,
656+
registered_at: this.block.block_height,
657+
};
658+
this.txData.names.push(testBnsName({ ...defaultArgs, ...args }));
659+
return this;
660+
}
661+
600662
build(): DataStoreBlockUpdateData {
601663
return this.data;
602664
}
@@ -647,6 +709,7 @@ export class TestMicroblockStreamBuilder {
647709
microblock_hash: this.microblock.microblock_hash,
648710
microblock_sequence: this.microblock.microblock_sequence,
649711
tx_index: ++this.txIndex,
712+
index_block_hash: '',
650713
};
651714
this.data.txs.push(testTx({ ...defaultBlockArgs, ...args }));
652715
this.eventIndex = -1;
@@ -672,6 +735,15 @@ export class TestMicroblockStreamBuilder {
672735
return this;
673736
}
674737

738+
addTxBnsName(args?: TestBnsNameArgs): TestMicroblockStreamBuilder {
739+
const defaultArgs: TestBnsNameArgs = {
740+
tx_id: this.txData.tx.tx_id,
741+
tx_index: this.txIndex,
742+
};
743+
this.txData.names.push(testBnsName({ ...defaultArgs, ...args }));
744+
return this;
745+
}
746+
675747
build(): DataStoreMicroblockUpdateData {
676748
return this.data;
677749
}

0 commit comments

Comments
 (0)