Skip to content

Commit

Permalink
Merge pull request #2029 from hirosystems/develop
Browse files Browse the repository at this point in the history
Cut release v7.12.0
  • Loading branch information
He1DAr authored Jul 8, 2024
2 parents 3a6dc25 + c35b65a commit 435be5a
Show file tree
Hide file tree
Showing 17 changed files with 3,382 additions and 110 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ jobs:
parallel: true

test-subnets:
if: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
65 changes: 65 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,71 @@ paths:
items:
type: string
enum: [coinbase, token_transfer, smart_contract, contract_call, poison_microblock, tenure_change]
- name: from_address
in: query
description: Option to filter results by sender address
required: false
schema:
type: string
- name: to_address
in: query
description: Option to filter results by recipient address
required: false
schema:
type: string
- name: sort_by
in: query
description: Option to sort results by block height, timestamp, or fee
required: false
schema:
type: string
enum: [block_height, burn_block_time, fee]
example: burn_block_time
default: block_height
- name: start_time
in: query
description: Filter by transactions after this timestamp (unix timestamp in seconds)
required: false
schema:
type: integer
example: 1704067200
- name: end_time
in: query
description: Filter by transactions before this timestamp (unix timestamp in seconds)
required: false
schema:
type: integer
example: 1706745599
- name: contract_id
in: query
description: Filter by contract call transactions involving this contract ID
required: false
schema:
type: string
example: "SP000000000000000000002Q6VF78.pox-4"
- name: function_name
in: query
description: Filter by contract call transactions involving this function name
required: false
schema:
type: string
example: "delegate-stx"
- name: nonce
in: query
description: Filter by transactions with this nonce
required: false
schema:
type: integer
example: 123
- name: order
in: query
description: Option to sort results in ascending or descending order
required: false
schema:
type: string
enum: [asc, desc]
example: desc
default: desc
- name: unanchored
in: query
description: Include transaction data from unanchored (i.e. unconfirmed) microblocks
Expand Down
11 changes: 11 additions & 0 deletions migrations/1718632097776_tx-sort-indexes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.up = pgm => {
pgm.createIndex('txs', 'burn_block_time');
pgm.createIndex('txs', 'fee_rate');
};

/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.down = pgm => {
pgm.dropIndex('txs', 'burn_block_time');
pgm.dropIndex('txs', 'fee_rate');
};
9 changes: 9 additions & 0 deletions migrations/1718887498565_tx-contract-call-indexes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.up = pgm => {
pgm.createIndex('txs', 'contract_call_function_name');
};

/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.down = pgm => {
pgm.dropIndex('txs', 'contract_call_function_name');
};
9 changes: 9 additions & 0 deletions migrations/1718887498565_tx-nonce-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.up = pgm => {
pgm.createIndex('txs', 'nonce');
};

/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
exports.down = pgm => {
pgm.dropIndex('txs', 'nonce');
};
14 changes: 10 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "9.0.9",
"@hirosystems/api-toolkit": "1.5.0",
"@hirosystems/api-toolkit": "1.6.2",
"@promster/express": "6.0.0",
"@promster/server": "6.0.6",
"@promster/types": "3.2.3",
Expand Down
109 changes: 109 additions & 0 deletions src/api/routes/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,121 @@ export function createTxRouter(db: PgStore): express.Router {
txTypeFilter = [];
}

let order: 'asc' | 'desc' | undefined;
if (req.query.order) {
if (
typeof req.query.order === 'string' &&
(req.query.order === 'asc' || req.query.order === 'desc')
) {
order = req.query.order;
} else {
throw new InvalidRequestError(
`The "order" query parameter must be a 'desc' or 'asc'`,
InvalidRequestErrorType.invalid_param
);
}
}

let fromAddress: string | undefined;
if (typeof req.query.from_address === 'string') {
if (!isValidC32Address(req.query.from_address)) {
throw new InvalidRequestError(
`Invalid query parameter for "from_address": "${req.query.from_address}" is not a valid STX address`,
InvalidRequestErrorType.invalid_param
);
}
fromAddress = req.query.from_address;
}

let toAddress: string | undefined;
if (typeof req.query.to_address === 'string') {
if (!isValidPrincipal(req.query.to_address)) {
throw new InvalidRequestError(
`Invalid query parameter for "to_address": "${req.query.to_address}" is not a valid STX address`,
InvalidRequestErrorType.invalid_param
);
}
toAddress = req.query.to_address;
}

let startTime: number | undefined;
if (typeof req.query.start_time === 'string') {
if (!/^\d{10}$/.test(req.query.start_time)) {
throw new InvalidRequestError(
`Invalid query parameter for "start_time": "${req.query.start_time}" is not a valid timestamp`,
InvalidRequestErrorType.invalid_param
);
}
startTime = parseInt(req.query.start_time);
}

let endTime: number | undefined;
if (typeof req.query.end_time === 'string') {
if (!/^\d{10}$/.test(req.query.end_time)) {
throw new InvalidRequestError(
`Invalid query parameter for "end_time": "${req.query.end_time}" is not a valid timestamp`,
InvalidRequestErrorType.invalid_param
);
}
endTime = parseInt(req.query.end_time);
}

let contractId: string | undefined;
if (typeof req.query.contract_id === 'string') {
if (!isValidPrincipal(req.query.contract_id)) {
throw new InvalidRequestError(
`Invalid query parameter for "contract_id": "${req.query.contract_id}" is not a valid principal`,
InvalidRequestErrorType.invalid_param
);
}
contractId = req.query.contract_id;
}

let functionName: string | undefined;
if (typeof req.query.function_name === 'string') {
functionName = req.query.function_name;
}

let nonce: number | undefined;
if (typeof req.query.nonce === 'string') {
if (!/^\d{1,10}$/.test(req.query.nonce)) {
throw new InvalidRequestError(
`Invalid query parameter for "nonce": "${req.query.nonce}" is not a valid nonce`,
InvalidRequestErrorType.invalid_param
);
}
nonce = parseInt(req.query.nonce);
}

let sortBy: 'block_height' | 'burn_block_time' | 'fee' | undefined;
if (req.query.sort_by) {
if (
typeof req.query.sort_by === 'string' &&
['block_height', 'burn_block_time', 'fee'].includes(req.query.sort_by)
) {
sortBy = req.query.sort_by as typeof sortBy;
} else {
throw new InvalidRequestError(
`The "sort_by" query parameter must be 'block_height', 'burn_block_time', or 'fee'`,
InvalidRequestErrorType.invalid_param
);
}
}
const includeUnanchored = isUnanchoredRequest(req, res, next);
const { results: txResults, total } = await db.getTxList({
offset,
limit,
txTypeFilter,
includeUnanchored,
fromAddress,
toAddress,
startTime,
endTime,
contractId,
functionName,
nonce,
order,
sortBy,
});
const results = txResults.map(tx => parseDbTx(tx));
const response: TransactionResults = { limit, offset, total, results };
Expand Down
19 changes: 4 additions & 15 deletions src/datastore/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,29 +273,18 @@ export function prefixedCols(columns: string[], prefix: string): string[] {
return columns.map(c => `${prefix}.${c}`);
}

/**
* Concatenates column names to use on a query. Necessary when one or more of those columns is complex enough
* so that postgres.js can't figure out how to list it (e.g. abi column, aggregates, partitions, etc.).
* @param sql - SQL client
* @param columns - list of columns
* @returns raw SQL column list string
*/
export function unsafeCols(sql: PgSqlClient, columns: string[]): postgres.PendingQuery<any> {
return sql.unsafe(columns.join(', '));
}

/**
* Shorthand function that returns a column query to retrieve the smart contract abi when querying transactions
* that may be of type `contract_call`. Usually used alongside `TX_COLUMNS` or `MEMPOOL_TX_COLUMNS`.
* @param tableName - Name of the table that will determine the transaction type. Defaults to `txs`.
* @returns `string` - abi column select statement portion
*/
export function abiColumn(tableName: string = 'txs'): string {
return `
CASE WHEN ${tableName}.type_id = ${DbTxTypeId.ContractCall} THEN (
export function abiColumn(sql: PgSqlClient, tableName: string = 'txs'): postgres.Fragment {
return sql`
CASE WHEN ${sql(tableName)}.type_id = ${DbTxTypeId.ContractCall} THEN (
SELECT abi
FROM smart_contracts
WHERE smart_contracts.contract_id = ${tableName}.contract_call_contract_id
WHERE smart_contracts.contract_id = ${sql(tableName)}.contract_call_contract_id
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
LIMIT 1
) END as abi
Expand Down
Loading

0 comments on commit 435be5a

Please sign in to comment.