Skip to content

Commit

Permalink
feat: add token bundle to transaction operations (cardano-foundation#282
Browse files Browse the repository at this point in the history
)

* feat: add token bundle to transaction operations

* fix: fixed token operations parsing and now old tests work

* fix: token quantity should be string

* feat: add multiassets test for block/transaction endpoint

Co-authored-by: Alan Verbner <alan.verbner@iohk.io>
  • Loading branch information
guido-ta and AlanVerbner committed Jan 18, 2021
1 parent 78fc530 commit 9ca1f65
Show file tree
Hide file tree
Showing 7 changed files with 550 additions and 50 deletions.
159 changes: 132 additions & 27 deletions cardano-rosetta-server/src/server/db/blockchain-repository.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { Pool, QueryResult } from 'pg';
import { Logger } from 'fastify';
import moment from 'moment';
import { Pool, QueryResult } from 'pg';
import {
Block,
FindTransactionWithToken,
GenesisBlock,
PolicyId,
PopulatedTransaction,
Token,
Transaction,
TransactionInOut,
Utxo
} from '../models';
import { hashStringToBuffer, hexFormatter } from '../utils/formatters';
import Queries, {
FindBalance,
FindTransaction,
FindTransactionDelegations,
FindTransactionDeregistrations,
FindTransactionFieldResult,
FindTransactionInOutResult,
FindTransactionRegistrations,
FindTransactionsInputs,
FindTransactionsOutputs,
FindTransactionWithdrawals,
FindTransactionRegistrations,
FindTransactionDeregistrations,
FindTransactionDelegations,
FindUtxo,
FindBalance
FindUtxo
} from './queries/blockchain-queries';
import { Logger } from 'fastify';
import { Block, GenesisBlock, Transaction, PopulatedTransaction, Utxo } from '../models';

export interface BlockchainRepository {
/**
Expand Down Expand Up @@ -134,36 +145,130 @@ const populateTransactionField = <T extends FindTransactionFieldResult>(
return updatedTransactionsMap;
}, transactionsMap);

/**
* Checks if operation has a token
* @param {FindTransactionInOutResult} operation
* @returns operationisFindTransactionWithToken
*/
const hasToken = (operation: FindTransactionInOutResult): operation is FindTransactionWithToken =>
operation.policy !== null && operation.name !== null && operation.quantity !== null;

const tryAddToken = <T extends TransactionInOut>(inOut: T, findResult: FindTransactionInOutResult): T => {
if (hasToken(findResult)) {
const { policy, name, quantity } = findResult;
const policyAsHex = hexFormatter(policy);
const nameAsHex = hexFormatter(name);
const tokenBundle = inOut.tokenBundle ?? { tokens: new Map<PolicyId, Token[]>() };
if (!tokenBundle.tokens.has(policyAsHex)) {
tokenBundle.tokens.set(policyAsHex, []);
}
tokenBundle.tokens.get(policyAsHex)!.push({ name: nameAsHex, quantity });

return {
...inOut,
tokenBundle
};
}
return inOut;
};

/**
* Input and output information to be queries from the db is quite similar and,
* in both cases, multi assets needs to processed.
*
* This function, if properly configured, processes both cases and updates the
* proper collection accordingly.
*
* @param row query result row to be processed
* @param transaction to be populated
* @param getCollection returns the input or output collection
* @param createInstance created a new instance of TransactionInput or TransactionOutput
* @param updateCollection properly sets the collection into the PopulatedTransactionObject
*/
const parseInOutRow = <T extends TransactionInOut, F extends FindTransactionInOutResult>(
row: F,
transaction: PopulatedTransaction,
getCollection: (transaction: PopulatedTransaction) => T[],
createInstance: (queryResult: F) => T,
updateCollection: (populatedTransaction: PopulatedTransaction, collection: T[]) => PopulatedTransaction
): PopulatedTransaction => {
// Get the collection where the input or output is stored
const collection = getCollection(transaction);
// Look for it in case it already exists. This is the case when there are multi-assets associated
// to the same output so several rows will be returned for the same input or output
const index = collection.findIndex(i => i.id === row.id);
if (index !== -1) {
// If it exists, it means that several MA were returned so we need to try to add the token
const updated = tryAddToken(collection[index], row);
// Proper item is updated in a copy of he collection
const newCollection = [...collection];
newCollection[index] = updated;
// Collection is updated in the PopulatedTransaction and then returned
return updateCollection(
{
...transaction
},
newCollection
);
}
// If it's a new input or output create an instance
const newInstance = createInstance(row);
// Then we try to populate it's token if any
const newInOut = tryAddToken(newInstance, row);
return updateCollection(
{
...transaction
},
collection.concat(newInOut)
);
};

/**
* Updates the transaction inputs
*
* @param transaction
* @param populatedTransaction
* @param input
*/
const parseInputsRow = (transaction: PopulatedTransaction, input: FindTransactionsInputs): PopulatedTransaction => ({
...transaction,
inputs: transaction.inputs.concat({
address: input.address,
value: input.value,
sourceTransactionHash: hexFormatter(input.sourceTxHash),
sourceTransactionIndex: input.sourceTxIndex
})
});
const parseInputsRow = (
populatedTransaction: PopulatedTransaction,
input: FindTransactionsInputs
): PopulatedTransaction =>
parseInOutRow(
input,
populatedTransaction,
transaction => transaction.inputs,
queryResult => ({
id: queryResult.id,
address: queryResult.address,
value: queryResult.value,
sourceTransactionHash: hexFormatter(queryResult.sourceTxHash),
sourceTransactionIndex: queryResult.sourceTxIndex
}),
(updatedTransaction, updatedCollection) => ({ ...updatedTransaction, inputs: updatedCollection })
);

/**
* Updates the transaction appending outputs
*
* @param transaction
* @param populatedTransaction
* @param output
*/
const parseOutputsRow = (transaction: PopulatedTransaction, output: FindTransactionsOutputs): PopulatedTransaction => ({
...transaction,
outputs: transaction.outputs.concat({
address: output.address,
value: output.value,
index: output.index
})
});
const parseOutputsRow = (
populatedTransaction: PopulatedTransaction,
output: FindTransactionsOutputs
): PopulatedTransaction =>
parseInOutRow(
output,
populatedTransaction,
transaction => transaction.outputs,
queryResult => ({
id: queryResult.id,
address: queryResult.address,
value: queryResult.value,
index: queryResult.index
}),
(updatedTransaction, updatedCollection) => ({ ...updatedTransaction, outputs: updatedCollection })
);

/**
* Updates the transaction appending withdrawals
Expand Down
33 changes: 25 additions & 8 deletions cardano-rosetta-server/src/server/db/queries/blockchain-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export interface FindTransactionFieldResult {
txHash: Buffer;
}

export interface FindTransactionInOutResult extends FindTransactionFieldResult {
id: number;
address: string;
value: string;
policy?: Buffer;
name?: Buffer;
quantity?: string;
}

// AND (block.block_no = $2 OR (block.block_no is null AND $2 = 0))
// This condition is made because genesis block has block_no = null
// Also, genesis number is 0, thats why $2 = 0.
Expand All @@ -70,19 +79,21 @@ AND (block.block_no = $2 OR (block.block_no is null AND $2 = 0))
AND block.hash = $3
`;

export interface FindTransactionsInputs extends FindTransactionFieldResult {
address: string;
value: string;
export interface FindTransactionsInputs extends FindTransactionInOutResult {
sourceTxHash: Buffer;
sourceTxIndex: number;
}

const findTransactionsInputs = `SELECT
tx_in.id as id,
source_tx_out.address as address,
source_tx_out.value as value,
tx.hash as "txHash",
source_tx.hash as "sourceTxHash",
tx_in.tx_out_index as "sourceTxIndex"
tx_in.tx_out_index as "sourceTxIndex",
source_ma_tx_out.policy as policy,
source_ma_tx_out.name as name,
source_ma_tx_out.quantity as quantity
FROM
tx
JOIN tx_in
Expand All @@ -92,6 +103,8 @@ JOIN tx_out as source_tx_out
AND tx_in.tx_out_index = source_tx_out.index
JOIN tx as source_tx
ON source_tx_out.tx_id = source_tx.id
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)`;

Expand All @@ -105,21 +118,25 @@ WHERE
previous_id IS NULL
LIMIT 1`;

export interface FindTransactionsOutputs extends FindTransactionFieldResult {
address: string;
value: string;
export interface FindTransactionsOutputs extends FindTransactionInOutResult {
index: number;
}

const findTransactionsOutputs = `
SELECT
tx_out.id as id,
address,
value,
tx.hash as "txHash",
index
index,
ma_tx_out.policy as policy,
ma_tx_out.name as name,
ma_tx_out.quantity as quantity
FROM tx
JOIN tx_out
ON tx.id = tx_out.tx_id
LEFT JOIN ma_tx_out
ON ma_tx_out.tx_out_id = tx_out.id
WHERE
tx.hash = ANY ($1)
`;
Expand Down
30 changes: 26 additions & 4 deletions cardano-rosetta-server/src/server/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FindTransactionInOutResult } from './db/queries/blockchain-queries';

export interface Block {
hash: string;
number: number;
Expand All @@ -21,22 +23,42 @@ export interface BlockIdentifier {
hash: string;
}

export interface FindTransactionWithToken extends FindTransactionInOutResult {
policy: Buffer;
name: Buffer;
quantity: string;
}

export type PolicyId = string;

export interface Token {
name: string;
quantity: string;
}

export interface TokenBundle {
tokens: Map<PolicyId, Token[]>;
}

export interface Utxo {
value: string;
transactionHash: string;
index: number;
}

export interface TransactionInput {
export interface TransactionInOut {
id: number;
address: string;
value: string;
tokenBundle?: TokenBundle;
}

export interface TransactionInput extends TransactionInOut {
sourceTransactionHash: string;
sourceTransactionIndex: number;
}

export interface TransactionOutput {
address: string;
value: string;
export interface TransactionOutput extends TransactionInOut {
index: number;
}

Expand Down
Loading

0 comments on commit 9ca1f65

Please sign in to comment.