Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cut release v7.12.0 #2029

Merged
merged 9 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading