Skip to content

Commit

Permalink
Feat/search transactions (cardano-foundation#426)
Browse files Browse the repository at this point in the history
* feat: add queries when searching by operation type

* feat: add search transactions with type filter

* feat: add filters to search transactions query

* test: adapt network options test

* refactor: move order by sentence to type queries

* Add coin filter to search transactions (cardano-foundation#428)

* feat: add coin filter to search transactions

* feat: add operator enum

* refactor: change coin query to not get hash and index by query param

* feat: add currency filter and refact old createCompose method

* feat: add coin filter to type query

* feat: address filter to search API

* feat: handle or operator when searching txs

* refactor: improve total count query for requests with or operator

* test: add test set up for search transactions endpoint

* test: add tests for currency, coin and tx hash filters

* test: add address and account identifier filter tests

* test: add test cases for search txs with type filter

* test: add tests for requests with composed filters at search api

* doc: add doc for search api

* fix: remove duplicated test

* test: resolve test conflicts

* doc: add how it works documentation for search api

* refactor: add disable search api default value
  • Loading branch information
jvieiro committed Sep 16, 2021
1 parent f9e4fd4 commit faf3652
Show file tree
Hide file tree
Showing 26 changed files with 12,998 additions and 74 deletions.
2 changes: 2 additions & 0 deletions cardano-rosetta-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ DEFAULT_RELATIVE_TTL=1000
EXEMPTION_TYPES_PATH="/etc/node/exemptions.json"
# request payload limit
BODY_LIMIT=1048576
# disables Search Api if its true
DISABLE_SEARCH_API=false
```

### Install packages from offline cache
Expand Down
44 changes: 29 additions & 15 deletions cardano-rosetta-server/src/server/controllers/controllers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { FastifyRequest } from 'fastify';
import { Services } from '../services/services';
import blockController, { BlockController } from './block-controller';
import accountController, { AccountController } from './account-controller';
import networkController, { NetworkController } from './network-controller';
import constructionController, { ConstructionController } from './construction-controller';
import searchController, { SearchController } from './search-controller';
import { CardanoCli } from '../utils/cardano/cli/cardanonode-cli';
import { CardanoNode } from '../utils/cardano/cli/cardano-node';

export interface Controllers extends BlockController, AccountController, NetworkController, ConstructionController {}
export interface Controllers extends BlockController, AccountController, NetworkController, ConstructionController {
searchTransactions?(
request: FastifyRequest<unknown, unknown, unknown, unknown, Components.Schemas.SearchTransactionsRequest>
): Promise<Components.Schemas.SearchTransactionsResponse | Components.Schemas.Error>;
}

/**
* Configures all the controllers required by the app
Expand All @@ -17,17 +23,25 @@ export const configure = (
services: Services,
cardanoCli: CardanoCli,
cardanoNode: CardanoNode,
networkId: string,
pageSize: number
): Controllers => ({
...blockController(services.blockService, services.cardanoService, pageSize, services.networkService),
...accountController(services.blockService, services.cardanoService, services.networkService),
...networkController(services.networkService, cardanoNode),
...constructionController(
services.constructionService,
services.cardanoService,
cardanoCli,
services.networkService,
services.blockService
)
});
pageSize: number,
disableSearchApi = false
): Controllers => {
const toReturn = {
...blockController(services.blockService, services.cardanoService, pageSize, services.networkService),
...accountController(services.blockService, services.cardanoService, services.networkService),
...networkController(services.networkService, cardanoNode),
...constructionController(
services.constructionService,
services.cardanoService,
cardanoCli,
services.networkService,
services.blockService
)
};
if (!disableSearchApi)
return {
...toReturn,
...searchController(services.blockService, services.cardanoService, pageSize, services.networkService)
};
return toReturn;
};
58 changes: 58 additions & 0 deletions cardano-rosetta-server/src/server/controllers/search-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { FastifyRequest } from 'fastify';
import { withNetworkValidation } from '../controllers/controllers-helper';
import { BlockService } from '../services/block-service';
import { CardanoService } from '../services/cardano-services';
import { NetworkService } from '../services/network-service';
import { mapToSearchTransactionsResponse } from '../utils/index-mapper';

export interface SearchController {
searchTransactions(
request: FastifyRequest<unknown, unknown, unknown, unknown, Components.Schemas.SearchTransactionsRequest>
): Promise<Components.Schemas.SearchTransactionsResponse | Components.Schemas.Error>;
}

const configure = (
blockService: BlockService,
cardanoService: CardanoService,
PAGE_SIZE: number,
networkService: NetworkService
): SearchController => ({
searchTransactions: async request =>
withNetworkValidation(
request.body.network_identifier,
request,
async () => {
const logger = request.log;
const searchTransactionsRequest = request.body;
logger.debug({ searchTransactionsRequest }, '[searchTransactions] Request received');
let limit = searchTransactionsRequest.limit;
if (limit === undefined || limit > PAGE_SIZE) {
logger.info('[searchTransactions] Setting limit value ', PAGE_SIZE);
limit = PAGE_SIZE;
}
let offset = searchTransactionsRequest.offset;
if (offset === undefined || offset === null) {
offset = 0;
}
const filters = { ...searchTransactionsRequest, limit, offset };
const transactionsFound = await blockService.findTransactionsByFilters(logger, filters, limit, offset);
logger.info('[searchTransactions] Looking for transactions full data');
const transactions = await blockService.fillTransactions(logger, transactionsFound.transactions);
const { poolDeposit } = await cardanoService.getDepositParameters(logger);
const parameters = {
transactions,
poolDeposit,
offset,
limit,
totalCount: transactionsFound.totalCount
};
const toReturn = mapToSearchTransactionsResponse(parameters);
logger.debug(toReturn, '[searchTransactions] About to return ');
return toReturn;
},
request.log,
networkService
)
});

export default configure;
102 changes: 96 additions & 6 deletions cardano-rosetta-server/src/server/db/blockchain-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,31 @@ import moment from 'moment';
import { Pool, QueryResult } from 'pg';
import {
Block,
FindTransaction,
FindTransactionWithToken,
GenesisBlock,
PolicyId,
PopulatedTransaction,
Token,
Transaction,
TransactionCount,
TransactionInOut,
TransactionPoolRegistrations,
Utxo,
MaBalance
MaBalance,
TotalCount,
SearchFilters
} from '../models';
import { LinearFeeParameters } from '../services/cardano-services';
import { hexStringToBuffer, hexFormatter, isEmptyHexString, remove0xPrefix } from '../utils/formatters';
import {
hexStringToBuffer,
hexFormatter,
isEmptyHexString,
remove0xPrefix,
coinIdentifierFormatter
} from '../utils/formatters';
import Queries, {
FindBalance,
FindTransaction,
FindTransactionDelegations,
FindTransactionDeregistrations,
FindTransactionFieldResult,
Expand All @@ -35,8 +44,17 @@ import Queries, {
FindUtxo,
FindMaBalance
} from './queries/blockchain-queries';
import { CatalystDataIndexes, CatalystSigIndexes } from '../utils/constants';
import { isVoteDataValid, isVoteSignatureValid } from '../utils/validations';
import SearchQueries from './queries/search-transactions-queries';
import { ADA, CatalystDataIndexes, CatalystSigIndexes, OperatorType } from '../utils/constants';
import {
isVoteDataValid,
isVoteSignatureValid,
validateAndGetStatus,
validateTransactionCoinMatch,
validateAccountIdAddressMatch,
validateOperationType,
validateCurrencies
} from '../utils/validations';
import { getAddressFromHexString } from '../utils/cardano/addresses';

export interface BlockchainRepository {
Expand Down Expand Up @@ -116,6 +134,16 @@ export interface BlockchainRepository {
* @param blockIdentifier block information, when value is not undefined balance should be count till requested block
*/
findBalanceByAddressAndBlock(logger: Logger, address: string, blockHash: string): Promise<string>;
/**
* Returns if any, the transactions matches the requested filters
* @param filters conditions to filter transactions by
*/
findTransactionsByFilters(
logger: Logger,
parameters: Components.Schemas.SearchTransactionsRequest,
limit: number,
offset: number
): Promise<TransactionCount>;
}

/**
Expand All @@ -124,13 +152,19 @@ export interface BlockchainRepository {
const parseTransactionRows = (result: QueryResult<FindTransaction>): Transaction[] =>
result.rows.map(row => ({
hash: hexFormatter(row.hash),
blockHash: row.blockHash && hexFormatter(row.blockHash),
blockHash: hexFormatter(row.blockHash),
blockNo: row.blockNo,
fee: row.fee,
size: row.size,
scriptSize: row.scriptSize,
validContract: row.validContract
}));

/**
* Maps from a total count query result to a number
*/
const parseTotalCount = (result: QueryResult<TotalCount>): number => result.rows[0].totalCount;

/**
* Creates a map of transactions where they key is the transaction hash
*
Expand All @@ -144,6 +178,7 @@ const mapTransactionsToDict = (transactions: Transaction[]): TransactionsMap =>
[hash]: {
hash,
blockHash: transaction.blockHash,
blockNo: transaction.blockNo,
fee: transaction.fee,
size: transaction.size,
scriptSize: transaction.scriptSize,
Expand Down Expand Up @@ -688,5 +723,60 @@ export const configure = (databaseInstance: Pool): BlockchainRepository => ({
logger.debug(`[findBalanceByAddressAndBlock] Found a balance of ${result.rows[0].balance}`);

return result.rows[0].balance;
},
async findTransactionsByFilters(
logger: Logger,
conditions: Components.Schemas.SearchTransactionsRequest,
limit: number,
offset: number
): Promise<TransactionCount> {
logger.debug('[findTransactionsByConditions] Conditions received to run the query ', {
conditions
});
const { type, status, success, operator, currency } = conditions;
const coinIdentifier = coinIdentifierFormatter(conditions?.coin_identifier?.identifier);
const transactionHash = conditions?.transaction_identifier?.hash;
const conditionsToQueryBy: SearchFilters = {
maxBlock: conditions?.max_block,
operator: operator ? operator : OperatorType.AND,
type,
coinIdentifier,
status: validateAndGetStatus(status, success),
transactionHash,
limit,
offset,
address: conditions.address || conditions.account_identifier?.address
};
validateTransactionCoinMatch(transactionHash, coinIdentifier);
validateAccountIdAddressMatch(conditions.address, conditions.account_identifier);
type && validateOperationType(type);
if (currency && currency.symbol !== ADA) {
validateCurrencies([currency]);
const currencyId = {
symbol: isEmptyHexString(currency.symbol) ? '' : currency.symbol,
policy: currency.metadata?.policyId
};
conditionsToQueryBy.currencyIdentifier = currencyId;
}
const { data: dataQuery, count: totalCountQuery } = SearchQueries.generateComposedQuery(conditionsToQueryBy);
logger.debug('[findTransactionsByConditions] About to search transactions');
const parameters = [];
if (transactionHash || coinIdentifier)
parameters.push(transactionHash ? hexStringToBuffer(transactionHash) : hexStringToBuffer(coinIdentifier!.hash));
const result: QueryResult<FindTransaction> = await databaseInstance.query(dataQuery, parameters);
logger.debug(`[findTransactionsByConditions] Found ${result.rowCount} transactions`);
const totalCountResult: QueryResult<TotalCount> = await databaseInstance.query(totalCountQuery, parameters);
const totalCount = parseTotalCount(totalCountResult);
logger.debug('[findTransactionsByConditions] Total count obtained ', totalCount);
if (result.rows.length > 0) {
return {
transactions: parseTransactionRows(result),
totalCount
};
}
return {
transactions: [],
totalCount
};
}
});
17 changes: 2 additions & 15 deletions cardano-rosetta-server/src/server/db/queries/blockchain-queries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CatalystLabels } from '../../utils/constants';
import { CurrencyId } from '../../models';

const findBlock = (blockNumber?: number, blockHash?: string): string => `
SELECT
Expand Down Expand Up @@ -32,20 +33,6 @@ WHERE
LIMIT 1
`;

export interface FindTransaction {
hash: Buffer;
blockHash: Buffer;
fee: string;
size: number;
scriptSize: number;
validContract: boolean;
}

export interface CurrencyId {
symbol: string;
policy: string;
}

const currenciesQuery = (currencies: CurrencyId[]): string =>
`SELECT tx_out_id
FROM ma_tx_out
Expand Down Expand Up @@ -131,7 +118,7 @@ LEFT JOIN ma_tx_out as source_ma_tx_out
ON source_ma_tx_out.tx_out_id = source_tx_out.id
WHERE
tx.hash = ANY ($1)
ORDER BY policy, name`;
ORDER BY policy, name, id`;

const findGenesisBlock = `
SELECT
Expand Down
Loading

0 comments on commit faf3652

Please sign in to comment.