feat: add at_block query param for /address endpoints

* feat: added block limit to all the address endpoints

* refactor: fixed typo and types issue

* docs: update docs for at_block params

* refactor: change at_block param to until block

* refactor: refactor openapi docs

* style: fixed style issue

* fix: fixed rebase issue

* fix: add where clause for address tx

* test: add unit test for atBlock

* chore: bump for codecov.io runner fix

Co-authored-by: Matthew Little <zone117x@gmail.com>
This commit is contained in:
Asim Mehmood
2021-11-30 21:18:51 +05:00
committed by GitHub
parent ab9b3de70a
commit 9f206a3d74
10 changed files with 439 additions and 207 deletions

View File

@@ -1034,6 +1034,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state up until that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success
@@ -1064,6 +1070,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state up until that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success
@@ -1081,6 +1093,12 @@ paths:
- Accounts
operationId: get_account_transactions
parameters:
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: limit
in: query
description: max number of account transactions to fetch
@@ -1093,12 +1111,6 @@ paths:
required: false
schema:
type: integer
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: height
in: query
description: Filter for transactions only at this given block height
@@ -1112,6 +1124,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state up until that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success
@@ -1164,6 +1182,12 @@ paths:
- Accounts
operationId: get_account_transactions_with_transfers
parameters:
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: limit
in: query
description: max number of account transactions to fetch
@@ -1176,12 +1200,6 @@ paths:
required: false
schema:
type: integer
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: height
in: query
description: Filter for transactions only at this given block height
@@ -1195,6 +1213,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state up until that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success
@@ -1235,6 +1259,12 @@ paths:
- Accounts
operationId: get_account_assets
parameters:
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: limit
in: query
description: max number of account assets to fetch
@@ -1247,12 +1277,6 @@ paths:
required: false
schema:
type: integer
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: unanchored
in: query
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
@@ -1260,6 +1284,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state at that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success
@@ -1280,6 +1310,12 @@ paths:
- Accounts
operationId: get_account_inbound
parameters:
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: limit
in: query
description: number of items to return
@@ -1292,12 +1328,6 @@ paths:
required: false
schema:
type: integer
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: height
in: query
description: Filter for transfers only at this given block height
@@ -1311,6 +1341,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state up until that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success
@@ -1330,6 +1366,12 @@ paths:
- Accounts
operationId: get_account_nft
parameters:
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: limit
in: query
description: number of items to return
@@ -1342,12 +1384,6 @@ paths:
required: false
schema:
type: integer
- name: principal
in: path
description: Stacks address or a Contract identifier (e.g. `SP31DA6FTSJX2WGTZ69SFY11BH51NZMB0ZW97B5P0.get-info`)
required: true
schema:
type: string
- name: unanchored
in: query
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
@@ -1355,6 +1391,12 @@ paths:
schema:
type: boolean
default: false
- name: until_block
in: query
description: returned data representing the state up until that point in time, rather than the current block.
required: false
schema:
type: string
responses:
200:
description: Success

View File

@@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express';
import { has0xPrefix } from './../helpers';
function handleBadRequest(res: Response, next: NextFunction, errorMessage: string): never {
const error = new Error(errorMessage);
@@ -134,3 +135,44 @@ export function getBlockHeightPathParam(
}
return height;
}
/**
* Determine if until_block query parameter exists or is an integer or string or if it is a valid height
* if it is a string with "0x" prefix consider it a block_hash if it is integer consider it block_height
* If type is not string or block_height is not valid or it also has mutually exclusive "unanchored" property a 400 bad requst is send and function throws.
* @returns `undefined` if param does not exist || block_height if number || block_hash if string || never if error
*/
export function parseUntilBlockQuery(
req: Request,
res: Response,
next: NextFunction
): undefined | number | string | never {
const untilBlock = req.query.until_block;
if (!untilBlock) return;
if (typeof untilBlock === 'string') {
//if mutually exclusive unachored is also specified, throw bad request error
if (isUnanchoredRequest(req, res, next)) {
handleBadRequest(
res,
next,
`can't handle both 'unanchored' and 'until_block' in the same request `
);
}
if (has0xPrefix(untilBlock)) {
//case for block_hash
return untilBlock;
} else {
//parse int to check if it is a block_height
const block_height = Number.parseInt(untilBlock, 10);
if (isNaN(block_height) || block_height < 1) {
handleBadRequest(
res,
next,
`Unexpected integer value for block height path parameter: ${block_height}`
);
}
return block_height;
}
}
handleBadRequest(res, next, 'until_block must be either `string` or `number`');
}

View File

@@ -3,7 +3,7 @@ import { addAsync, RouterWithAsync } from '@awaitjs/express';
import * as Bluebird from 'bluebird';
import { DataStore } from '../../datastore/common';
import { parseLimitQuery, parsePagingQueryInput } from '../pagination';
import { isUnanchoredRequest, getBlockParams } from '../query-helpers';
import { isUnanchoredRequest, getBlockParams, parseUntilBlockQuery } from '../query-helpers';
import {
bufferToHexPrefixString,
formatMapToObject,
@@ -30,6 +30,7 @@ import {
} from '@stacks/stacks-blockchain-api-types';
import { ChainID, cvToString, deserializeCV } from '@stacks/transactions';
import { validate } from '../validate';
import { NextFunction, Request, Response } from 'express';
const MAX_TX_PER_REQUEST = 50;
const MAX_ASSETS_PER_REQUEST = 50;
@@ -50,6 +51,41 @@ const parseStxInboundLimit = parseLimitQuery({
errorMsg: '`limit` must be equal to or less than ' + MAX_STX_INBOUND_PER_REQUEST,
});
async function getBlockHeight(
untilBlock: number | string | undefined,
req: Request,
res: Response,
next: NextFunction,
db: DataStore
): Promise<number> {
let blockHeight = 0;
if (typeof untilBlock === 'number') {
blockHeight = untilBlock;
} else if (typeof untilBlock === 'string') {
const block = await db.getBlock({ hash: untilBlock });
if (!block.found) {
const error = `block not found with hash ${untilBlock}`;
res.status(404).json({ error: error });
next(error);
throw new Error(error);
}
blockHeight = block.result.block_height;
} else {
const includeUnanchored = isUnanchoredRequest(req, res, next);
const currentBlockHeight = await db.getCurrentBlockHeight();
if (!currentBlockHeight.found) {
const error = `no current block`;
res.status(404).json({ error: error });
next(error);
throw new Error(error);
}
blockHeight = currentBlockHeight.result + (includeUnanchored ? 1 : 0);
}
return blockHeight;
}
interface AddressAssetEvents {
results: TransactionEvent[];
limit: number;
@@ -65,15 +101,11 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
if (!isValidPrincipal(stxAddress)) {
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
// Get balance info for STX token
const includeUnanchored = isUnanchoredRequest(req, res, next);
const currentBlockHeight = await db.getCurrentBlockHeight();
if (!currentBlockHeight.found) {
return res.status(500).json({ error: `no current block` });
}
const blockHeight = currentBlockHeight.result + (includeUnanchored ? 1 : 0);
const stxBalanceResult = await db.getStxBalanceAtBlock(stxAddress, blockHeight);
const tokenOfferingLocked = await db.getTokenOfferingLocked(stxAddress, blockHeight);
const result: AddressStxBalanceResponse = {
@@ -102,20 +134,18 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
const includeUnanchored = isUnanchoredRequest(req, res, next);
const currentBlockHeight = await db.getCurrentBlockHeight();
if (!currentBlockHeight.found) {
return res.status(500).json({ error: `no current block` });
}
const blockHeight = currentBlockHeight.result + (includeUnanchored ? 1 : 0);
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
// Get balance info for STX token
const stxBalanceResult = await db.getStxBalanceAtBlock(stxAddress, blockHeight);
const tokenOfferingLocked = await db.getTokenOfferingLocked(stxAddress, blockHeight);
// Get balances for fungible tokens
const ftBalancesResult = await db.getFungibleTokenBalances({ stxAddress, includeUnanchored });
const ftBalancesResult = await db.getFungibleTokenBalances({
stxAddress,
untilBlock: blockHeight,
});
const ftBalances = formatMapToObject(ftBalancesResult, val => {
return {
balance: val.balance.toString(),
@@ -125,7 +155,10 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
});
// Get counts for non-fungible tokens
const nftBalancesResult = await db.getNonFungibleTokenCounts({ stxAddress, includeUnanchored });
const nftBalancesResult = await db.getNonFungibleTokenCounts({
stxAddress,
untilBlock: blockHeight,
});
const nftBalances = formatMapToObject(nftBalancesResult, val => {
return {
count: val.count.toString(),
@@ -165,14 +198,29 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockParams = getBlockParams(req, res, next);
let atSingleBlock = false;
let blockHeight = 0;
if (blockParams.blockHeight) {
if (untilBlock) {
return res
.status(400)
.json({ error: `can't handle until_block and block_height in the same request` });
}
atSingleBlock = true;
blockHeight = blockParams.blockHeight;
} else {
blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
}
const limit = parseTxQueryLimit(req.query.limit ?? 20);
const offset = parsePagingQueryInput(req.query.offset ?? 0);
const { results: txResults, total } = await db.getAddressTxs({
stxAddress: stxAddress,
limit,
offset,
...blockParams,
blockHeight,
atSingleBlock,
});
// TODO: use getBlockWithMetadata or similar to avoid transaction integrity issues from lazy resolving block tx data (primarily the contract-call ABI data)
const results = await Bluebird.mapSeries(txResults, async tx => {
@@ -229,14 +277,29 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockParams = getBlockParams(req, res, next);
let atSingleBlock = false;
let blockHeight = 0;
if (blockParams.blockHeight) {
if (untilBlock) {
return res
.status(400)
.json({ error: `can't handle until_block and block_height in the same request` });
}
atSingleBlock = true;
blockHeight = blockParams.blockHeight;
} else {
blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
}
const limit = parseTxQueryLimit(req.query.limit ?? 20);
const offset = parsePagingQueryInput(req.query.offset ?? 0);
const { results: txResults, total } = await db.getAddressTxsWithAssetTransfers({
stxAddress: stxAddress,
limit,
offset,
...blockParams,
blockHeight,
atSingleBlock,
});
// TODO: use getBlockWithMetadata or similar to avoid transaction integrity issues from lazy resolving block tx data (primarily the contract-call ABI data)
@@ -289,14 +352,16 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
if (!isValidPrincipal(stxAddress)) {
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
const includeUnanchored = isUnanchoredRequest(req, res, next);
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
const limit = parseAssetsQueryLimit(req.query.limit ?? 20);
const offset = parsePagingQueryInput(req.query.offset ?? 0);
const { results: assetEvents, total } = await db.getAddressAssetEvents({
stxAddress,
limit,
offset,
includeUnanchored,
blockHeight,
});
const results = assetEvents.map(event => parseDbEvent(event));
const response: AddressAssetEvents = { limit, offset, total, results };
@@ -315,15 +380,32 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
if (!isValidPrincipal(stxAddress)) {
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
let atSingleBlock = false;
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockParams = getBlockParams(req, res, next);
let blockHeight = 0;
if (blockParams.blockHeight) {
if (untilBlock) {
return res
.status(400)
.json({ error: `can't handle until_block and block_height in the same request` });
}
atSingleBlock = true;
blockHeight = blockParams.blockHeight;
} else {
blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
}
const limit = parseStxInboundLimit(req.query.limit ?? 20);
const offset = parsePagingQueryInput(req.query.offset ?? 0);
const blockParams = getBlockParams(req, res, next);
const { results, total } = await db.getInboundTransfers({
stxAddress,
limit,
offset,
sendManyContractId,
...blockParams,
blockHeight,
atSingleBlock,
});
const transfers: InboundStxTransfer[] = results.map(r => ({
sender: r.sender,
@@ -354,14 +436,17 @@ export function createAddressRouter(db: DataStore, chainId: ChainID): RouterWith
return res.status(400).json({ error: `invalid STX address "${stxAddress}"` });
}
const untilBlock = parseUntilBlockQuery(req, res, next);
const blockHeight = await getBlockHeight(untilBlock, req, res, next, db);
const limit = parseAssetsQueryLimit(req.query.limit ?? 20);
const offset = parsePagingQueryInput(req.query.offset ?? 0);
const includeUnanchored = isUnanchoredRequest(req, res, next);
const response = await db.getAddressNFTEvent({
stxAddress,
limit,
offset,
blockHeight,
includeUnanchored,
});
const nft_events = response.results.map(row => ({

View File

@@ -158,6 +158,7 @@ export function createSocketIORouter(db: DataStore, server: http.Server) {
const dbTxsQuery = await db.getAddressTxsWithAssetTransfers({
stxAddress: address,
blockHeight: blockHeight,
atSingleBlock: true,
});
if (dbTxsQuery.total == 0) {
return;

View File

@@ -401,6 +401,7 @@ export function createWsRpcRouter(db: DataStore, server: http.Server): WebSocket
const dbTxsQuery = await db.getAddressTxsWithAssetTransfers({
stxAddress: address,
blockHeight: blockHeight,
atSingleBlock: true,
});
if (dbTxsQuery.total == 0) {
return;

View File

@@ -701,11 +701,11 @@ export interface DataStore extends DataStoreEventEmitter {
getStxBalanceAtBlock(stxAddress: string, blockHeight: number): Promise<DbStxBalance>;
getFungibleTokenBalances(args: {
stxAddress: string;
includeUnanchored: boolean;
untilBlock: number;
}): Promise<Map<string, DbFtBalance>>;
getNonFungibleTokenCounts(args: {
stxAddress: string;
includeUnanchored: boolean;
untilBlock: number;
}): Promise<Map<string, { count: bigint; totalSent: bigint; totalReceived: bigint }>>;
getUnlockedStxSupply(
@@ -720,21 +720,21 @@ export interface DataStore extends DataStoreEventEmitter {
getSTXFaucetRequests(address: string): Promise<{ results: DbFaucetRequest[] }>;
getAddressTxs(
args: {
stxAddress: string;
limit: number;
offset: number;
} & ({ blockHeight: number } | { includeUnanchored: boolean })
): Promise<{ results: DbTx[]; total: number }>;
getAddressTxs(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit: number;
offset: number;
}): Promise<{ results: DbTx[]; total: number }>;
getAddressTxsWithAssetTransfers(
args: {
stxAddress: string;
limit?: number;
offset?: number;
} & ({ blockHeight: number } | { includeUnanchored: boolean })
): Promise<{ results: DbTxWithAssetTransfers[]; total: number }>;
getAddressTxsWithAssetTransfers(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit?: number;
offset?: number;
}): Promise<{ results: DbTxWithAssetTransfers[]; total: number }>;
getInformationTxsWithStxTransfers(args: {
stxAddress: string;
@@ -743,9 +743,9 @@ export interface DataStore extends DataStoreEventEmitter {
getAddressAssetEvents(args: {
stxAddress: string;
blockHeight: number;
limit: number;
offset: number;
includeUnanchored: boolean;
}): Promise<{ results: DbEvent[]; total: number }>;
getAddressNonces(args: {
@@ -757,14 +757,14 @@ export interface DataStore extends DataStoreEventEmitter {
detectedMissingNonces: number[];
}>;
getInboundTransfers(
args: {
stxAddress: string;
limit: number;
offset: number;
sendManyContractId: string;
} & ({ blockHeight: number } | { includeUnanchored: boolean })
): Promise<{ results: DbInboundStxTransfer[]; total: number }>;
getInboundTransfers(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit: number;
offset: number;
sendManyContractId: string;
}): Promise<{ results: DbInboundStxTransfer[]; total: number }>;
searchHash(args: { hash: string }): Promise<FoundOrNot<DbSearchResult>>;
@@ -776,6 +776,7 @@ export interface DataStore extends DataStoreEventEmitter {
getAddressNFTEvent(args: {
stxAddress: string;
blockHeight: number;
limit: number;
offset: number;
includeUnanchored: boolean;

View File

@@ -516,24 +516,22 @@ export class MemoryDataStore
getFungibleTokenBalances(args: {
stxAddress: string;
includeUnanchored: boolean;
untilBlock: number;
}): Promise<Map<string, DbStxBalance>> {
throw new Error('not yet implemented');
}
getNonFungibleTokenCounts(args: {
stxAddress: string;
includeUnanchored: boolean;
untilBlock: number;
}): Promise<Map<string, { count: bigint; totalSent: bigint; totalReceived: bigint }>> {
throw new Error('not yet implemented');
}
getAddressTxs({
stxAddress,
limit,
offset,
}: {
getAddressTxs(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit: number;
offset: number;
}): Promise<{ results: DbTx[]; total: number }> {
@@ -549,19 +547,17 @@ export class MemoryDataStore
getAddressTxsWithAssetTransfers(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit?: number;
offset?: number;
blockHeight?: number;
}): Promise<{ results: DbTxWithAssetTransfers[]; total: number }> {
throw new Error('not yet implemented');
}
getAddressAssetEvents({
stxAddress,
limit,
offset,
}: {
getAddressAssetEvents(args: {
stxAddress: string;
blockHeight: number;
limit: number;
offset: number;
}): Promise<{ results: DbEvent[]; total: number }> {
@@ -579,12 +575,13 @@ export class MemoryDataStore
throw new Error('not yet implemented');
}
getInboundTransfers({
stxAddress,
}: {
getInboundTransfers(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit: number;
offset: number;
sendManyContractId: string;
}): Promise<{ results: DbInboundStxTransfer[]; total: number }> {
throw new Error('not yet implemented');
}
@@ -636,8 +633,10 @@ export class MemoryDataStore
getAddressNFTEvent(args: {
stxAddress: string;
blockHeight: number;
limit: number;
offset: number;
includeUnanchored: boolean;
}): Promise<{ results: AddressNftEventIdentifier[]; total: number }> {
throw new Error('Method not implemented.');
}

View File

@@ -4870,15 +4870,14 @@ export class PgDataStore
stxAddress,
limit,
offset,
includeUnanchored,
blockHeight,
}: {
stxAddress: string;
limit: number;
offset: number;
includeUnanchored: boolean;
blockHeight: number;
}): Promise<{ results: DbEvent[]; total: number }> {
return this.queryTx(async client => {
const maxBlockHeight = await this.getMaxBlockHeight(client, { includeUnanchored });
const results = await client.query<
{
asset_type: 'stx_lock' | 'stx' | 'ft' | 'nft';
@@ -4929,7 +4928,7 @@ export class PgDataStore
LIMIT $2
OFFSET $3
`,
[stxAddress, limit, offset, maxBlockHeight]
[stxAddress, limit, offset, blockHeight]
);
const events: DbEvent[] = results.rows.map(row => {
@@ -5002,15 +5001,11 @@ export class PgDataStore
});
}
async getFungibleTokenBalances({
stxAddress,
includeUnanchored,
}: {
async getFungibleTokenBalances(args: {
stxAddress: string;
includeUnanchored: boolean;
untilBlock: number;
}): Promise<Map<string, DbFtBalance>> {
return this.queryTx(async client => {
const blockHeight = await this.getMaxBlockHeight(client, { includeUnanchored });
const result = await client.query<{
asset_identifier: string;
credit_total: string | null;
@@ -5037,7 +5032,7 @@ export class PgDataStore
SELECT coalesce(credit.asset_identifier, debit.asset_identifier) as asset_identifier, credit_total, debit_total
FROM credit FULL JOIN debit USING (asset_identifier)
`,
[stxAddress, blockHeight]
[args.stxAddress, args.untilBlock]
);
// sort by asset name (case-insensitive)
const rows = result.rows.sort((r1, r2) =>
@@ -5055,15 +5050,11 @@ export class PgDataStore
});
}
async getNonFungibleTokenCounts({
stxAddress,
includeUnanchored,
}: {
async getNonFungibleTokenCounts(args: {
stxAddress: string;
includeUnanchored: boolean;
untilBlock: number;
}): Promise<Map<string, { count: bigint; totalSent: bigint; totalReceived: bigint }>> {
return this.queryTx(async client => {
const blockHeight = await this.getMaxBlockHeight(client, { includeUnanchored });
const result = await client.query<{
asset_identifier: string;
received_total: string | null;
@@ -5090,7 +5081,7 @@ export class PgDataStore
SELECT coalesce(credit.asset_identifier, debit.asset_identifier) as asset_identifier, received_total, sent_total
FROM credit FULL JOIN debit USING (asset_identifier)
`,
[stxAddress, blockHeight]
[args.stxAddress, args.untilBlock]
);
// sort by asset name (case-insensitive)
const rows = result.rows.sort((r1, r2) =>
@@ -5108,26 +5099,14 @@ export class PgDataStore
});
}
async getAddressTxs(
args: {
stxAddress: string;
limit: number;
offset: number;
} & ({ blockHeight: number } | { includeUnanchored: boolean })
): Promise<{ results: DbTx[]; total: number }> {
async getAddressTxs(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit: number;
offset: number;
}): Promise<{ results: DbTx[]; total: number }> {
return this.queryTx(async client => {
let atSingleBlock: boolean;
const queryParams: (string | number)[] = [args.stxAddress, args.limit, args.offset];
if ('blockHeight' in args) {
queryParams.push(args.blockHeight);
atSingleBlock = true;
} else {
const blockHeight = await this.getMaxBlockHeight(client, {
includeUnanchored: args.includeUnanchored,
});
atSingleBlock = false;
queryParams.push(blockHeight);
}
const principal = isValidPrincipal(args.stxAddress);
if (!principal) {
return { results: [], total: 0 };
@@ -5135,7 +5114,9 @@ export class PgDataStore
// Smart contracts with a very high tx volume get frequent requests for the last N tx, where N
// is commonly <= 50. We'll query a materialized view if this is the case.
const useMaterializedView =
principal.type == 'contractAddress' && !atSingleBlock && args.limit + args.offset <= 50;
principal.type == 'contractAddress' &&
!args.atSingleBlock &&
args.limit + args.offset <= 50;
const resultQuery = useMaterializedView
? await client.query<TxQueryResult & { count: number }>(
`
@@ -5146,7 +5127,7 @@ export class PgDataStore
LIMIT $2
OFFSET $3
`,
queryParams
[args.stxAddress, args.limit, args.offset, args.blockHeight]
)
: await client.query<TxQueryResult & { count: number }>(
`
@@ -5170,12 +5151,12 @@ export class PgDataStore
)
SELECT ${TX_COLUMNS}, (COUNT(*) OVER())::integer as count
FROM principal_txs
${atSingleBlock ? 'WHERE block_height = $4' : 'WHERE block_height <= $4'}
${args.atSingleBlock ? 'WHERE block_height = $4' : 'WHERE block_height <= $4'}
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
LIMIT $2
OFFSET $3
`,
queryParams
[args.stxAddress, args.limit, args.offset, args.blockHeight]
);
const count = resultQuery.rowCount > 0 ? resultQuery.rows[0].count : 0;
const parsed = resultQuery.rows.map(r => this.parseTxQueryResult(r));
@@ -5249,29 +5230,22 @@ export class PgDataStore
});
}
async getAddressTxsWithAssetTransfers(
args: {
stxAddress: string;
limit?: number;
offset?: number;
} & ({ blockHeight: number } | { includeUnanchored: boolean })
): Promise<{ results: DbTxWithAssetTransfers[]; total: number }> {
async getAddressTxsWithAssetTransfers(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit?: number;
offset?: number;
}): Promise<{ results: DbTxWithAssetTransfers[]; total: number }> {
return this.queryTx(async client => {
let atSingleBlock: boolean;
const queryParams: (string | number)[] = [args.stxAddress];
if ('blockHeight' in args) {
// Single block mode ignores `limit` and `offset` arguments so we can retrieve all
// address events for that address in that block.
atSingleBlock = true;
if (args.atSingleBlock) {
queryParams.push(args.blockHeight);
} else {
const blockHeight = await this.getMaxBlockHeight(client, {
includeUnanchored: args.includeUnanchored,
});
atSingleBlock = false;
queryParams.push(args.limit ?? 20);
queryParams.push(args.offset ?? 0);
queryParams.push(blockHeight);
queryParams.push(args.blockHeight);
}
// Use a JOIN to include stx_events associated with the address's txs
const resultQuery = await client.query<
@@ -5310,9 +5284,9 @@ export class PgDataStore
)
SELECT ${TX_COLUMNS}, (COUNT(*) OVER())::integer as count
FROM principal_txs
${atSingleBlock ? 'WHERE block_height = $2' : 'WHERE block_height <= $4'}
${args.atSingleBlock ? 'WHERE block_height = $2' : 'WHERE block_height <= $4'}
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
${!atSingleBlock ? 'LIMIT $2 OFFSET $3' : ''}
${!args.atSingleBlock ? 'LIMIT $2 OFFSET $3' : ''}
), events AS (
SELECT
tx_id, sender, recipient, event_index, amount,
@@ -5460,30 +5434,19 @@ export class PgDataStore
return txs;
}
async getInboundTransfers(
args: {
stxAddress: string;
limit: number;
offset: number;
sendManyContractId: string;
} & ({ blockHeight: number } | { includeUnanchored: boolean })
): Promise<{ results: DbInboundStxTransfer[]; total: number }> {
async getInboundTransfers(args: {
stxAddress: string;
blockHeight: number;
atSingleBlock: boolean;
limit: number;
offset: number;
sendManyContractId: string;
}): Promise<{ results: DbInboundStxTransfer[]; total: number }> {
return this.queryTx(async client => {
const queryParams: (string | number)[] = [
args.stxAddress,
args.sendManyContractId,
args.limit,
args.offset,
];
let whereClause: string;
if ('blockHeight' in args) {
queryParams.push(args.blockHeight);
if (args.atSingleBlock) {
whereClause = 'WHERE block_height = $5';
} else {
const blockHeight = await this.getMaxBlockHeight(client, {
includeUnanchored: args.includeUnanchored,
});
queryParams.push(blockHeight);
whereClause = 'WHERE block_height <= $5';
}
const resultQuery = await client.query<TransferQueryResult & { count: number }>(
@@ -5539,7 +5502,7 @@ export class PgDataStore
LIMIT $3
OFFSET $4
`,
queryParams
[args.stxAddress, args.sendManyContractId, args.limit, args.offset, args.blockHeight]
);
const count = resultQuery.rowCount > 0 ? resultQuery.rows[0].count : 0;
const parsed: DbInboundStxTransfer[] = resultQuery.rows.map(r => {
@@ -5814,12 +5777,10 @@ export class PgDataStore
stxAddress: string;
limit: number;
offset: number;
blockHeight: number;
includeUnanchored: boolean;
}): Promise<{ results: AddressNftEventIdentifier[]; total: number }> {
return this.queryTx(async client => {
const maxBlockHeight = await this.getMaxBlockHeight(client, {
includeUnanchored: args.includeUnanchored,
});
const result = await client.query<AddressNftEventIdentifier & { count: string }>(
// Join against `nft_custody` materialized view only if we're looking for canonical results.
`
@@ -5843,7 +5804,7 @@ export class PgDataStore
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC, event_index DESC
LIMIT $2 OFFSET $3
`,
[args.stxAddress, args.limit, args.offset, maxBlockHeight]
[args.stxAddress, args.limit, args.offset, args.blockHeight]
);
const count = result.rows.length > 0 ? parseInt(result.rows[0].count) : 0;

View File

@@ -4614,6 +4614,36 @@ describe('api tests', () => {
expect(searchResult.status).toBe(404);
});
test('exclusive address endpoints params', async () => {
const addressEndpoints = [
'/stx',
'/balances',
'/transactions',
'/transactions_with_transfers',
'/assets',
'/stx_inbound',
'/nft_events',
];
//check for mutually exclusive unachored and and until_block
for (const path of addressEndpoints) {
const response = await supertest(api.server).get(
`/extended/v1/address/STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6${path}?until_block=5&unanchored=true`
);
expect(response.status).toBe(400);
}
const addressEndpoints1 = ['/transactions', '/transactions_with_transfers', '/stx_inbound'];
/// check for mutually exclusive until_block adn height params
for (const path of addressEndpoints1) {
const response1 = await supertest(api.server).get(
`/extended/v1/address/STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6${path}?until_block=5&height=0`
);
expect(response1.status).toBe(400);
}
});
test('Success: nft events for address', async () => {
const addr1 = 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1';
const addr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4';

View File

@@ -514,21 +514,22 @@ describe('postgres datastore', () => {
await db.updateFtEvent(client, tx, event);
}
const blockHeight = await db.getMaxBlockHeight(client, { includeUnanchored: false });
const addrAResult = await db.getFungibleTokenBalances({
stxAddress: 'addrA',
includeUnanchored: false,
untilBlock: blockHeight,
});
const addrBResult = await db.getFungibleTokenBalances({
stxAddress: 'addrB',
includeUnanchored: false,
untilBlock: blockHeight,
});
const addrCResult = await db.getFungibleTokenBalances({
stxAddress: 'addrC',
includeUnanchored: false,
untilBlock: blockHeight,
});
const addrDResult = await db.getFungibleTokenBalances({
stxAddress: 'addrD',
includeUnanchored: false,
untilBlock: blockHeight,
});
expect([...addrAResult]).toEqual([
@@ -665,21 +666,23 @@ describe('postgres datastore', () => {
await db.updateNftEvent(client, tx, event);
}
const blockHeight = await db.getMaxBlockHeight(client, { includeUnanchored: false });
const addrAResult = await db.getNonFungibleTokenCounts({
stxAddress: 'addrA',
includeUnanchored: false,
untilBlock: blockHeight,
});
const addrBResult = await db.getNonFungibleTokenCounts({
stxAddress: 'addrB',
includeUnanchored: false,
untilBlock: blockHeight,
});
const addrCResult = await db.getNonFungibleTokenCounts({
stxAddress: 'addrC',
includeUnanchored: false,
untilBlock: blockHeight,
});
const addrDResult = await db.getNonFungibleTokenCounts({
stxAddress: 'addrD',
includeUnanchored: false,
untilBlock: blockHeight,
});
expect([...addrAResult]).toEqual([
@@ -790,6 +793,7 @@ describe('postgres datastore', () => {
sender: string,
recipient: string,
amount: number,
dbBlock: DbBlock,
canonical: boolean = true
): DbTx => {
const tx: DbTx = {
@@ -831,57 +835,65 @@ describe('postgres datastore', () => {
return tx;
};
const txs = [
createStxTx('none', 'addrA', 100_000),
createStxTx('addrA', 'addrB', 100),
createStxTx('addrA', 'addrB', 250),
createStxTx('addrA', 'addrB', 40, false),
createStxTx('addrB', 'addrC', 15),
createStxTx('addrA', 'addrC', 35),
createStxTx('addrE', 'addrF', 2),
createStxTx('addrE', 'addrF', 2),
createStxTx('addrE', 'addrF', 2),
createStxTx('addrE', 'addrF', 2),
createStxTx('addrE', 'addrF', 2),
createStxTx('none', 'addrA', 100_000, dbBlock),
createStxTx('addrA', 'addrB', 100, dbBlock),
createStxTx('addrA', 'addrB', 250, dbBlock),
createStxTx('addrA', 'addrB', 40, dbBlock, false),
createStxTx('addrB', 'addrC', 15, dbBlock),
createStxTx('addrA', 'addrC', 35, dbBlock),
createStxTx('addrE', 'addrF', 2, dbBlock),
createStxTx('addrE', 'addrF', 2, dbBlock),
createStxTx('addrE', 'addrF', 2, dbBlock),
createStxTx('addrE', 'addrF', 2, dbBlock),
createStxTx('addrE', 'addrF', 2, dbBlock),
];
for (const tx of txs) {
await db.updateTx(client, tx);
}
const blockHeight = await db.getMaxBlockHeight(client, { includeUnanchored: false });
const addrAResult = await db.getAddressTxs({
stxAddress: 'addrA',
limit: 3,
offset: 0,
includeUnanchored: false,
blockHeight: blockHeight,
atSingleBlock: false,
});
const addrBResult = await db.getAddressTxs({
stxAddress: 'addrB',
limit: 3,
offset: 0,
includeUnanchored: false,
blockHeight: blockHeight,
atSingleBlock: false,
});
const addrCResult = await db.getAddressTxs({
stxAddress: 'addrC',
limit: 3,
offset: 0,
includeUnanchored: false,
blockHeight: blockHeight,
atSingleBlock: false,
});
const addrDResult = await db.getAddressTxs({
stxAddress: 'addrD',
limit: 3,
offset: 0,
includeUnanchored: false,
blockHeight: blockHeight,
atSingleBlock: false,
});
const addrEResult = await db.getAddressTxs({
stxAddress: 'addrE',
limit: 3,
offset: 0,
includeUnanchored: false,
blockHeight: blockHeight,
atSingleBlock: false,
});
const addrEResultP2 = await db.getAddressTxs({
stxAddress: 'addrE',
limit: 3,
offset: 3,
includeUnanchored: false,
blockHeight: blockHeight,
atSingleBlock: false,
});
expect(addrEResult.total).toBe(5);
@@ -984,6 +996,62 @@ describe('postgres datastore', () => {
},
]);
expect(mapAddrTxResults(addrDResult.results)).toEqual([]);
//test for atBlock query
const dbBlock1: DbBlock = {
block_hash: '0xffff',
index_block_hash: '0x1235',
parent_index_block_hash: '0x5679',
parent_block_hash: '0x5670',
parent_microblock_hash: '',
parent_microblock_sequence: 0,
block_height: 2,
burn_block_time: 1594647996,
burn_block_hash: '0x1235',
burn_block_height: 124,
miner_txid: '0x4322',
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, dbBlock1);
const txs1 = [
createStxTx('addrA', 'addrB', 100, dbBlock1),
createStxTx('addrA', 'addrB', 250, dbBlock1),
createStxTx('addrA', 'addrB', 40, dbBlock1, false),
createStxTx('addrB', 'addrC', 15, dbBlock1),
createStxTx('addrA', 'addrC', 35, dbBlock1),
createStxTx('addrE', 'addrF', 2, dbBlock1),
createStxTx('addrE', 'addrF', 2, dbBlock1),
createStxTx('addrE', 'addrF', 2, dbBlock1),
createStxTx('addrE', 'addrF', 2, dbBlock1),
createStxTx('addrE', 'addrF', 2, dbBlock1),
];
for (const tx of txs) {
await db.updateTx(client, tx);
}
const addrAAtBlockResult = await db.getAddressTxs({
stxAddress: 'addrA',
limit: 3,
offset: 0,
blockHeight: 2,
atSingleBlock: true,
});
const addrAAllBlockResult = await db.getAddressTxs({
stxAddress: 'addrA',
limit: 3,
offset: 0,
blockHeight: 2,
atSingleBlock: false,
});
expect(addrAAtBlockResult.total).toBe(4);
expect(addrAAllBlockResult.total).toBe(8);
});
test('pg get address asset events', async () => {
@@ -1230,11 +1298,13 @@ describe('postgres datastore', () => {
await db.updateNftEvent(client, tx3, event);
}
const blockHeight = await db.getMaxBlockHeight(client, { includeUnanchored: false });
const assetDbEvents = await db.getAddressAssetEvents({
stxAddress: 'addrA',
limit: 10000,
offset: 0,
includeUnanchored: false,
blockHeight: blockHeight,
});
const assetEvents = assetDbEvents.results.map(event => parseDbEvent(event));
expect(assetEvents).toEqual([