Skip to content

Commit 435be5a

Browse files
authored
Merge pull request #2029 from hirosystems/develop
Cut release v7.12.0
2 parents 3a6dc25 + c35b65a commit 435be5a

17 files changed

+3382
-110
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ jobs:
413413
parallel: true
414414

415415
test-subnets:
416+
if: false
416417
runs-on: ubuntu-latest
417418
steps:
418419
- uses: actions/checkout@v3

docs/openapi.yaml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,71 @@ paths:
219219
items:
220220
type: string
221221
enum: [coinbase, token_transfer, smart_contract, contract_call, poison_microblock, tenure_change]
222+
- name: from_address
223+
in: query
224+
description: Option to filter results by sender address
225+
required: false
226+
schema:
227+
type: string
228+
- name: to_address
229+
in: query
230+
description: Option to filter results by recipient address
231+
required: false
232+
schema:
233+
type: string
234+
- name: sort_by
235+
in: query
236+
description: Option to sort results by block height, timestamp, or fee
237+
required: false
238+
schema:
239+
type: string
240+
enum: [block_height, burn_block_time, fee]
241+
example: burn_block_time
242+
default: block_height
243+
- name: start_time
244+
in: query
245+
description: Filter by transactions after this timestamp (unix timestamp in seconds)
246+
required: false
247+
schema:
248+
type: integer
249+
example: 1704067200
250+
- name: end_time
251+
in: query
252+
description: Filter by transactions before this timestamp (unix timestamp in seconds)
253+
required: false
254+
schema:
255+
type: integer
256+
example: 1706745599
257+
- name: contract_id
258+
in: query
259+
description: Filter by contract call transactions involving this contract ID
260+
required: false
261+
schema:
262+
type: string
263+
example: "SP000000000000000000002Q6VF78.pox-4"
264+
- name: function_name
265+
in: query
266+
description: Filter by contract call transactions involving this function name
267+
required: false
268+
schema:
269+
type: string
270+
example: "delegate-stx"
271+
- name: nonce
272+
in: query
273+
description: Filter by transactions with this nonce
274+
required: false
275+
schema:
276+
type: integer
277+
example: 123
278+
- name: order
279+
in: query
280+
description: Option to sort results in ascending or descending order
281+
required: false
282+
schema:
283+
type: string
284+
enum: [asc, desc]
285+
example: desc
286+
default: desc
222287
- name: unanchored
223288
in: query
224289
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.createIndex('txs', 'burn_block_time');
4+
pgm.createIndex('txs', 'fee_rate');
5+
};
6+
7+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
8+
exports.down = pgm => {
9+
pgm.dropIndex('txs', 'burn_block_time');
10+
pgm.dropIndex('txs', 'fee_rate');
11+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.createIndex('txs', 'contract_call_function_name');
4+
};
5+
6+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
7+
exports.down = pgm => {
8+
pgm.dropIndex('txs', 'contract_call_function_name');
9+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.createIndex('txs', 'nonce');
4+
};
5+
6+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
7+
exports.down = pgm => {
8+
pgm.dropIndex('txs', 'nonce');
9+
};

package-lock.json

Lines changed: 10 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
},
8686
"dependencies": {
8787
"@apidevtools/json-schema-ref-parser": "9.0.9",
88-
"@hirosystems/api-toolkit": "1.5.0",
88+
"@hirosystems/api-toolkit": "1.6.2",
8989
"@promster/express": "6.0.0",
9090
"@promster/server": "6.0.6",
9191
"@promster/types": "3.2.3",

src/api/routes/tx.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,121 @@ export function createTxRouter(db: PgStore): express.Router {
6565
txTypeFilter = [];
6666
}
6767

68+
let order: 'asc' | 'desc' | undefined;
69+
if (req.query.order) {
70+
if (
71+
typeof req.query.order === 'string' &&
72+
(req.query.order === 'asc' || req.query.order === 'desc')
73+
) {
74+
order = req.query.order;
75+
} else {
76+
throw new InvalidRequestError(
77+
`The "order" query parameter must be a 'desc' or 'asc'`,
78+
InvalidRequestErrorType.invalid_param
79+
);
80+
}
81+
}
82+
83+
let fromAddress: string | undefined;
84+
if (typeof req.query.from_address === 'string') {
85+
if (!isValidC32Address(req.query.from_address)) {
86+
throw new InvalidRequestError(
87+
`Invalid query parameter for "from_address": "${req.query.from_address}" is not a valid STX address`,
88+
InvalidRequestErrorType.invalid_param
89+
);
90+
}
91+
fromAddress = req.query.from_address;
92+
}
93+
94+
let toAddress: string | undefined;
95+
if (typeof req.query.to_address === 'string') {
96+
if (!isValidPrincipal(req.query.to_address)) {
97+
throw new InvalidRequestError(
98+
`Invalid query parameter for "to_address": "${req.query.to_address}" is not a valid STX address`,
99+
InvalidRequestErrorType.invalid_param
100+
);
101+
}
102+
toAddress = req.query.to_address;
103+
}
104+
105+
let startTime: number | undefined;
106+
if (typeof req.query.start_time === 'string') {
107+
if (!/^\d{10}$/.test(req.query.start_time)) {
108+
throw new InvalidRequestError(
109+
`Invalid query parameter for "start_time": "${req.query.start_time}" is not a valid timestamp`,
110+
InvalidRequestErrorType.invalid_param
111+
);
112+
}
113+
startTime = parseInt(req.query.start_time);
114+
}
115+
116+
let endTime: number | undefined;
117+
if (typeof req.query.end_time === 'string') {
118+
if (!/^\d{10}$/.test(req.query.end_time)) {
119+
throw new InvalidRequestError(
120+
`Invalid query parameter for "end_time": "${req.query.end_time}" is not a valid timestamp`,
121+
InvalidRequestErrorType.invalid_param
122+
);
123+
}
124+
endTime = parseInt(req.query.end_time);
125+
}
126+
127+
let contractId: string | undefined;
128+
if (typeof req.query.contract_id === 'string') {
129+
if (!isValidPrincipal(req.query.contract_id)) {
130+
throw new InvalidRequestError(
131+
`Invalid query parameter for "contract_id": "${req.query.contract_id}" is not a valid principal`,
132+
InvalidRequestErrorType.invalid_param
133+
);
134+
}
135+
contractId = req.query.contract_id;
136+
}
137+
138+
let functionName: string | undefined;
139+
if (typeof req.query.function_name === 'string') {
140+
functionName = req.query.function_name;
141+
}
142+
143+
let nonce: number | undefined;
144+
if (typeof req.query.nonce === 'string') {
145+
if (!/^\d{1,10}$/.test(req.query.nonce)) {
146+
throw new InvalidRequestError(
147+
`Invalid query parameter for "nonce": "${req.query.nonce}" is not a valid nonce`,
148+
InvalidRequestErrorType.invalid_param
149+
);
150+
}
151+
nonce = parseInt(req.query.nonce);
152+
}
153+
154+
let sortBy: 'block_height' | 'burn_block_time' | 'fee' | undefined;
155+
if (req.query.sort_by) {
156+
if (
157+
typeof req.query.sort_by === 'string' &&
158+
['block_height', 'burn_block_time', 'fee'].includes(req.query.sort_by)
159+
) {
160+
sortBy = req.query.sort_by as typeof sortBy;
161+
} else {
162+
throw new InvalidRequestError(
163+
`The "sort_by" query parameter must be 'block_height', 'burn_block_time', or 'fee'`,
164+
InvalidRequestErrorType.invalid_param
165+
);
166+
}
167+
}
68168
const includeUnanchored = isUnanchoredRequest(req, res, next);
69169
const { results: txResults, total } = await db.getTxList({
70170
offset,
71171
limit,
72172
txTypeFilter,
73173
includeUnanchored,
174+
fromAddress,
175+
toAddress,
176+
startTime,
177+
endTime,
178+
contractId,
179+
functionName,
180+
nonce,
181+
order,
182+
sortBy,
74183
});
75184
const results = txResults.map(tx => parseDbTx(tx));
76185
const response: TransactionResults = { limit, offset, total, results };

src/datastore/helpers.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -273,29 +273,18 @@ export function prefixedCols(columns: string[], prefix: string): string[] {
273273
return columns.map(c => `${prefix}.${c}`);
274274
}
275275

276-
/**
277-
* Concatenates column names to use on a query. Necessary when one or more of those columns is complex enough
278-
* so that postgres.js can't figure out how to list it (e.g. abi column, aggregates, partitions, etc.).
279-
* @param sql - SQL client
280-
* @param columns - list of columns
281-
* @returns raw SQL column list string
282-
*/
283-
export function unsafeCols(sql: PgSqlClient, columns: string[]): postgres.PendingQuery<any> {
284-
return sql.unsafe(columns.join(', '));
285-
}
286-
287276
/**
288277
* Shorthand function that returns a column query to retrieve the smart contract abi when querying transactions
289278
* that may be of type `contract_call`. Usually used alongside `TX_COLUMNS` or `MEMPOOL_TX_COLUMNS`.
290279
* @param tableName - Name of the table that will determine the transaction type. Defaults to `txs`.
291280
* @returns `string` - abi column select statement portion
292281
*/
293-
export function abiColumn(tableName: string = 'txs'): string {
294-
return `
295-
CASE WHEN ${tableName}.type_id = ${DbTxTypeId.ContractCall} THEN (
282+
export function abiColumn(sql: PgSqlClient, tableName: string = 'txs'): postgres.Fragment {
283+
return sql`
284+
CASE WHEN ${sql(tableName)}.type_id = ${DbTxTypeId.ContractCall} THEN (
296285
SELECT abi
297286
FROM smart_contracts
298-
WHERE smart_contracts.contract_id = ${tableName}.contract_call_contract_id
287+
WHERE smart_contracts.contract_id = ${sql(tableName)}.contract_call_contract_id
299288
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
300289
LIMIT 1
301290
) END as abi

0 commit comments

Comments
 (0)