Skip to content

Commit

Permalink
Feat/206 account balance staking support (cardano-foundation#213)
Browse files Browse the repository at this point in the history
* feat: Handle stake accounts in account/balance

Refs cardano-foundation#206

* test: add stake account test

Refs cardano-foundation#206

* refactor: refact findUtxoByAddressAndBlock function

change name for a more generic one and handle properly stake accounts

Refs cardano-foundation#206

* test: add data set for rewards

Refs cardano-foundation#206

* fix: fix account balance query

* test: add stake account balance tests

* refactor: create isStakeAddress function

* test: adapt fixture data for account balance

* fix: fix account balance query
Use block id as filter
  • Loading branch information
jvieiro authored and AlanVerbner committed Nov 27, 2020
1 parent 1373ff6 commit 4e57b2e
Show file tree
Hide file tree
Showing 13 changed files with 68,596 additions and 35 deletions.
Expand Up @@ -20,7 +20,7 @@ const configure = (blockService: BlockService, networkId: string): AccountContro
const accountAddress = accountBalanceRequest.account_identifier.address;
logger.debug({ accountBalanceRequest: request }, '[accountBalance] Request received');
logger.info(`[accountBalance] Looking for block: ${accountBalanceRequest.block_identifier || 'latest'}`);
const blockUtxos = await blockService.findUtxoByAddressAndBlock(
const blockUtxos = await blockService.findBalanceDataByAddressAndBlock(
logger,
accountBalanceRequest.account_identifier.address,
accountBalanceRequest.block_identifier?.index,
Expand Down
24 changes: 23 additions & 1 deletion cardano-rosetta-server/src/server/db/blockchain-repository.ts
Expand Up @@ -7,8 +7,9 @@ import Queries, {
FindTransactionsInputs,
FindTransactionsOutputs,
FindTransactionWithdrawals,
FindTransactionRegistrations,
FindUtxo,
FindTransactionRegistrations
FindBalance
} from './queries/blockchain-queries';
import { Logger } from 'fastify';
import { Block, GenesisBlock, Transaction, PopulatedTransaction, Utxo } from '../models';
Expand Down Expand Up @@ -66,6 +67,13 @@ export interface BlockchainRepository {
* @param blockIdentifier block information, when value is not undefined balance should be count till requested block
*/
findUtxoByAddressAndBlock(logger: Logger, address: string, blockHash: string): Promise<Utxo[]>;

/**
* Returns the balance for address till block identified by blockIdentifier if present, else the last
* @param address account's address to count balance
* @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>;
}

/**
Expand Down Expand Up @@ -341,5 +349,19 @@ export const configure = (databaseInstance: Pool): BlockchainRepository => ({
transactionHash: hexFormatter(utxo.txHash),
index: utxo.index
}));
},
async findBalanceByAddressAndBlock(logger: Logger, address, blockHash): Promise<string> {
const parameters = [address, hashStringToBuffer(blockHash)];
logger.debug(
{ address, blockHash },
'[findBalanceByAddressAndBlock] About to run findBalanceByAddressAndBlock query with parameters:'
);
const result: QueryResult<FindBalance> = await databaseInstance.query(
Queries.findBalanceByAddressAndBlock,
parameters
);
logger.debug(`[findBalanceByAddressAndBlock] Found a balance of ${result.rows[0].balance}`);

return result.rows[0].balance;
}
});
21 changes: 19 additions & 2 deletions cardano-rosetta-server/src/server/db/queries/blockchain-queries.ts
Expand Up @@ -192,7 +192,6 @@ ${selectFields}
WHERE
tx_out.address = $1 AND
tx_in_tx.id IS NULL
`;

const selectUtxoDetail = `SELECT
Expand All @@ -202,6 +201,23 @@ const selectUtxoDetail = `SELECT

const findUtxoByAddressAndBlock = findUtxoFieldsByAddressAndBlock(selectUtxoDetail);

const findBalanceByAddressAndBlock = `SELECT (SELECT COALESCE(SUM(r.amount),0)
FROM reward r
JOIN stake_address ON
stake_address.id = r.addr_id
JOIN block ON
block.id = r.block_id
WHERE stake_address.view = $1
AND block.id <= (SELECT id FROM block WHERE hash = $2))-
(SELECT COALESCE(SUM(w.amount),0)
FROM withdrawal w
JOIN tx ON tx.id = w.tx_id AND
tx.block_id <= (SELECT id FROM block WHERE hash = $2)
JOIN stake_address ON stake_address.id = w.addr_id
WHERE stake_address.view = $1)
AS balance
`;

const Queries = {
findBlock,
findTransactionsByBlock,
Expand All @@ -212,7 +228,8 @@ const Queries = {
findTransactionRegistrations,
findLatestBlockNumber,
findGenesisBlock,
findUtxoByAddressAndBlock
findUtxoByAddressAndBlock,
findBalanceByAddressAndBlock
};

export default Queries;
5 changes: 5 additions & 0 deletions cardano-rosetta-server/src/server/models.ts
Expand Up @@ -72,3 +72,8 @@ export interface BlockUtxos {
block: Block;
utxos: Utxo[];
}

export interface BalanceAtBlock {
block: Block;
balance: string;
}
33 changes: 26 additions & 7 deletions cardano-rosetta-server/src/server/services/block-service.ts
@@ -1,7 +1,8 @@
import { Logger } from 'fastify';
import { Block, BlockUtxos, GenesisBlock, Transaction, PopulatedTransaction } from '../models';
import { Block, BlockUtxos, BalanceAtBlock, GenesisBlock, Transaction, PopulatedTransaction } from '../models';
import { ErrorFactory } from '../utils/errors';
import { BlockchainRepository } from '../db/blockchain-repository';
import { CardanoService } from './cardano-services';

/* eslint-disable camelcase */
export interface BlockService {
Expand Down Expand Up @@ -68,10 +69,15 @@ export interface BlockService {
* @param number
* @param hash
*/
findUtxoByAddressAndBlock(logger: Logger, address: string, number?: number, hash?: string): Promise<BlockUtxos>;
findBalanceDataByAddressAndBlock(
logger: Logger,
address: string,
number?: number,
hash?: string
): Promise<BlockUtxos | BalanceAtBlock>;
}

const configure = (repository: BlockchainRepository): BlockService => ({
const configure = (repository: BlockchainRepository, cardanoService: CardanoService): BlockService => ({
async findBlock(logger, number, hash) {
logger.info({ number, hash }, '[findBlock] Looking for block:');
// cardano doesn't have block zero but we need to map it to genesis
Expand Down Expand Up @@ -133,17 +139,30 @@ const configure = (repository: BlockchainRepository): BlockService => ({
logger.debug({ genesisBlock }, '[getGenesisBlock] Returning genesis block');
return genesisBlock;
},
async findUtxoByAddressAndBlock(logger, address, number, hash) {
async findBalanceDataByAddressAndBlock(logger, address, number, hash) {
const block = await this.findBlock(logger, number, hash);
if (block === null) {
logger.error('[findUtxoByAddressAndBlock] Block not found');
logger.error('[findBalanceDataByAddressAndBlock] Block not found');
throw ErrorFactory.blockNotFoundError();
}
logger.info(`[findUtxoByAddressAndBlock] Looking for utxos for address ${address} and block ${block.hash}`);

logger.info(`[findBalanceDataByAddressAndBlock] Looking for utxos for address ${address} and block ${block.hash}`);
if (cardanoService.isStakeAddress(address)) {
logger.debug(`[findBalanceDataByAddressAndBlock] About to get balance for ${address}`);
const balance = await repository.findBalanceByAddressAndBlock(logger, address, block.hash);
logger.debug(
balance,
`[findBalanceDataByAddressAndBlock] Found stake balance of ${balance} for address ${address}`
);
return {
block,
balance
};
}
const utxoDetails = await repository.findUtxoByAddressAndBlock(logger, address, block.hash);
logger.debug(
utxoDetails,
`[findUtxoByAddressAndBlock] Found ${utxoDetails.length} utxo details for address ${address}`
`[findBalanceDataByAddressAndBlock] Found ${utxoDetails.length} utxo details for address ${address}`
);
return {
block,
Expand Down
26 changes: 24 additions & 2 deletions cardano-rosetta-server/src/server/services/cardano-services.ts
Expand Up @@ -20,7 +20,9 @@ import {
stakingOperations,
AddressType,
SIGNATURE_LENGTH,
PUBLIC_KEY_BYTES_LENGTH
PUBLIC_KEY_BYTES_LENGTH,
stakeType,
PREFIX_LENGTH
} from '../utils/constants';

export enum NetworkIdentifier {
Expand Down Expand Up @@ -87,6 +89,20 @@ export interface CardanoService {
*/
getHashOfSignedTransaction(logger: Logger, signedTransaction: string): string;

/**
* This function returns a the address prefix based on a string encoded one
*
* @param address to be parsed
*/
getPrefixFromAddress(address: string): string;

/**
* Returns true if the address's prefix belongs to stake address
*
* @param address
*/
isStakeAddress(address: string): boolean;

/**
* Creates an unsigned transaction for the given operation.
*
Expand Down Expand Up @@ -477,7 +493,13 @@ const configure = (linearFeeParameters: LinearFeeParameters): CardanoService =>
return null;
}
},

getPrefixFromAddress(address) {
return address.slice(0, PREFIX_LENGTH);
},
isStakeAddress(address) {
const addressPrefix = this.getPrefixFromAddress(address);
return [stakeType.STAKE as string, stakeType.STAKE_TEST as string].some(type => addressPrefix.includes(type));
},
getHashOfSignedTransaction(logger, signedTransaction) {
try {
logger.info(`[getHashOfSignedTransaction] About to hash signed transaction ${signedTransaction}`);
Expand Down
2 changes: 1 addition & 1 deletion cardano-rosetta-server/src/server/services/services.ts
Expand Up @@ -22,8 +22,8 @@ export const configure = (
DEFAULT_RELATIVE_TTL: number,
linearFeeParameters: LinearFeeParameters
): Services => {
const blockServiceInstance = blockService(repositories.blockchainRepository);
const cardanoServiceInstance = cardanoService(linearFeeParameters);
const blockServiceInstance = blockService(repositories.blockchainRepository, cardanoServiceInstance);
return {
blockService: blockServiceInstance,
constructionService: constructionService(blockServiceInstance, DEFAULT_RELATIVE_TTL),
Expand Down
6 changes: 6 additions & 0 deletions cardano-rosetta-server/src/server/utils/constants.ts
Expand Up @@ -10,11 +10,17 @@ export const ADA_DECIMALS = 6;
export const VIN = 'Vin';
export const VOUT = 'Vout';
export const SIGNATURE_TYPE = 'ed25519';
export const PREFIX_LENGTH = 10;

// Nibbles
export const SIGNATURE_LENGTH = 128;
export const PUBLIC_KEY_BYTES_LENGTH = 64;

export enum stakeType {
STAKE = 'stake',
STAKE_TEST = 'stake_test'
}

export enum operationType {
INPUT = 'input',
OUTPUT = 'output',
Expand Down
32 changes: 22 additions & 10 deletions cardano-rosetta-server/src/server/utils/data-mapper.ts
Expand Up @@ -4,7 +4,7 @@ import cbor from 'cbor';
import { NetworkIdentifier } from '../services/cardano-services';
import { NetworkStatus } from '../services/network-service';
import { ADA, ADA_DECIMALS, CARDANO, MAINNET, operationType, SIGNATURE_TYPE, SUCCESS_STATUS } from './constants';
import { Block, BlockUtxos, Network, PopulatedTransaction, Utxo } from '../models';
import { Block, BlockUtxos, BalanceAtBlock, Network, PopulatedTransaction, Utxo } from '../models';

const COIN_SPENT_ACTION = 'coin_spent';
const COIN_CREATED_ACTION = 'coin_created';
Expand Down Expand Up @@ -193,20 +193,32 @@ const parseUtxoDetails = (utxoDetails: Utxo[]): Components.Schemas.Coin[] =>

/**
* Generates an AccountBalance response object
* @param blockUtxos
* @param blockBalanceData
* @param accountAddress
*/
export const mapToAccountBalanceResponse = (blockUtxos: BlockUtxos): Components.Schemas.AccountBalanceResponse => {
const balanceForAddress = blockUtxos.utxos
.reduce((acum, current) => acum + BigInt(current.value), BigInt(0))
.toString();
export const mapToAccountBalanceResponse = (
blockBalanceData: BlockUtxos | BalanceAtBlock
): Components.Schemas.AccountBalanceResponse => {
// FIXME: handle this in a better way
if (blockBalanceData.hasOwnProperty('utxos')) {
const balanceForAddress = (blockBalanceData as BlockUtxos).utxos
.reduce((acum, current) => acum + BigInt(current.value), BigInt(0))
.toString();
return {
block_identifier: {
index: blockBalanceData.block.number,
hash: blockBalanceData.block.hash
},
balances: [mapAmount(balanceForAddress)],
coins: parseUtxoDetails((blockBalanceData as BlockUtxos).utxos)
};
}
return {
block_identifier: {
index: blockUtxos.block.number,
hash: blockUtxos.block.hash
index: blockBalanceData.block.number,
hash: blockBalanceData.block.hash
},
balances: [mapAmount(balanceForAddress)],
coins: parseUtxoDetails(blockUtxos.utxos)
balances: [mapAmount((blockBalanceData as BalanceAtBlock).balance)]
};
};

Expand Down
60 changes: 60 additions & 0 deletions cardano-rosetta-server/test/e2e/account/account-api.test.ts
Expand Up @@ -287,4 +287,64 @@ describe('/account/balance endpoint', () => {
coins: []
});
});
test('should return 0 for the balance of stake account at block with no earned rewards', async () => {
const response = await server.inject({
method: 'post',
url: ACCOUNT_BALANCE_ENDPOINT,
payload: generatePayload(
CARDANO,
'mainnet',
'stake1uyqq2a22arunrft3k9ehqc7yjpxtxjmvgndae80xw89mwyge9skyp',
4490560,
'6fca1ba5a6ccd557968140e2586f2fed947785f4ef15bac0090657db80c68386'
)
});
expect(response.statusCode).toEqual(StatusCodes.OK);
expect(response.json()).toEqual({
block_identifier: {
index: 4490560,
hash: '6fca1ba5a6ccd557968140e2586f2fed947785f4ef15bac0090657db80c68386'
},
balances: [
{
value: '0',
currency: {
decimals: 6,
symbol: 'ADA'
}
}
]
});
});
// At this point the total amount of rewards is 112588803 (at block 4597956) + 111979582 (at block 4619398)
// and the total amount of withdrawals is 112588803 (at block 4598038)
test('should sum all rewards and subtract all withdrawals till block 4876885', async () => {
const response = await server.inject({
method: 'post',
url: ACCOUNT_BALANCE_ENDPOINT,
payload: generatePayload(
CARDANO,
'mainnet',
'stake1uyqq2a22arunrft3k9ehqc7yjpxtxjmvgndae80xw89mwyge9skyp',
4876885,
'8633863f0fc42a0436c2754ce70684a902e2f7b2349a080321e5c3f5e11fd184'
)
});
expect(response.statusCode).toEqual(StatusCodes.OK);
expect(response.json()).toEqual({
block_identifier: {
index: 4876885,
hash: '8633863f0fc42a0436c2754ce70684a902e2f7b2349a080321e5c3f5e11fd184'
},
balances: [
{
value: '111979582',
currency: {
decimals: 6,
symbol: 'ADA'
}
}
]
});
});
});

0 comments on commit 4e57b2e

Please sign in to comment.