Skip to content

Commit 8797ded

Browse files
authored
fix: resolve bns names correctly in /v1/addresses/stacks/[:address] (#1175)
* fix: bns name resolution * fix: name-transfer test
1 parent 666b8a6 commit 8797ded

File tree

6 files changed

+259
-114
lines changed

6 files changed

+259
-114
lines changed

src/api/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export async function startApiServer(opts: {
245245
router.use(cors());
246246
router.use('/namespaces', createBnsNamespacesRouter(datastore));
247247
router.use('/names', createBnsNamesRouter(datastore, chainId));
248-
router.use('/addresses', createBnsAddressesRouter(datastore));
248+
router.use('/addresses', createBnsAddressesRouter(datastore, chainId));
249249
return router;
250250
})()
251251
);

src/api/routes/bns/addresses.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import * as express from 'express';
22
import { asyncHandler } from '../../async-handler';
33
import { DataStore } from '../../../datastore/common';
44
import { isUnanchoredRequest } from '../../query-helpers';
5+
import { ChainID } from '@stacks/transactions';
56

67
const SUPPORTED_BLOCKCHAINS = ['stacks'];
78

8-
export function createBnsAddressesRouter(db: DataStore): express.Router {
9+
export function createBnsAddressesRouter(db: DataStore, chainId: ChainID): express.Router {
910
const router = express.Router();
1011
router.get(
1112
'/:blockchain/:address',
@@ -20,6 +21,7 @@ export function createBnsAddressesRouter(db: DataStore): express.Router {
2021
const namesByAddress = await db.getNamesByAddressList({
2122
address: address,
2223
includeUnanchored,
24+
chainId,
2325
});
2426
if (namesByAddress.found) {
2527
res.json({ names: namesByAddress.result });

src/datastore/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@stacks/stacks-blockchain-api-types';
2121
import { getTxSenderAddress } from '../event-stream/reader';
2222
import { RawTxQueryResult } from './postgres-store';
23-
import { ClarityAbi } from '@stacks/transactions';
23+
import { ChainID, ClarityAbi } from '@stacks/transactions';
2424
import { Block } from '@stacks/stacks-blockchain-api-types';
2525

2626
export interface DbBlock {
@@ -947,6 +947,7 @@ export interface DataStore extends DataStoreEventEmitter {
947947
getNamesByAddressList(args: {
948948
address: string;
949949
includeUnanchored: boolean;
950+
chainId: ChainID;
950951
}): Promise<FoundOrNot<string[]>>;
951952
getNamesList(args: {
952953
page: number;

src/datastore/postgres-store.ts

Lines changed: 73 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import {
4040
isValidPrincipal,
4141
isSmartContractTx,
4242
bnsNameCV,
43+
getBnsSmartContractId,
44+
bnsHexValueToName,
4345
} from '../helpers';
4446
import {
4547
DataStore,
@@ -104,7 +106,7 @@ import {
104106
} from '@stacks/stacks-blockchain-api-types';
105107
import { getTxTypeId } from '../api/controllers/db-controller';
106108
import { isProcessableTokenMetadata } from '../event-stream/tokens-contract-handler';
107-
import { ChainID, ClarityAbi } from '@stacks/transactions';
109+
import { ChainID, ClarityAbi, hexToCV, TupleCV } from '@stacks/transactions';
108110
import {
109111
PgAddressNotificationPayload,
110112
PgBlockNotificationPayload,
@@ -7032,18 +7034,13 @@ export class PgDataStore
70327034
} catch (error) {
70337035
return;
70347036
}
7035-
const assetIdentifier =
7036-
chainId === ChainID.Mainnet
7037-
? 'SP000000000000000000002Q6VF78.bns::names'
7038-
: 'ST000000000000000000002AMW42H.bns::names';
7039-
const nftCustody = includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody';
70407037
const nameCustody = await client.query<{ recipient: string }>(
70417038
`
70427039
SELECT recipient
7043-
FROM ${nftCustody}
7040+
FROM ${includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody'}
70447041
WHERE asset_identifier = $1 AND value = $2
70457042
`,
7046-
[assetIdentifier, value]
7043+
[getBnsSmartContractId(chainId), value]
70477044
);
70487045
if (nameCustody.rowCount === 0) {
70497046
return;
@@ -7167,59 +7164,88 @@ export class PgDataStore
71677164
async getNamesByAddressList({
71687165
address,
71697166
includeUnanchored,
7167+
chainId,
71707168
}: {
71717169
address: string;
71727170
includeUnanchored: boolean;
7171+
chainId: ChainID;
71737172
}): Promise<FoundOrNot<string[]>> {
71747173
const queryResult = await this.queryTx(async client => {
71757174
const maxBlockHeight = await this.getMaxBlockHeight(client, { includeUnanchored });
7176-
const query = await client.query<{ name: string }>(
7175+
// 1. Get subdomains owned by this address.
7176+
// These don't produce NFT events so we have to look directly at the `subdomains` table.
7177+
const subdomainsQuery = await client.query<{ fully_qualified_subdomain: string }>(
71777178
`
7178-
WITH address_names AS(
7179-
(
7180-
SELECT name
7181-
FROM names
7182-
WHERE address = $1
7183-
AND registered_at <= $2
7184-
AND canonical = true AND microblock_canonical = true
7179+
WITH addr_subdomains AS (
7180+
SELECT DISTINCT ON (fully_qualified_subdomain)
7181+
fully_qualified_subdomain
7182+
FROM
7183+
subdomains
7184+
WHERE
7185+
owner = $1
7186+
AND block_height <= $2
7187+
AND canonical = TRUE
7188+
AND microblock_canonical = TRUE
71857189
)
7186-
UNION ALL (
7187-
SELECT DISTINCT ON (fully_qualified_subdomain) fully_qualified_subdomain as name
7188-
FROM subdomains
7189-
WHERE owner = $1
7190-
AND block_height <= $2
7191-
AND canonical = true AND microblock_canonical = true
7192-
)),
7193-
7194-
latest_names AS(
7195-
(
7196-
SELECT DISTINCT ON (names.name) names.name, address, registered_at as block_height, tx_index
7197-
FROM names, address_names
7198-
WHERE address_names.name = names.name
7199-
AND canonical = true AND microblock_canonical = true
7200-
ORDER BY names.name, registered_at DESC, tx_index DESC
7201-
)
7202-
UNION ALL(
7203-
SELECT DISTINCT ON (fully_qualified_subdomain) fully_qualified_subdomain as name, owner as address, block_height, tx_index
7204-
FROM subdomains, address_names
7205-
WHERE fully_qualified_subdomain = address_names.name
7206-
AND canonical = true AND microblock_canonical = true
7207-
ORDER BY fully_qualified_subdomain, block_height DESC, tx_index DESC
7208-
))
7209-
7210-
SELECT name from latest_names
7211-
WHERE address = $1
7212-
ORDER BY name, block_height DESC, tx_index DESC
7190+
SELECT DISTINCT ON (fully_qualified_subdomain)
7191+
fully_qualified_subdomain
7192+
FROM
7193+
subdomains
7194+
INNER JOIN addr_subdomains USING (fully_qualified_subdomain)
7195+
WHERE
7196+
canonical = TRUE
7197+
AND microblock_canonical = TRUE
7198+
ORDER BY
7199+
fully_qualified_subdomain
72137200
`,
72147201
[address, maxBlockHeight]
72157202
);
7216-
return query;
7203+
// 2. Get names owned by this address which were imported from Blockstack v1.
7204+
// These also don't have an associated NFT event so we have to look directly at the `names` table,
7205+
// however, we'll also check if any of these names are still owned by the same user.
7206+
const importedNamesQuery = await client.query<{ name: string }>(
7207+
`
7208+
SELECT
7209+
name
7210+
FROM
7211+
names
7212+
WHERE
7213+
address = $1
7214+
AND registered_at = 0
7215+
AND canonical = TRUE
7216+
AND microblock_canonical = TRUE
7217+
`,
7218+
[address]
7219+
);
7220+
const oldImportedNamesQuery = await client.query<{ value: string }>(
7221+
`
7222+
SELECT value
7223+
FROM ${includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody'}
7224+
WHERE recipient <> $1 AND value = ANY($2)
7225+
`,
7226+
[address, importedNamesQuery.rows.map(i => bnsNameCV(i.name))]
7227+
);
7228+
const oldImportedNames = oldImportedNamesQuery.rows.map(i => bnsHexValueToName(i.value));
7229+
// 3. Get newer NFT names owned by this address.
7230+
const nftNamesQuery = await client.query<{ value: string }>(
7231+
`
7232+
SELECT value
7233+
FROM ${includeUnanchored ? 'nft_custody_unanchored' : 'nft_custody'}
7234+
WHERE recipient = $1 AND asset_identifier = $2
7235+
`,
7236+
[address, getBnsSmartContractId(chainId)]
7237+
);
7238+
const results: Set<string> = new Set([
7239+
...subdomainsQuery.rows.map(i => i.fully_qualified_subdomain),
7240+
...importedNamesQuery.rows.map(i => i.name).filter(i => !oldImportedNames.includes(i)),
7241+
...nftNamesQuery.rows.map(i => bnsHexValueToName(i.value)),
7242+
]);
7243+
return Array.from(results.values()).sort();
72177244
});
7218-
7219-
if (queryResult.rowCount > 0) {
7245+
if (queryResult.length > 0) {
72207246
return {
72217247
found: true,
7222-
result: queryResult.rows.map(r => r.name),
7248+
result: queryResult,
72237249
};
72247250
}
72257251
return { found: false } as const;

src/helpers.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ 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 { bufferCV, ChainID, cvToHex, tupleCV } from '@stacks/transactions';
12+
import {
13+
BufferCV,
14+
bufferCV,
15+
ChainID,
16+
cvToHex,
17+
hexToCV,
18+
TupleCV,
19+
tupleCV,
20+
} from '@stacks/transactions';
1321
import BigNumber from 'bignumber.js';
1422
import {
1523
CliConfigSetColors,
@@ -960,6 +968,24 @@ export function bnsNameCV(name: string): Buffer {
960968
);
961969
}
962970

971+
/**
972+
* Converts a hex Clarity value for a BNS name NFT into the string name
973+
* @param hex - hex encoded Clarity value of BNS name NFT
974+
* @returns BNS name string
975+
*/
976+
export function bnsHexValueToName(hex: string): string {
977+
const tuple = hexToCV(hex) as TupleCV;
978+
const name = tuple.data.name as BufferCV;
979+
const namespace = tuple.data.namespace as BufferCV;
980+
return `${name.buffer.toString('utf8')}.${namespace.buffer.toString('utf8')}`;
981+
}
982+
983+
export function getBnsSmartContractId(chainId: ChainID): string {
984+
return chainId === ChainID.Mainnet
985+
? 'SP000000000000000000002Q6VF78.bns::names'
986+
: 'ST000000000000000000002AMW42H.bns::names';
987+
}
988+
963989
export function getSendManyContract(chainId: ChainID) {
964990
const contractId =
965991
chainId === ChainID.Mainnet

0 commit comments

Comments
 (0)