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

Transactions filters #321

Merged
merged 9 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
7 changes: 6 additions & 1 deletion packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,14 @@ Return transaction set paged
Status can be either `SUCCESS` or `FAIL`. In case of error tx, message will appear in the `error` field as Base64 string

* `limit` cannot be more then 100
* `owner` Identity identifier
* `status` can be `SUCCESS`, `FAIL` or `ALL`
* `filters` array of transactions types
pshenmic marked this conversation as resolved.
Show resolved Hide resolved
* `min` number of min `gas_used`
pshenmic marked this conversation as resolved.
Show resolved Hide resolved
* `max` number of max `gas_used`

```
GET /transactions?=1&limit=10&order=asc
GET /transactions?=1&limit=10&order=asc&owner=6q9RFbeea73tE31LGMBLFZhtBUX3wZL3TcNynqE18Zgs&filters=0&filters=1&status=ALL&min=0&max=9999999

{
pagination: {
Expand Down
31 changes: 29 additions & 2 deletions packages/api/src/controllers/TransactionsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const TransactionsDAO = require('../dao/TransactionsDAO')
const utils = require('../utils')
const { calculateInterval, iso8601duration } = require('../utils')
const Intervals = require('../enums/IntervalsEnum')
const StateTransitionEnum = require('../enums/StateTransitionEnum')

class TransactionsController {
constructor (client, knex, dapi) {
Expand All @@ -23,13 +24,39 @@ class TransactionsController {
}

getTransactions = async (request, response) => {
const { page = 1, limit = 10, order = 'asc' } = request.query
const {
page = 1,
limit = 10,
order = 'asc',
filters,
owner,
status = 'ALL',
min,
max
} = request.query

if (order !== 'asc' && order !== 'desc') {
return response.status(400).send({ message: `invalid ordering value ${order}. only 'asc' or 'desc' is valid values` })
}

const transactions = await this.transactionsDAO.getTransactions(Number(page ?? 1), Number(limit ?? 10), order)
const stateTransitionIndexes = Object.entries(StateTransitionEnum).map(([, entry]) => entry)

const validatedFilters = filters?.map((filter) => stateTransitionIndexes.includes(filter))
pshenmic marked this conversation as resolved.
Show resolved Hide resolved

if (validatedFilters?.includes(false) || filters?.length === 0) {
return response.status(400).send({ message: 'invalid filters values' })
}

const transactions = await this.transactionsDAO.getTransactions(
Number(page ?? 1),
Number(limit ?? 10),
order,
filters,
owner,
status,
min,
max
)

response.send(transactions)
}
Expand Down
45 changes: 42 additions & 3 deletions packages/api/src/dao/TransactionsDAO.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,60 @@ module.exports = class TransactionsDAO {
return Transaction.fromRow({ ...row, aliases })
}

getTransactions = async (page, limit, order) => {
getTransactions = async (page, limit, order, filters, owner, status, min, max) => {
const fromRank = ((page - 1) * limit) + 1
const toRank = fromRank + limit - 1

let filtersQuery = ''
const filtersBindings = []

if (filters) {
// Currently knex cannot digest an array of numbers correctly
// https://github.com/knex/knex/issues/2060
filtersQuery = filters.length > 1 ? `type in (${filters.join(',')})` : `type = ${filters[0]}`
}

if (owner) {
filtersBindings.push(owner)
filtersQuery = filtersQuery !== '' ? filtersQuery + ' and owner = ?' : 'owner = ?'
}

if (status !== 'ALL') {
filtersBindings.push(status)
filtersQuery = filtersQuery !== '' ? filtersQuery + ' and status = ?' : 'status = ?'
}

if (min) {
filtersBindings.push(min)
filtersQuery = filtersQuery !== '' ? filtersQuery + ' and gas_used >= ?' : 'gas_used >= ?'
}

if (max) {
filtersBindings.push(max)
filtersQuery = filtersQuery !== '' ? filtersQuery + ' and gas_used <= ?' : 'gas_used <= ?'
}

const aliasesSubquery = this.knex('identity_aliases')
.select('identity_identifier', this.knex.raw('array_agg(alias) as aliases'))
.groupBy('identity_identifier')
.as('aliases')

const subquery = this.knex('state_transitions')
const filtersSubquery = this.knex('state_transitions')
.select(this.knex('state_transitions').count('hash').as('total_count'), 'state_transitions.hash as tx_hash',
'state_transitions.data as data', 'state_transitions.type as type', 'state_transitions.index as index',
'state_transitions.gas_used as gas_used', 'state_transitions.status as status', 'state_transitions.error as error',
'state_transitions.block_hash as block_hash', 'state_transitions.id as id', 'state_transitions.owner as owner')
.whereRaw(filtersQuery, filtersBindings)
.as('state_transitions')

const subquery = this.knex(filtersSubquery)
.select('tx_hash', 'total_count',
'data', 'type', 'index',
'gas_used', 'status', 'error',
'block_hash', 'id', 'owner',
'identity_identifier', 'aliases'
)
.select(this.knex.raw(`rank() over (order by state_transitions.id ${order}) rank`))
.select('aliases')
.leftJoin(aliasesSubquery, 'aliases.identity_identifier', 'state_transitions.owner')
.as('state_transitions')

Expand All @@ -74,6 +112,7 @@ module.exports = class TransactionsDAO {
.leftJoin('blocks', 'blocks.hash', 'block_hash')
.whereBetween('rank', [fromRank, toRank])
.orderBy('state_transitions.id', order)

const totalCount = rows.length > 0 ? Number(rows[0].total_count) : 0

const resultSet = await Promise.all(rows.map(async (row) => {
Expand Down
19 changes: 19 additions & 0 deletions packages/api/src/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ const schemaTypes = [
type: ['integer', 'null'],
minimum: 0,
maximum: 8
},
filters: {
type: ['array', 'null'],
items: {
type: 'number'
}
},
status: {
type: ['string', 'null'],
enum: ['SUCCESS', 'FAIL', 'ALL']
},
owner: {
type: ['string', 'null']
},
min: {
type: ['number', 'null']
},
max: {
type: ['number', 'null']
}
}
},
Expand Down
168 changes: 167 additions & 1 deletion packages/api/test/integration/transactions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ describe('Transaction routes', () => {
})

const transaction = await fixtures.transaction(knex, {
block_hash: block.hash, data: '{}', type: StateTransitionEnum.DATA_CONTRACT_CREATE, owner: identity.identifier
block_hash: block.hash,
data: '{}',
type: StateTransitionEnum.DATA_CONTRACT_CREATE,
owner: identity.identifier,
gas_used: i * 123
})

transactions.push({ transaction, block })
Expand Down Expand Up @@ -231,6 +235,168 @@ describe('Transaction routes', () => {
assert.deepEqual(expectedTransactions, body.resultSet)
})

it('should return default set of transactions desc with owner', async () => {
const owner = transactions[0].transaction.owner

const { body } = await client.get(`/transactions?order=desc&owner=${owner}`)
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')

assert.equal(body.resultSet.length, 10)
assert.equal(body.pagination.total, transactions.length)
assert.equal(body.pagination.page, 1)
assert.equal(body.pagination.limit, 10)

const expectedTransactions = transactions
.filter(transaction => transaction.transaction.owner === owner)
.sort((a, b) => b.transaction.id - a.transaction.id)
.slice(0, 10)
.map(transaction => ({
blockHash: transaction.block.hash,
blockHeight: transaction.block.height,
data: '{}',
hash: transaction.transaction.hash,
index: transaction.transaction.index,
timestamp: transaction.block.timestamp.toISOString(),
type: transaction.transaction.type,
gasUsed: transaction.transaction.gas_used,
status: transaction.transaction.status,
error: transaction.transaction.error,
owner: {
identifier: transaction.transaction.owner,
aliases: [{
alias: identityAlias.alias,
status: 'ok'
}]
}
}))

assert.deepEqual(expectedTransactions, body.resultSet)
})

it('should return default set of transactions desc with owner and type filter', async () => {
const owner = transactions[0].transaction.owner

const { body } = await client.get(`/transactions?order=desc&owner=${owner}&filters=0&filters=8`)
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')

assert.equal(body.resultSet.length, 10)
assert.equal(body.pagination.total, transactions.length)
assert.equal(body.pagination.page, 1)
assert.equal(body.pagination.limit, 10)

const expectedTransactions = transactions
.filter(transaction => transaction.transaction.owner === owner)
.sort((a, b) => b.transaction.id - a.transaction.id)
.slice(0, 10)
.map(transaction => ({
blockHash: transaction.block.hash,
blockHeight: transaction.block.height,
data: '{}',
hash: transaction.transaction.hash,
index: transaction.transaction.index,
timestamp: transaction.block.timestamp.toISOString(),
type: transaction.transaction.type,
gasUsed: transaction.transaction.gas_used,
status: transaction.transaction.status,
error: transaction.transaction.error,
owner: {
identifier: transaction.transaction.owner,
aliases: [{
alias: identityAlias.alias,
status: 'ok'
}]
}
}))

assert.deepEqual(expectedTransactions, body.resultSet)
})

it('should return default set of transactions desc with owner and type filter and status', async () => {
const owner = transactions[0].transaction.owner

const { body } = await client.get(`/transactions?order=desc&owner=${owner}&filters=1&status=FAIL`)
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')

assert.equal(body.resultSet.length, 1)
assert.equal(body.pagination.total, transactions.length)
assert.equal(body.pagination.page, 1)
assert.equal(body.pagination.limit, 10)

const expectedTransactions = transactions
.filter(transaction => transaction.transaction.status === 'FAIL')
.map(transaction => ({
blockHash: transaction.block.hash,
blockHeight: transaction.block.height,
data: '{}',
hash: transaction.transaction.hash,
index: transaction.transaction.index,
timestamp: transaction.block.timestamp.toISOString(),
type: transaction.transaction.type,
gasUsed: transaction.transaction.gas_used,
status: transaction.transaction.status,
error: transaction.transaction.error,
owner: {
identifier: transaction.transaction.owner,
aliases: [{
alias: identityAlias.alias,
status: 'ok'
}]
}
}))

assert.deepEqual(expectedTransactions, body.resultSet)
})

it('should return default set of transactions desc with owner and type filter and min-max', async () => {
const owner = transactions[0].transaction.owner

const { body } = await client.get(`/transactions?order=desc&owner=${owner}&filters=0&min=246&max=1107`)
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')

assert.equal(body.resultSet.length, 8)
assert.equal(body.pagination.total, transactions.length)
assert.equal(body.pagination.page, 1)
assert.equal(body.pagination.limit, 10)

const expectedTransactions = transactions
.filter(transaction => transaction.transaction.gas_used <= 1107 && transaction.transaction.gas_used >= 246)
.map(transaction => ({
blockHash: transaction.block.hash,
blockHeight: transaction.block.height,
data: '{}',
hash: transaction.transaction.hash,
index: transaction.transaction.index,
timestamp: transaction.block.timestamp.toISOString(),
type: transaction.transaction.type,
gasUsed: transaction.transaction.gas_used,
status: transaction.transaction.status,
error: transaction.transaction.error,
owner: {
identifier: transaction.transaction.owner,
aliases: [{
alias: identityAlias.alias,
status: 'ok'
}]
}
}))

assert.deepEqual(expectedTransactions, body.resultSet)
})

it('should return empty set of transactions desc with owner and type filter', async () => {
const owner = transactions[0].transaction.owner

const { body } = await client.get(`/transactions?order=desc&owner=${owner}&filters=8`)
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')

assert.equal(body.resultSet.length, 0)
})

it('should return be able to walk through pages desc', async () => {
const { body } = await client.get('/transactions?page=3&limit=3&order=desc')
.expect(200)
Expand Down
9 changes: 7 additions & 2 deletions packages/frontend/src/app/api/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,14 @@ Return transaction set paged
Status can be either `SUCCESS` or `FAIL`. In case of error tx, message will appear in the `error` field as Base64 string

* `limit` cannot be more then 100
* `owner` Identity identifier
* `status` can be `SUCCESS`, `FAIL` or `ALL`
* `filters` array of transactions types
* `min` number of min `gas_used`
* `max` number of max `gas_used`

```
GET /transactions?=1&limit=10&order=asc
GET /transactions?=1&limit=10&order=asc&owner=6q9RFbeea73tE31LGMBLFZhtBUX3wZL3TcNynqE18Zgs&filters=0&filters=1&status=ALL&min=0&max=9999999

{
pagination: {
Expand Down Expand Up @@ -1368,8 +1373,8 @@ IDENTITY_CREATE with instantLock
"signature": "2019d90a905092dd3074da3cd42b05abe944d857fc2573e81e1d39a16ba659c00c7b38b88bee46a853c5c30deb9c2ae3abf4fbb781eec12b86a0928ca7b02ced7d",
"documentTypeName": "domain",
"indexName": "parentNameAndLabel",
"proTxHash": 'ad4e38fc81da72d61b14238ee6e5b91915554e24d725718800692d3a863c910b',
"choice": "Abstain",
"proTxHash": 'ad4e38fc81da72d61b14238ee6e5b91915554e24d725718800692d3a863c910b',
"raw": "08005b246080ba64350685fe302d3d790f5bb238cb619920d46230c844f079944a233bb2df460e72e3d59e7fe1c082ab3a5bd9445dd0dd5c4894a6d9f0d9ed9404b5000000e668c659af66aee1e72c186dde7b5b7e0a1d712a09c40d5721f622bf53c5315506646f6d61696e12706172656e744e616d65416e644c6162656c021204646173681203793031010c00412019d90a905092dd3074da3cd42b05abe944d857fc2573e81e1d39a16ba659c00c7b38b88bee46a853c5c30deb9c2ae3abf4fbb781eec12b86a0928ca7b02ced7d"
}
```
Expand Down