Skip to content

Commit 508afc7

Browse files
authored
feat: events-only endpoint for address and tx_id (#1027)
* feat: wip- added api for events for address * feat: added events query for tx and address * docs: added docs for event endpoints * test: update test builder to increase event index * docs: fixed json typo * docs: update examples * refactor: cleanup * feat: change event endpoint and ad txId and principal as query param * docs: update docs for the endpoint * perf: change query from multiple query to single union query * fix: revert init.ts unnecessary changes * fix: used tx_id instead of txId in query param * refactor: use try catch only for relevant function * fix: validate address and tx_id before sending to data layer * fix: join tx to use micrblock sequence orderinging in events * refactor: put 0x prefix on txId * docs: update docs * docs: update docs for transaction events endpoint * refactor: refactor comments and test headers * perf: added cache handler for tx/events endpoint * fix: fixed conflicts * style: change comment place
1 parent 1f09c0e commit 508afc7

File tree

11 files changed

+1064
-3
lines changed

11 files changed

+1064
-3
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"limit": 96,
3+
"offset": 0,
4+
"events": [
5+
{
6+
"event_index": 0,
7+
"event_type": "non_fungible_token_asset",
8+
"tx_id": "0x05ccc123db703a2808afaaf88b6b3240f14391d14fde701bd20d7206c9133af6",
9+
"asset": {
10+
"asset_event_type": "transfer",
11+
"asset_id": "ST000000000000000000002AMW42H.bns::names",
12+
"sender": "STKVDRCTN8C81T22QHR9PG9GPD3V3WPQYBYFHPT4",
13+
"recipient": "STRWN68C36Z7WTDD1TJERTAZ4SXDRMMDB29M4VNQ",
14+
"value": {
15+
"hex": "0x0c00000002046e616d65020000000a62696c616c7465737435096e616d6573706163650200000003627463",
16+
"repr": "(tuple (name 0x62696c616c7465737435) (namespace 0x627463))"
17+
}
18+
}
19+
},
20+
{
21+
"event_index": 1,
22+
"event_type": "smart_contract_log",
23+
"tx_id": "0x05ccc123db703a2808afaaf88b6b3240f14391d14fde701bd20d7206c9133af6",
24+
"contract_log": {
25+
"contract_id": "ST000000000000000000002AMW42H.bns",
26+
"topic": "print",
27+
"value": {
28+
"hex": "0x0c000000010a6174746163686d656e740c00000003106174746163686d656e742d696e646578010000000000000000000000000000028304686173680200000014b472a266d0bd89c13706a4132ccfb16f7c3b9fcb086d657461646174610c00000004046e616d65020000000a62696c616c7465737435096e616d6573706163650200000003627463026f700d0000000d6e616d652d7472616e736665720974782d73656e646572051a27b6e19aaa1880e842bc709b4130b347b1f2d7f2",
29+
"repr": "(tuple (attachment (tuple (attachment-index u643) (hash 0xb472a266d0bd89c13706a4132ccfb16f7c3b9fcb) (metadata (tuple (name 0x62696c616c7465737435) (namespace 0x627463) (op \"name-transfer\") (tx-sender STKVDRCTN8C81T22QHR9PG9GPD3V3WPQYBYFHPT4))))))"
30+
}
31+
}
32+
}
33+
]
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"description": "GET event for the given transaction",
4+
"title": "TransactionEventsResponse",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"required": [
8+
"results",
9+
"limit",
10+
"offset"
11+
],
12+
"properties": {
13+
"limit": {
14+
"type": "integer"
15+
},
16+
"offset": {
17+
"type": "integer"
18+
},
19+
"results": {
20+
"type": "array",
21+
"items": {
22+
"$ref": "../../entities/transaction-events/transaction-event.schema.json"
23+
}
24+
}
25+
}
26+
}

docs/generated.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export type SchemaMergeRootStub =
101101
| NonFungibleTokensMetadataList
102102
| MempoolTransactionListResponse
103103
| GetRawTransactionResult
104+
| TransactionEventsResponse
104105
| TransactionResults
105106
| PostCoreNodeTransactionsError
106107
| AddressNonces
@@ -3402,6 +3403,14 @@ export interface GetRawTransactionResult {
34023403
raw_tx: string;
34033404
[k: string]: unknown | undefined;
34043405
}
3406+
/**
3407+
* GET event for the given transaction
3408+
*/
3409+
export interface TransactionEventsResponse {
3410+
limit: number;
3411+
offset: number;
3412+
results: TransactionEvent[];
3413+
}
34053414
/**
34063415
* GET request that returns transactions
34073416
*/

docs/openapi.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3296,3 +3296,59 @@ paths:
32963296
$ref: ./api/core-node/post-fee-transaction-response.schema.json
32973297
example:
32983298
$ref: ./api/core-node/post-fee-transaction-response.example.json
3299+
/extended/v1/tx/events:
3300+
get:
3301+
summary: Transaction Events
3302+
description: Retrieves the list of events filtered by principal (STX address or Smart Contract ID), transaction id or event types.
3303+
The list of event types is ('smart_contract_log', 'stx_lock', 'stx_asset', 'fungible_token_asset', 'non_fungible_token_asset').
3304+
tags:
3305+
- Transactions
3306+
operationId: get_filtered_events
3307+
parameters:
3308+
- name: tx_id
3309+
in: query
3310+
description: Hash of transaction
3311+
required: false
3312+
schema:
3313+
type: string
3314+
example: '0x29e25515652dad41ef675bd0670964e3d537b80ec19cf6ca6f1dd65d5bc642c5'
3315+
- name: address
3316+
in: query
3317+
description: Stacks address or a Contract identifier
3318+
required: false
3319+
schema:
3320+
type: string
3321+
example: ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4
3322+
- name: limit
3323+
in: query
3324+
description: number of items to return
3325+
required: false
3326+
schema:
3327+
type: integer
3328+
example: 100
3329+
- name: offset
3330+
in: query
3331+
description: number of items to skip
3332+
required: false
3333+
schema:
3334+
type: integer
3335+
example: 50
3336+
- name: type
3337+
in: query
3338+
description: Filter the events on event type
3339+
required: false
3340+
schema:
3341+
type: array
3342+
items:
3343+
type: string
3344+
enum: [smart_contract_log, stx_lock, stx_asset, fungible_token_asset, non_fungible_token_asset]
3345+
example: stx_lock
3346+
responses:
3347+
200:
3348+
description: Success
3349+
content:
3350+
application/json:
3351+
schema:
3352+
$ref: ./api/transaction/get-transaction-events.schema.json
3353+
example:
3354+
$ref: ./api/transaction/get-transaction-events.example.json

src/api/query-helpers.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ClarityAbi } from '@stacks/transactions';
22
import { NextFunction, Request, Response } from 'express';
3-
import { has0xPrefix, hexToBuffer, isValidPrincipal } from './../helpers';
3+
import { has0xPrefix, hexToBuffer, isValidPrincipal, parseEventTypeStrings } from './../helpers';
44
import { InvalidRequestError, InvalidRequestErrorType } from '../errors';
5+
import { DbEventTypeId } from './../datastore/common';
56

67
function handleBadRequest(res: Response, next: NextFunction, errorMessage: string): never {
78
const error = new InvalidRequestError(errorMessage, InvalidRequestErrorType.bad_request);
@@ -222,3 +223,66 @@ export function validatePrincipal(stxAddress: string) {
222223
);
223224
}
224225
}
226+
227+
export function parseAddressOrTxId(
228+
req: Request,
229+
res: Response,
230+
next: NextFunction
231+
): { address: string; txId: undefined } | { address: undefined; txId: string } | never {
232+
const address = req.query.address;
233+
const txId = req.query.tx_id;
234+
if (!address && !txId) {
235+
handleBadRequest(res, next, `can not find 'address' or 'tx_id' in the request`);
236+
}
237+
if (address && txId) {
238+
//if mutually exclusive address and txId specified throw
239+
handleBadRequest(res, next, `can't handle both 'address' and 'tx_id' in the same request`);
240+
}
241+
if (address) {
242+
if (typeof address === 'string') {
243+
validatePrincipal(address);
244+
return { address, txId: undefined };
245+
}
246+
handleBadRequest(res, next, `invalid 'address'`);
247+
}
248+
if (typeof txId === 'string') {
249+
const txIdHex = has0xPrefix(txId) ? txId : '0x' + txId;
250+
validateRequestHexInput(txIdHex);
251+
return { address: undefined, txId: txIdHex };
252+
}
253+
handleBadRequest(res, next, `invalid 'tx_id'`);
254+
}
255+
256+
export function parseEventTypeFilter(
257+
req: Request,
258+
res: Response,
259+
next: NextFunction
260+
): DbEventTypeId[] {
261+
const typeQuery = req.query.type;
262+
let eventTypeFilter: DbEventTypeId[];
263+
if (Array.isArray(typeQuery)) {
264+
try {
265+
eventTypeFilter = parseEventTypeStrings(typeQuery as string[]);
266+
} catch (error) {
267+
handleBadRequest(res, next, `invalid 'event type'`);
268+
}
269+
} else if (typeof typeQuery === 'string') {
270+
try {
271+
eventTypeFilter = parseEventTypeStrings([typeQuery]);
272+
} catch (error) {
273+
handleBadRequest(res, next, `invalid 'event type'`);
274+
}
275+
} else if (typeQuery) {
276+
handleBadRequest(res, next, `invalid 'event type format'`);
277+
} else {
278+
eventTypeFilter = [
279+
DbEventTypeId.SmartContractLog,
280+
DbEventTypeId.StxAsset,
281+
DbEventTypeId.FungibleTokenAsset,
282+
DbEventTypeId.NonFungibleTokenAsset,
283+
DbEventTypeId.StxLock,
284+
]; //no filter provided , return all types of events
285+
}
286+
287+
return eventTypeFilter;
288+
}

src/api/routes/tx.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import * as express from 'express';
22
import { asyncHandler } from '../async-handler';
3-
import { DataStore, DbTx, DbMempoolTx } from '../../datastore/common';
3+
import { DataStore, DbTx, DbMempoolTx, DbEventTypeId } from '../../datastore/common';
44
import {
55
getTxFromDataStore,
66
parseTxTypeStrings,
77
parseDbMempoolTx,
88
searchTx,
99
searchTxs,
1010
parseDbTx,
11+
parseDbEvent,
1112
} from '../controllers/db-controller';
1213
import {
1314
waiter,
@@ -18,12 +19,15 @@ import {
1819
bufferToHexPrefixString,
1920
isValidPrincipal,
2021
hexToBuffer,
22+
parseEventTypeStrings,
2123
} from '../../helpers';
2224
import { InvalidRequestError, InvalidRequestErrorType } from '../../errors';
2325
import {
2426
isUnanchoredRequest,
2527
getBlockHeightPathParam,
2628
validateRequestHexInput,
29+
parseAddressOrTxId,
30+
parseEventTypeFilter,
2731
} from '../query-helpers';
2832
import { parseLimitQuery, parsePagingQueryInput } from '../pagination';
2933
import { validate } from '../validate';
@@ -259,6 +263,28 @@ export function createTxRouter(db: DataStore): express.Router {
259263
})
260264
);
261265

266+
router.get(
267+
'/events',
268+
cacheHandler,
269+
asyncHandler(async (req, res, next) => {
270+
const limit = parseTxQueryEventsLimit(req.query['limit'] ?? 96);
271+
const offset = parsePagingQueryInput(req.query['offset'] ?? 0);
272+
273+
const principalOrTxId = parseAddressOrTxId(req, res, next);
274+
const eventTypeFilter = parseEventTypeFilter(req, res, next);
275+
276+
const { results } = await db.getTransactionEvents({
277+
addressOrTxId: principalOrTxId,
278+
eventTypeFilter,
279+
offset,
280+
limit,
281+
});
282+
const response = { limit, offset, events: results.map(e => parseDbEvent(e)) };
283+
setETagCacheHeaders(res);
284+
res.status(200).json(response);
285+
})
286+
);
287+
262288
// TODO: Add cache headers. Impossible right now since this tx might be from a block or from the mempool.
263289
router.get(
264290
'/:tx_id',

src/datastore/common.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,21 @@ export interface DataStore extends DataStoreEventEmitter {
690690
offset: number;
691691
}): Promise<{ results: DbEvent[] }>;
692692

693+
/**
694+
* It retrieves filtered events from the db based on transaction, principal or event type. Note: It does not accept both principal and txId at the same time
695+
* @param args - addressOrTxId: filter for either transaction id or address
696+
* @param args - eventTypeFilter: filter based on event types ids
697+
* @param args - limit: returned that many rows
698+
* @param args - offset: skip that any rows
699+
* @returns returns array of events
700+
*/
701+
getTransactionEvents(args: {
702+
addressOrTxId: { address: string; txId: undefined } | { address: undefined; txId: string };
703+
eventTypeFilter: DbEventTypeId[];
704+
limit: number;
705+
offset: number;
706+
}): Promise<{ results: DbEvent[] }>;
707+
693708
getTxListDetails(args: { txIds: string[]; includeUnanchored: boolean }): Promise<DbTx[]>; // tx_id is returned for not found case
694709

695710
getSmartContractList(contractIds: string[]): Promise<DbSmartContract[]>;

0 commit comments

Comments
 (0)