feat: include entity metadata in search endpoint responses #651

This commit is contained in:
M Hassan Tariq
2021-12-07 21:03:16 +05:00
committed by GitHub
parent 001e9a9077
commit f993e0d2ef
13 changed files with 1192 additions and 21 deletions

View File

@@ -24,6 +24,14 @@
"entity_type": {
"type": "string",
"enum": ["standard_address"]
},
"metadata": {
"type": "object",
"anyOf": [
{
"$ref": "../address/get-address-stx-balance.schema.json"
}
]
}
}
}

View File

@@ -49,6 +49,15 @@
"type": "integer"
}
}
},
"metadata": {
"type": "object",
"anyOf": [
{
"$ref": "../../entities/blocks/block.schema.json"
}
],
"additionalItems": false
}
}
}

View File

@@ -52,6 +52,18 @@
"description": "Corresponding tx_id for smart_contract"
}
}
},
"metadata": {
"type": "object",
"anyOf": [
{
"$ref": "../../entities/mempool-transactions/transaction.schema.json"
},
{
"$ref": "../../entities/transactions/transaction.schema.json"
}
],
"additionalItems": false
}
}
}

View File

@@ -35,6 +35,15 @@
"type": "string"
}
}
},
"metadata": {
"type": "object",
"anyOf": [
{
"$ref": "../../entities/mempool-transactions/transaction.schema.json"
}
],
"additionalItems": false
}
}
}

View File

@@ -49,6 +49,15 @@
"type": "string"
}
}
},
"metadata": {
"type": "object",
"anyOf": [
{
"$ref": "../../entities/transactions/transaction.schema.json"
}
],
"additionalItems": false
}
}
}

5
docs/generated.d.ts vendored
View File

@@ -2885,6 +2885,7 @@ export interface AddressSearchResult {
*/
entity_id: string;
entity_type: "standard_address";
metadata?: AddressStxBalanceResponse;
};
}
/**
@@ -2920,6 +2921,7 @@ export interface BlockSearchResult {
burn_block_time: number;
height: number;
};
metadata?: Block;
};
}
/**
@@ -2959,6 +2961,7 @@ export interface ContractSearchResult {
*/
tx_id?: string;
};
metadata?: MempoolTransaction | Transaction;
};
}
/**
@@ -3000,6 +3003,7 @@ export interface MempoolTxSearchResult {
tx_data: {
tx_type: string;
};
metadata?: MempoolTransaction;
};
}
/**
@@ -3035,6 +3039,7 @@ export interface TxSearchResult {
block_height: number;
tx_type: string;
};
metadata?: Transaction;
};
}
/**

View File

@@ -1677,6 +1677,11 @@ paths:
schema:
type: string
description: The hex hash string for a block or transaction, account address, or contract address
- in: query
name: include_metadata
schema:
type: boolean
description: This includes the detailed data for purticular hash in the response
operationId: search_by_id
responses:
200:

View File

@@ -57,6 +57,7 @@ import {
DbTxStatus,
DbTxTypeId,
DbSmartContract,
DbSearchResultWithMetadata,
} from '../../datastore/common';
import {
unwrapOptional,
@@ -1122,3 +1123,62 @@ export async function searchTx(
}
}
}
export async function searchHashWithMetadata(
hash: string,
db: DataStore
): Promise<FoundOrNot<DbSearchResultWithMetadata>> {
// checking for tx
const txQuery = await db.getTxListDetails({ txIds: [hash], includeUnanchored: true });
if (txQuery.length > 0) {
// tx found
const tx = txQuery[0];
return {
found: true,
result: {
entity_type: 'tx_id',
entity_id: tx.tx_id,
entity_data: tx,
},
};
}
// checking for mempool tx
const mempoolTxQuery = await db.getMempoolTxs({
txIds: [hash],
includeUnanchored: true,
includePruned: true,
});
if (mempoolTxQuery.length > 0) {
// mempool tx found
const mempoolTx = mempoolTxQuery[0];
return {
found: true,
result: {
entity_type: 'mempool_tx_id',
entity_id: mempoolTx.tx_id,
entity_data: mempoolTx,
},
};
}
// checking for block
const blockQuery = await db.getBlockWithMetadata({ hash }, { txs: true, microblocks: true });
if (blockQuery.found) {
// block found
const result = parseDbBlock(
blockQuery.result.block,
blockQuery.result.txs.map(tx => tx.tx_id),
blockQuery.result.microblocks.accepted.map(mb => mb.microblock_hash),
blockQuery.result.microblocks.streamed.map(mb => mb.microblock_hash)
);
return {
found: true,
result: {
entity_type: 'block_hash',
entity_id: result.hash,
entity_data: result,
},
};
}
// found nothing
return { found: false };
}

View File

@@ -9,17 +9,12 @@ function handleBadRequest(res: Response, next: NextFunction, errorMessage: strin
throw error;
}
/**
* Determines if the query parameters of a request are intended to include unanchored tx data.
* If an error is encountered while parsing the query param then a 400 response with an error message
* is sent and the function returns `void`.
*/
export function isUnanchoredRequest(
export function booleanValueForParam(
req: Request,
res: Response,
next: NextFunction
next: NextFunction,
paramName: string
): boolean | never {
const paramName = 'unanchored';
if (!(paramName in req.query)) {
return false;
}
@@ -31,7 +26,7 @@ export function isUnanchoredRequest(
case '1':
case 'yes':
case 'on':
// If specified without a value, e.g. `?unanchored&thing=1` then treat it as true
// If specified without a value, e.g. `?paramName` then treat it as true
case '':
return true;
case 'false':
@@ -48,6 +43,20 @@ export function isUnanchoredRequest(
);
}
/**
* Determines if the query parameters of a request are intended to include unanchored tx data.
* If an error is encountered while parsing the query param then a 400 response with an error message
* is sent and the function returns `void`.
*/
export function isUnanchoredRequest(
req: Request,
res: Response,
next: NextFunction
): boolean | never {
const paramName = 'unanchored';
return booleanValueForParam(req, res, next, paramName);
}
/**
* Determines if the query parameters of a request are intended to include data for a specific block height,
* or if the request intended to include unanchored tx data. If neither a block height parameter or an unanchored

View File

@@ -1,7 +1,14 @@
import * as express from 'express';
import { addAsync, RouterWithAsync } from '@awaitjs/express';
import { DataStore, DbBlock, DbTx, DbMempoolTx } from '../../datastore/common';
import { isValidPrincipal, has0xPrefix } from '../../helpers';
import {
DataStore,
DbBlock,
DbTx,
DbMempoolTx,
DbSearchResult,
DbSearchResultWithMetadata,
} from '../../datastore/common';
import { isValidPrincipal, has0xPrefix, FoundOrNot } from '../../helpers';
import {
Transaction,
Block,
@@ -12,9 +19,16 @@ import {
ContractSearchResult,
AddressSearchResult,
SearchErrorResult,
AddressStxBalanceResponse,
} from '@stacks/stacks-blockchain-api-types';
import { getTxTypeString } from '../controllers/db-controller';
import {
getTxTypeString,
parseDbMempoolTx,
parseDbTx,
searchHashWithMetadata,
} from '../controllers/db-controller';
import { address } from 'bitcoinjs-lib';
import { booleanValueForParam } from '../query-helpers';
const enum SearchResultType {
TxId = 'tx_id',
@@ -29,7 +43,7 @@ const enum SearchResultType {
export function createSearchRouter(db: DataStore): RouterWithAsync {
const router = addAsync(express.Router());
const performSearch = async (term: string): Promise<SearchResult> => {
const performSearch = async (term: string, includeMetadata: boolean): Promise<SearchResult> => {
// Check if term is a 32-byte hash, e.g.:
// `0x4ac9b89ec7f2a0ca3b4399888904f171d7bdf3460b1c63ea86c28a83c2feaad8`
// `4ac9b89ec7f2a0ca3b4399888904f171d7bdf3460b1c63ea86c28a83c2feaad8`
@@ -41,9 +55,35 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
}
if (hashBuffer !== undefined && hashBuffer.length === 32) {
const hash = '0x' + hashBuffer.toString('hex');
const queryResult = await db.searchHash({ hash });
let queryResult: FoundOrNot<DbSearchResult> | FoundOrNot<DbSearchResultWithMetadata> = {
found: false,
};
if (!includeMetadata) {
queryResult = await db.searchHash({ hash });
} else {
queryResult = await searchHashWithMetadata(hash, db);
}
if (queryResult.found) {
if (queryResult.result.entity_type === 'block_hash') {
if (queryResult.result.entity_type === 'block_hash' && queryResult.result.entity_data) {
if (includeMetadata) {
const blockData = queryResult.result.entity_data as Block;
const blockResult: BlockSearchResult = {
found: true,
result: {
entity_id: queryResult.result.entity_id,
entity_type: SearchResultType.BlockHash,
block_data: {
canonical: blockData.canonical,
hash: blockData.hash,
parent_block_hash: blockData.parent_block_hash,
burn_block_time: blockData.burn_block_time,
height: blockData.height,
},
metadata: blockData,
},
};
return blockResult;
}
const blockData = queryResult.result.entity_data as DbBlock;
const blockResult: BlockSearchResult = {
found: true,
@@ -76,6 +116,9 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
},
},
};
if (includeMetadata) {
txResult.result.metadata = parseDbTx(txData);
}
return txResult;
} else if (queryResult.result.entity_type === 'mempool_tx_id') {
const txData = queryResult.result.entity_data as DbMempoolTx;
@@ -89,6 +132,9 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
},
},
};
if (includeMetadata) {
txResult.result.metadata = parseDbMempoolTx(txData);
}
return txResult;
} else {
throw new Error(
@@ -139,6 +185,9 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
},
},
};
if (includeMetadata) {
contractResult.result.metadata = parseDbTx(txData);
}
return contractResult;
} else {
// Associated tx is a mempool tx
@@ -154,6 +203,9 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
},
},
};
if (includeMetadata) {
contractResult.result.metadata = parseDbMempoolTx(txData);
}
return contractResult;
}
} else if (entityType === SearchResultType.ContractAddress) {
@@ -175,6 +227,32 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
entity_type: entityType,
},
};
if (includeMetadata) {
const currentBlockHeight = await db.getCurrentBlockHeight();
if (!currentBlockHeight.found) {
throw new Error('No current block');
}
const blockHeight = currentBlockHeight.result + 1;
const stxBalanceResult = await db.getStxBalanceAtBlock(
principalResult.result.entity_id,
blockHeight
);
const result: AddressStxBalanceResponse = {
balance: stxBalanceResult.balance.toString(),
total_sent: stxBalanceResult.totalSent.toString(),
total_received: stxBalanceResult.totalReceived.toString(),
total_fees_sent: stxBalanceResult.totalFeesSent.toString(),
total_miner_rewards_received: stxBalanceResult.totalMinerRewardsReceived.toString(),
lock_tx_id: stxBalanceResult.lockTxId,
locked: stxBalanceResult.locked.toString(),
lock_height: stxBalanceResult.lockHeight,
burnchain_lock_height: stxBalanceResult.burnchainLockHeight,
burnchain_unlock_height: stxBalanceResult.burnchainUnlockHeight,
};
addrResult.result.metadata = result;
}
return addrResult;
} else {
return {
@@ -192,11 +270,11 @@ export function createSearchRouter(db: DataStore): RouterWithAsync {
};
};
router.getAsync('/:term', async (req, res) => {
router.getAsync('/:term', async (req, res, next) => {
const { term: rawTerm } = req.params;
const includeMetadata = booleanValueForParam(req, res, next, 'include_metadata');
const term = rawTerm.trim();
const searchResult = await performSearch(term);
const searchResult = await performSearch(term, includeMetadata);
if (!searchResult.found) {
res.status(404);
}

View File

@@ -14,10 +14,15 @@ import {
Transaction,
} from '../p2p/tx';
import { c32address } from 'c32check';
import { AddressTokenOfferingLocked, TransactionType } from '@stacks/stacks-blockchain-api-types';
import {
AddressTokenOfferingLocked,
MempoolTransaction,
TransactionType,
} from '@stacks/stacks-blockchain-api-types';
import { getTxSenderAddress } from '../event-stream/reader';
import { RawTxQueryResult } from './postgres-store';
import { ClarityAbi } from '@stacks/transactions';
import { Block } from '@stacks/stacks-blockchain-api-types';
export interface DbBlock {
block_hash: string;
@@ -398,6 +403,12 @@ export interface DbSearchResult {
entity_data?: DbBlock | DbMempoolTx | DbTx;
}
export interface DbSearchResultWithMetadata {
entity_type: 'standard_address' | 'contract_address' | 'block_hash' | 'tx_id' | 'mempool_tx_id';
entity_id: string;
entity_data?: Block | DbMempoolTx | DbTx;
}
export interface DbFtBalance {
balance: bigint;
totalSent: bigint;

View File

@@ -88,11 +88,13 @@ import {
DbNonFungibleTokenMetadata,
DbFungibleTokenMetadata,
DbTokenMetadataQueueEntry,
DbSearchResultWithMetadata,
} from './common';
import {
AddressTokenOfferingLocked,
TransactionType,
AddressUnlockSchedule,
Block,
} from '@stacks/stacks-blockchain-api-types';
import { getTxTypeId } from '../api/controllers/db-controller';
import { isProcessableTokenMetadata } from '../event-stream/tokens-contract-handler';

View File

@@ -47,7 +47,7 @@ import { PgDataStore, cycleMigrations, runMigrations } from '../datastore/postgr
import { PoolClient } from 'pg';
import { bufferToHexPrefixString, I32_MAX, microStxToStx, STACKS_DECIMAL_PLACES } from '../helpers';
import { FEE_RATE } from './../api/routes/fee-rate';
import { FeeRateRequest } from 'docs/generated';
import { Block, FeeRateRequest } from 'docs/generated';
describe('api tests', () => {
let db: PgDataStore;
@@ -1826,7 +1826,7 @@ describe('api tests', () => {
// test whitespace
const searchResult3 = await supertest(api.server).get(
`/extended/v1/search/ 1234000000000000000000000000000000000000000000000000000000000000 `
`/extended/v1/search/ 1234000000000000000000000000000000000000000000000000000000000000`
);
expect(searchResult3.status).toBe(200);
expect(searchResult3.type).toBe('application/json');
@@ -1912,6 +1912,315 @@ describe('api tests', () => {
expect(JSON.parse(searchResult7.text)).toEqual(expectedResp7);
});
test('search term - hash with metadata', async () => {
const block: DbBlock = {
block_hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
index_block_hash: '0xdeadbeef',
parent_index_block_hash: '0x00',
parent_block_hash: '0xff0011',
parent_microblock_hash: '',
parent_microblock_sequence: 0,
block_height: 1,
burn_block_time: 94869286,
burn_block_hash: '0x1234',
burn_block_height: 123,
miner_txid: '0x4321',
canonical: true,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
const tx: DbTx = {
tx_id: '0x4567000000000000000000000000000000000000000000000000000000000000',
tx_index: 4,
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.alloc(0),
index_block_hash: block.index_block_hash,
block_hash: block.block_hash,
block_height: 1,
burn_block_time: 2837565,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.Coinbase,
coinbase_payload: Buffer.from('coinbase hi'),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
parent_block_hash: '',
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
const mempoolTx: DbMempoolTx = {
pruned: false,
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.from('test-raw-tx'),
type_id: DbTxTypeId.Coinbase,
receipt_time: 123456,
coinbase_payload: Buffer.from('coinbase hi'),
status: 1,
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: 'sender-addr',
origin_hash_mode: 1,
};
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
const dataStoreUpdate: DataStoreBlockUpdateData = {
block: block,
microblocks: [],
minerRewards: [],
txs: [
{
tx: tx,
stxEvents: [],
stxLockEvents: [],
ftEvents: [],
nftEvents: [],
contractLogEvents: [],
smartContracts: [],
names: [],
namespaces: [],
},
],
};
await db.update(dataStoreUpdate);
const blockMetadata = {
burn_block_hash: '0x1234',
burn_block_height: 123,
burn_block_time: 94869286,
burn_block_time_iso: '1973-01-03T00:34:46.000Z',
canonical: true,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
height: 1,
microblocks_accepted: [],
microblocks_streamed: [],
miner_txid: '0x4321',
parent_block_hash: '0xff0011',
parent_microblock_hash: '',
parent_microblock_sequence: 0,
txs: ['0x4567000000000000000000000000000000000000000000000000000000000000'],
};
const searchResult1 = await supertest(api.server).get(
`/extended/v1/search/0x1234000000000000000000000000000000000000000000000000000000000000?include_metadata=true`
);
expect(searchResult1.status).toBe(200);
expect(searchResult1.type).toBe('application/json');
const expectedResp1 = {
found: true,
result: {
entity_id: '0x1234000000000000000000000000000000000000000000000000000000000000',
entity_type: 'block_hash',
block_data: {
canonical: true,
hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
parent_block_hash: '0xff0011',
burn_block_time: 94869286,
height: 1,
},
metadata: blockMetadata,
},
};
expect(JSON.parse(searchResult1.text)).toEqual(expectedResp1);
// test without 0x-prefix
const searchResult2 = await supertest(api.server).get(
`/extended/v1/search/1234000000000000000000000000000000000000000000000000000000000000?include_metadata=true`
);
expect(searchResult2.status).toBe(200);
expect(searchResult2.type).toBe('application/json');
const expectedResp2 = {
found: true,
result: {
entity_id: '0x1234000000000000000000000000000000000000000000000000000000000000',
entity_type: 'block_hash',
block_data: {
canonical: true,
hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
parent_block_hash: '0xff0011',
burn_block_time: 94869286,
height: 1,
},
metadata: blockMetadata,
},
};
expect(JSON.parse(searchResult2.text)).toEqual(expectedResp2);
// test whitespace
const searchResult3 = await supertest(api.server).get(
`/extended/v1/search/ 1234000000000000000000000000000000000000000000000000000000000000?include_metadata=true`
);
expect(searchResult3.status).toBe(200);
expect(searchResult3.type).toBe('application/json');
const expectedResp3 = {
found: true,
result: {
entity_id: '0x1234000000000000000000000000000000000000000000000000000000000000',
entity_type: 'block_hash',
block_data: {
canonical: true,
hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
parent_block_hash: '0xff0011',
burn_block_time: 94869286,
height: 1,
},
metadata: blockMetadata,
},
};
expect(JSON.parse(searchResult3.text)).toEqual(expectedResp3);
// test mempool tx search
const searchResult4 = await supertest(api.server).get(
`/extended/v1/search/0x8912000000000000000000000000000000000000000000000000000000000000?include_metadata=1`
);
expect(searchResult4.status).toBe(200);
expect(searchResult4.type).toBe('application/json');
const expectedResp4 = {
found: true,
result: {
entity_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
entity_type: 'mempool_tx_id',
tx_data: { tx_type: 'coinbase' },
metadata: {
anchor_mode: 'any',
coinbase_payload: {
data: '0x636f696e62617365206869',
},
fee_rate: '1234',
nonce: 0,
post_condition_mode: 'allow',
post_conditions: [],
receipt_time: 123456,
receipt_time_iso: '1970-01-02T10:17:36.000Z',
sender_address: 'sender-addr',
sponsored: false,
tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
tx_status: 'success',
tx_type: 'coinbase',
},
},
};
expect(JSON.parse(searchResult4.text)).toEqual(expectedResp4);
// test hash not found
const searchResult5 = await supertest(api.server).get(
`/extended/v1/search/0x1111000000000000000000000000000000000000000000000000000000000000?include_metadata=on`
);
expect(searchResult5.status).toBe(404);
expect(searchResult5.type).toBe('application/json');
const expectedResp6 = {
found: false,
result: { entity_type: 'unknown_hash' },
error:
'No block or transaction found with hash "0x1111000000000000000000000000000000000000000000000000000000000000"',
};
expect(JSON.parse(searchResult5.text)).toEqual(expectedResp6);
// test invalid hash hex
const invalidHex = '0x1111w00000000000000000000000000000000000000000000000000000000000';
const searchResult6 = await supertest(api.server).get(
`/extended/v1/search/${invalidHex}?include_metadata`
);
expect(searchResult6.status).toBe(404);
expect(searchResult6.type).toBe('application/json');
const expectedResp7 = {
found: false,
result: { entity_type: 'invalid_term' },
error:
'The term "0x1111w00000000000000000000000000000000000000000000000000000000000" is not a valid block hash, transaction ID, contract principal, or account address principal',
};
expect(JSON.parse(searchResult6.text)).toEqual(expectedResp7);
// test tx search
const searchResult8 = await supertest(api.server).get(
`/extended/v1/search/0x4567000000000000000000000000000000000000000000000000000000000000?include_metadata`
);
expect(searchResult8.status).toBe(200);
expect(searchResult8.type).toBe('application/json');
const expectedResp8 = {
found: true,
result: {
entity_id: '0x4567000000000000000000000000000000000000000000000000000000000000',
entity_type: 'tx_id',
tx_data: {
canonical: true,
block_hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
burn_block_time: 2837565,
block_height: 1,
tx_type: 'coinbase',
},
metadata: {
tx_id: '0x4567000000000000000000000000000000000000000000000000000000000000',
nonce: 0,
fee_rate: '1234',
sender_address: 'sender-addr',
sponsored: false,
post_condition_mode: 'allow',
post_conditions: [],
anchor_mode: 'any',
is_unanchored: false,
block_hash: '0x1234000000000000000000000000000000000000000000000000000000000000',
parent_block_hash: '',
block_height: 1,
burn_block_time: 2837565,
burn_block_time_iso: '1970-02-02T20:12:45.000Z',
parent_burn_block_time: 1626122935,
parent_burn_block_time_iso: '2021-07-12T20:48:55.000Z',
canonical: true,
tx_index: 4,
tx_status: 'success',
tx_result: {
hex: '0x0100000000000000000000000000000001',
repr: 'u1',
},
microblock_hash: '',
microblock_sequence: 2147483647,
microblock_canonical: true,
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
tx_type: 'coinbase',
coinbase_payload: {
data: '0x636f696e62617365206869',
},
},
},
};
expect(JSON.parse(searchResult8.text)).toEqual(expectedResp8);
});
test('search term - principal', async () => {
const addr1 = 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1';
const addr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4';
@@ -1930,6 +2239,27 @@ describe('api tests', () => {
const contractAddr2 = 'STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.contract-name';
const contractAddr3 = 'STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.test-contract';
const block: DbBlock = {
block_hash: '0x1234',
index_block_hash: '0x1234',
parent_index_block_hash: '0x2345',
parent_block_hash: '0x5678',
parent_microblock_hash: '',
parent_microblock_sequence: 0,
block_height: 100123123,
burn_block_time: 39486,
burn_block_hash: '0x1234',
burn_block_height: 100123123,
miner_txid: '0x4321',
canonical: true,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
await db.updateBlock(client, block);
const stxTx1: DbTx = {
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 0,
@@ -2071,6 +2401,7 @@ describe('api tests', () => {
recipient: 'none',
sender: addr4,
};
await db.updateStxEvent(client, stxTx1, stxEvent2);
// test address as a stx event sender
@@ -2330,6 +2661,629 @@ describe('api tests', () => {
expect(JSON.parse(searchResult13.text)).toEqual(expectedResp13);
});
test('search term - principal with metadata', async () => {
const addr1 = 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1';
const addr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4';
const addr3 = 'ST37VASHEJRMFRS91GWK1HZZKKEYQTEP85ARXCQPH';
const addr4 = 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C';
const addr5 = 'ST3YKTGBCY1BNKN6J18A3QKAX7CE36SZH3A5XN9ZQ';
const addr6 = 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR';
const addr7 = 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G';
const addr8 = 'ST3AMFNNS7KBQ28ECMJMN2G3AGJ37SSA2HSY82CMH';
const addr9 = 'STAR26VJ4BC24SMNKRY533MAM0K3JA5ZJDVBD45A';
const contractAddr1 = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world';
const contractAddr2 = 'STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.contract-name';
const contractAddr3 = 'STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.test-contract';
const block: DbBlock = {
block_hash: '0x1234',
index_block_hash: '0x1234',
parent_index_block_hash: '0x2345',
parent_block_hash: '0x5678',
parent_microblock_hash: '',
parent_microblock_sequence: 0,
block_height: 1,
burn_block_time: 39486,
burn_block_hash: '0x1234',
burn_block_height: 100123123,
miner_txid: '0x4321',
canonical: true,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
const stxTx1: DbTx = {
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 0,
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.alloc(0),
index_block_hash: '0x5432',
block_hash: block.block_hash,
block_height: block.block_height,
burn_block_time: 2837565,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: Buffer.from('hi'),
token_transfer_recipient_address: 'none',
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
parent_block_hash: '',
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: addr1,
origin_hash_mode: 1,
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
const stxTx2: DbTx = {
tx_id: '0x2222000000000000000000000000000000000000000000000000000000000000',
tx_index: 0,
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.alloc(0),
index_block_hash: '0x5432',
block_hash: block.block_hash,
block_height: block.block_height,
burn_block_time: 2837565,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.TokenTransfer,
token_transfer_amount: 1n,
token_transfer_memo: Buffer.from('hi'),
token_transfer_recipient_address: addr2,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
parent_block_hash: '',
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: 'none',
origin_hash_mode: 1,
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
const stxEvent1: DbStxEvent = {
canonical: true,
event_type: DbEventTypeId.StxAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 1,
block_height: block.block_height,
amount: 1n,
recipient: addr3,
sender: 'none',
};
const stxEvent2: DbStxEvent = {
canonical: true,
event_type: DbEventTypeId.StxAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 1,
block_height: 1,
amount: 1n,
recipient: 'none',
sender: addr4,
};
const ftEvent1: DbFtEvent = {
canonical: true,
event_type: DbEventTypeId.FungibleTokenAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 1,
block_height: block.block_height,
asset_identifier: 'some-asset',
amount: 1n,
recipient: addr5,
sender: 'none',
};
const ftEvent2: DbFtEvent = {
canonical: true,
event_type: DbEventTypeId.FungibleTokenAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 1,
block_height: block.block_height,
asset_identifier: 'some-asset',
amount: 1n,
recipient: 'none',
sender: addr6,
};
const nftEvent1: DbNftEvent = {
canonical: true,
event_type: DbEventTypeId.NonFungibleTokenAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 1,
block_height: block.block_height,
asset_identifier: 'some-asset',
value: serializeCV(intCV(0)),
recipient: addr7,
sender: 'none',
};
const nftEvent2: DbNftEvent = {
canonical: true,
event_type: DbEventTypeId.NonFungibleTokenAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: '0x1111000000000000000000000000000000000000000000000000000000000000',
tx_index: 1,
block_height: block.block_height,
asset_identifier: 'some-asset',
value: serializeCV(intCV(0)),
recipient: 'none',
sender: addr8,
};
const smartContractTx: DbTx = {
type_id: DbTxTypeId.SmartContract,
tx_id: '0x1111880000000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.alloc(0),
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
parent_block_hash: '',
smart_contract_contract_id: contractAddr1,
smart_contract_source_code: '(some-src)',
block_height: 1,
tx_index: 0,
index_block_hash: block.index_block_hash,
block_hash: block.block_hash,
burn_block_time: 2837565,
parent_burn_block_time: 1626122935,
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: 'none',
origin_hash_mode: 1,
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
};
const smartContract: DbSmartContract = {
tx_id: '0x421234',
canonical: true,
block_height: block.block_height,
contract_id: contractAddr1,
source_code: '(some-src)',
abi: '{"some-abi":1}',
};
const dataStoreUpdate: DataStoreBlockUpdateData = {
block: block,
microblocks: [],
minerRewards: [],
txs: [
{
tx: stxTx1,
stxEvents: [stxEvent1, stxEvent2],
stxLockEvents: [],
ftEvents: [ftEvent1, ftEvent2],
nftEvents: [nftEvent1, nftEvent2],
contractLogEvents: [],
smartContracts: [],
names: [],
namespaces: [],
},
{
tx: stxTx2,
stxEvents: [],
stxLockEvents: [],
ftEvents: [],
nftEvents: [],
contractLogEvents: [],
smartContracts: [],
names: [],
namespaces: [],
},
{
tx: smartContractTx,
stxEvents: [],
stxLockEvents: [],
ftEvents: [],
nftEvents: [],
contractLogEvents: [],
smartContracts: [smartContract],
names: [],
namespaces: [],
},
],
};
await db.update(dataStoreUpdate);
// test address as a tx sender
const searchResult1 = await supertest(api.server).get(
`/extended/v1/search/${addr1}?include_metadata`
);
expect(searchResult1.status).toBe(200);
expect(searchResult1.type).toBe('application/json');
const expectedResp1 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr1,
metadata: {
balance: '-1234',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
lock_height: 0,
lock_tx_id: '',
locked: '0',
total_fees_sent: '1234',
total_miner_rewards_received: '0',
total_received: '0',
total_sent: '0',
},
},
};
expect(JSON.parse(searchResult1.text)).toEqual(expectedResp1);
// test address as a stx tx recipient
const searchResult2 = await supertest(api.server).get(
`/extended/v1/search/${addr2}?include_metadata`
);
expect(searchResult2.status).toBe(200);
expect(searchResult2.type).toBe('application/json');
const expectedResp2 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr2,
metadata: {
balance: '0',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
lock_height: 0,
lock_tx_id: '',
locked: '0',
total_fees_sent: '0',
total_miner_rewards_received: '0',
total_received: '0',
total_sent: '0',
},
},
};
expect(JSON.parse(searchResult2.text)).toEqual(expectedResp2);
// test address as a stx event recipient
const searchResult3 = await supertest(api.server).get(
`/extended/v1/search/${addr3}?include_metadata`
);
expect(searchResult3.status).toBe(200);
expect(searchResult3.type).toBe('application/json');
const expectedResp3 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr3,
metadata: {
balance: '1',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
lock_height: 0,
lock_tx_id: '',
locked: '0',
total_fees_sent: '0',
total_miner_rewards_received: '0',
total_received: '1',
total_sent: '0',
},
},
};
expect(JSON.parse(searchResult3.text)).toEqual(expectedResp3);
// test address as a stx event sender
const searchResult4 = await supertest(api.server).get(
`/extended/v1/search/${addr4}?include_metadata=true`
);
expect(searchResult4.status).toBe(200);
expect(searchResult4.type).toBe('application/json');
const expectedResp4 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr4,
metadata: {
balance: '-1',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
lock_height: 0,
lock_tx_id: '',
locked: '0',
total_fees_sent: '0',
total_miner_rewards_received: '0',
total_received: '0',
total_sent: '1',
},
},
};
expect(JSON.parse(searchResult4.text)).toEqual(expectedResp4);
// test address as a ft event recipient
const searchResult5 = await supertest(api.server).get(
`/extended/v1/search/${addr5}?include_metadata`
);
expect(searchResult5.status).toBe(200);
expect(searchResult5.type).toBe('application/json');
const emptyStandardAddressMetadata = {
balance: '0',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
lock_height: 0,
lock_tx_id: '',
locked: '0',
total_fees_sent: '0',
total_miner_rewards_received: '0',
total_received: '0',
total_sent: '0',
};
const expectedResp5 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr5,
metadata: emptyStandardAddressMetadata,
},
};
expect(JSON.parse(searchResult5.text)).toEqual(expectedResp5);
// test address as a ft event sender
const searchResult6 = await supertest(api.server).get(
`/extended/v1/search/${addr6}?include_metadata`
);
expect(searchResult6.status).toBe(200);
expect(searchResult6.type).toBe('application/json');
const expectedResp6 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr6,
metadata: emptyStandardAddressMetadata,
},
};
expect(JSON.parse(searchResult6.text)).toEqual(expectedResp6);
// test address as a nft event recipient
const searchResult7 = await supertest(api.server).get(
`/extended/v1/search/${addr7}?include_metadata`
);
expect(searchResult7.status).toBe(200);
expect(searchResult7.type).toBe('application/json');
const expectedResp7 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr7,
metadata: emptyStandardAddressMetadata,
},
};
expect(JSON.parse(searchResult7.text)).toEqual(expectedResp7);
// test address as a nft event sender
const searchResult8 = await supertest(api.server).get(
`/extended/v1/search/${addr8}?include_metadata`
);
expect(searchResult8.status).toBe(200);
expect(searchResult8.type).toBe('application/json');
const expectedResp8 = {
found: true,
result: {
entity_type: 'standard_address',
entity_id: addr8,
metadata: emptyStandardAddressMetadata,
},
};
expect(JSON.parse(searchResult8.text)).toEqual(expectedResp8);
// test contract address
const searchResult9 = await supertest(api.server).get(
`/extended/v1/search/${contractAddr1}?include_metadata=true`
);
expect(searchResult9.status).toBe(200);
expect(searchResult9.type).toBe('application/json');
const expectedResp9 = {
found: true,
result: {
entity_id: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
entity_type: 'contract_address',
tx_data: {
canonical: true,
block_hash: '0x1234',
burn_block_time: 2837565,
block_height: 1,
tx_type: 'smart_contract',
tx_id: '0x1111880000000000000000000000000000000000000000000000000000000000',
},
metadata: {
anchor_mode: 'any',
block_hash: '0x1234',
block_height: 1,
burn_block_time: 2837565,
burn_block_time_iso: '1970-02-02T20:12:45.000Z',
canonical: true,
event_count: 0,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
fee_rate: '1234',
is_unanchored: false,
microblock_canonical: true,
microblock_hash: '',
microblock_sequence: 2147483647,
nonce: 0,
parent_block_hash: '',
parent_burn_block_time: 1626122935,
parent_burn_block_time_iso: '2021-07-12T20:48:55.000Z',
post_condition_mode: 'allow',
post_conditions: [],
sender_address: 'none',
smart_contract: {
contract_id: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
source_code: '(some-src)',
},
sponsored: false,
tx_id: '0x1111880000000000000000000000000000000000000000000000000000000000',
tx_index: 0,
tx_result: {
hex: '0x0100000000000000000000000000000001',
repr: 'u1',
},
tx_status: 'success',
tx_type: 'smart_contract',
},
},
};
expect(JSON.parse(searchResult9.text)).toEqual(expectedResp9);
const smartContractMempoolTx: DbMempoolTx = {
pruned: false,
type_id: DbTxTypeId.SmartContract,
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.from('test-raw-tx'),
receipt_time: 123456,
smart_contract_contract_id: contractAddr2,
smart_contract_source_code: '(some-src)',
status: 1,
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 1234n,
sponsored: false,
sponsor_address: undefined,
sender_address: 'none',
origin_hash_mode: 1,
};
await db.updateMempoolTxs({ mempoolTxs: [smartContractMempoolTx] });
// test contract address associated with mempool tx
const searchResult10 = await supertest(api.server).get(
`/extended/v1/search/${contractAddr2}?include_metadata`
);
expect(searchResult10.status).toBe(200);
expect(searchResult10.type).toBe('application/json');
const expectedResp10 = {
found: true,
result: {
entity_id: 'STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.contract-name',
entity_type: 'contract_address',
tx_data: {
tx_type: 'smart_contract',
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
},
metadata: {
anchor_mode: 'any',
fee_rate: '1234',
nonce: 0,
post_condition_mode: 'allow',
post_conditions: [],
receipt_time: 123456,
receipt_time_iso: '1970-01-02T10:17:36.000Z',
sender_address: 'none',
smart_contract: {
contract_id: 'STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.contract-name',
source_code: '(some-src)',
},
sponsored: false,
tx_id: '0x1111882200000000000000000000000000000000000000000000000000000000',
tx_status: 'success',
tx_type: 'smart_contract',
},
},
};
expect(JSON.parse(searchResult10.text)).toEqual(expectedResp10);
// test contract address not found
const searchResult11 = await supertest(api.server).get(
`/extended/v1/search/${contractAddr3}?include_metadata`
);
expect(searchResult11.status).toBe(404);
expect(searchResult11.type).toBe('application/json');
const expectedResp11 = {
found: false,
result: { entity_type: 'contract_address' },
error:
'No principal found with address "STSPS4JYDEYCPPCSHE3MM2NCEGR07KPBETNEZCBQ.test-contract"',
};
expect(JSON.parse(searchResult11.text)).toEqual(expectedResp11);
// test standard address not found
const searchResult12 = await supertest(api.server).get(
`/extended/v1/search/${addr9}?include_metadata`
);
expect(searchResult12.status).toBe(404);
expect(searchResult12.type).toBe('application/json');
const expectedResp12 = {
found: false,
result: { entity_type: 'standard_address' },
error: 'No principal found with address "STAR26VJ4BC24SMNKRY533MAM0K3JA5ZJDVBD45A"',
};
expect(JSON.parse(searchResult12.text)).toEqual(expectedResp12);
// test invalid term
const invalidTerm = 'bogus123';
const searchResult13 = await supertest(api.server).get(
`/extended/v1/search/${invalidTerm}?include_metadata`
);
expect(searchResult13.status).toBe(404);
expect(searchResult13.type).toBe('application/json');
const expectedResp13 = {
found: false,
result: { entity_type: 'invalid_term' },
error:
'The term "bogus123" is not a valid block hash, transaction ID, contract principal, or account address principal',
};
expect(JSON.parse(searchResult13.text)).toEqual(expectedResp13);
});
test('address transaction transfers', async () => {
const testAddr1 = 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1';
const testAddr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4';