mirror of
https://github.com/alexgo-io/stacks-blockchain-api.git
synced 2026-06-12 07:45:21 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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`');
|
||||
}
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user