Skip to content

Commit

Permalink
feat: get bns name from bns v2 api
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo authored and edgarkhanzadian committed Nov 8, 2024
1 parent 70c81d7 commit 2bf31d1
Show file tree
Hide file tree
Showing 7 changed files with 399 additions and 167 deletions.
2 changes: 2 additions & 0 deletions packages/models/src/network/network.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const BESTINSLOT_API_BASE_URL_TESTNET = 'https://leatherapi_testnet.besti

export const STX20_API_BASE_URL_MAINNET = 'https://api.stx20.com/api/v1';

export const BNS_V2_API_BASE_URL = 'https://api.bnsv2.com';

// Copied from @stacks/transactions to avoid dependencies
export enum ChainID {
Testnet = 2147483648,
Expand Down
1 change: 1 addition & 0 deletions packages/query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"alex-sdk": "2.1.4",
"axios": "1.7.7",
"bignumber.js": "9.1.2",
"bns-v2-sdk": "1.3.1",
"lodash.get": "4.4.2",
"p-queue": "8.0.1",
"url-join": "5.0.0",
Expand Down
16 changes: 7 additions & 9 deletions packages/query/src/stacks/bns/bns.query.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { QueryFunctionContext, useQuery } from '@tanstack/react-query';

import { NetworkModes } from '@leather.io/models';

import { useCurrentNetworkState } from '../../leather-query-provider';
import { StacksQueryPrefixes } from '../../query-prefixes';
import { StacksClient, useStacksClient } from '../stacks-client';
import { fetchNamesForAddress } from './bns.utils';

const staleTime = 24 * 60 * 60 * 1000; // 24 hours
Expand All @@ -17,26 +18,23 @@ const queryOptions = {

interface CreateGetBnsNamesOwnedByAddressQueryOptionsArgs {
address: string;
client: StacksClient;
isTestnet: boolean;
network: NetworkModes;
}
export function createGetBnsNamesOwnedByAddressQueryOptions({
address,
client,
isTestnet,
network,
}: CreateGetBnsNamesOwnedByAddressQueryOptionsArgs) {
return {
enabled: address !== '',
queryKey: [StacksQueryPrefixes.GetBnsNamesByAddress, address],
queryFn: async ({ signal }: QueryFunctionContext) =>
fetchNamesForAddress({ client, address, isTestnet, signal }),
fetchNamesForAddress({ address, network, signal }),
...queryOptions,
} as const;
}

export function useGetBnsNamesOwnedByAddressQuery(address: string) {
const client = useStacksClient();
const { isTestnet } = useCurrentNetworkState();
const { mode } = useCurrentNetworkState();

return useQuery(createGetBnsNamesOwnedByAddressQueryOptions({ address, client, isTestnet }));
return useQuery(createGetBnsNamesOwnedByAddressQueryOptions({ address, network: mode }));
}
20 changes: 20 additions & 0 deletions packages/query/src/stacks/bns/bns.schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod';

const bnsV2NameSchema = z.object({
full_name: z.string(),
name_string: z.string(),
namespace_string: z.string(),
owner: z.string(),
registered_at: z.string(),
renewal_height: z.string(),
stx_burn: z.string(),
revoked: z.boolean(),
});

export const bnsV2NamesByAddressSchema = z.object({
total: z.number(),
current_burn_block: z.number(),
limit: z.number(),
offset: z.number(),
names: z.array(bnsV2NameSchema),
});
57 changes: 57 additions & 0 deletions packages/query/src/stacks/bns/bns.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import axios from 'axios';
import { getPrimaryName } from 'bns-v2-sdk';
import { describe, expect, it, vi } from 'vitest';

import { fetchNamesForAddress } from './bns.utils';

vi.mock('axios');
vi.mock('bns-v2-sdk');

describe('bns.utils', () => {
describe('fetchNamesForAddress', () => {
const mockAddress = 'ST123';
const mockSignal = new AbortController().signal;

it('returns single name without fetching primary name', async () => {
vi.mocked(axios.get).mockResolvedValueOnce({
data: { names: [{ full_name: 'test.btc' }] },
});

const result = await fetchNamesForAddress({
address: mockAddress,
signal: mockSignal,
network: 'mainnet',
});

expect(result).toEqual({ names: ['test.btc'] });
expect(getPrimaryName).not.toHaveBeenCalled();
});

it('orders primary name first when multiple names exist', async () => {
vi.mocked(axios.get).mockResolvedValueOnce({
data: {
names: [
{ full_name: 'secondary.btc' },
{ full_name: 'primary.btc' },
{ full_name: 'another.btc' },
],
},
});

vi.mocked(getPrimaryName).mockResolvedValueOnce({
name: 'primary',
namespace: 'btc',
});

const result = await fetchNamesForAddress({
address: mockAddress,
signal: mockSignal,
network: 'mainnet',
});

expect(result).toEqual({
names: ['primary.btc', 'secondary.btc', 'another.btc'],
});
});
});
});
52 changes: 39 additions & 13 deletions packages/query/src/stacks/bns/bns.utils.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,63 @@
import { parseZoneFile } from '@fungible-systems/zone-file';
import { BnsNamesOwnByAddressResponse } from '@stacks/stacks-blockchain-api-types';
import axios from 'axios';
import { getPrimaryName } from 'bns-v2-sdk';
import { z } from 'zod';

import { BNS_V2_API_BASE_URL, NetworkModes } from '@leather.io/models';
import { isString, isUndefined } from '@leather.io/utils';

import { StacksClient } from '../stacks-client';
import { bnsV2NamesByAddressSchema } from './bns.schemas';

/**
* Fetch names owned by an address.
*/
interface FetchNamesForAddressArgs {
client: StacksClient;
address: string;
isTestnet: boolean;
network: NetworkModes;
signal: AbortSignal;
}

type BnsV2NamesByAddressResponse = z.infer<typeof bnsV2NamesByAddressSchema>;

async function fetchPrimaryName(address: string, network: NetworkModes) {
try {
const res = await getPrimaryName({ address, network });
return `${res?.name}.${res?.namespace}`;
} catch (error) {
// Ignore error
return undefined;
}
}

export async function fetchNamesForAddress({
client,
address,
isTestnet,
signal,
network,
}: FetchNamesForAddressArgs): Promise<BnsNamesOwnByAddressResponse> {
const fetchFromApi = async () => {
return client.getNamesOwnedByAddress(address, signal);
};
if (isTestnet) {
return fetchFromApi();
const res = await axios.get<BnsV2NamesByAddressResponse>(
`${BNS_V2_API_BASE_URL}/names/address/${address}/valid`,
{ signal }
);

const namesResponse = res.data.names.map(name => name.full_name);

// If the address owns multiple names, we need to fetch the primary name from SDK
let primaryName: string | undefined;
if (namesResponse.length > 1) {
primaryName = await fetchPrimaryName(address, network);
}

const bnsNames = await fetchFromApi();
const names = [];

// Put the primary name first
if (primaryName) {
names.push(primaryName);
}

const bnsName = 'names' in bnsNames ? bnsNames.names[0] : null;
const names: string[] = [];
if (bnsName) names.push(bnsName);
// Add the rest of the names and filter out the primary name
names.push(...namesResponse.filter(name => name !== primaryName));

return { names };
}
Expand Down
Loading

0 comments on commit 2bf31d1

Please sign in to comment.