fix: address txs abi and reported total

* fix: get abi info when querying address txs

* fix: query return type

* fix: only parse abi json if it is a string

* fix: keep stringify

* fix: broken test

* feat: move abi query to materialized view, fix count

* chore: add test with contract call

* chore: create incremental migration for materialized view changes

* fix: undo vscode temp changes

* chore: test contract-call function decoding in `/extended/v1/address/<contract-principal>/transactions`

* chore: test contract-call function decoding in `/extended/v1/address/<standard-principal>/transactions`

Co-authored-by: Matthew Little <zone117x@gmail.com>
This commit is contained in:
Rafael Cárdenas
2021-12-10 13:30:01 -06:00
committed by GitHub
parent 5697f6f477
commit a280073dae
5 changed files with 416 additions and 13 deletions

View File

@@ -663,7 +663,7 @@ function parseDbTxTypeMetadata(dbTx: DbTx | DbMempoolTx): TransactionMetadata {
let functionAbi: ClarityAbiFunction | undefined;
const abi = dbTx.abi;
if (abi) {
const contractAbi: ClarityAbi = JSON.parse(JSON.stringify(abi));
const contractAbi: ClarityAbi = typeof abi === 'string' ? JSON.parse(abi) : abi;
functionAbi = contractAbi.functions.find(fn => fn.name === functionName);
if (!functionAbi) {
throw new Error(

View File

@@ -5267,9 +5267,9 @@ export class PgDataStore
!args.atSingleBlock &&
args.limit + args.offset <= 50;
const resultQuery = useMaterializedView
? await client.query<TxQueryResult & { count: number }>(
? await client.query<ContractTxQueryResult & { count: number }>(
`
SELECT ${TX_COLUMNS}, (COUNT(*) OVER())::integer as count
SELECT ${TX_COLUMNS}, abi, count
FROM latest_contract_txs
WHERE contract_id = $1 AND block_height <= $4
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
@@ -5278,7 +5278,7 @@ export class PgDataStore
`,
[args.stxAddress, args.limit, args.offset, args.blockHeight]
)
: await client.query<TxQueryResult & { count: number }>(
: await client.query<ContractTxQueryResult & { count: number }>(
`
WITH principal_txs AS (
WITH event_txs AS (
@@ -5298,14 +5298,24 @@ export class PgDataStore
ON txs.tx_id = event_txs.tx_id
WHERE txs.canonical = true AND txs.microblock_canonical = true
)
SELECT ${TX_COLUMNS}, (COUNT(*) OVER())::integer as count
SELECT ${TX_COLUMNS},
CASE
WHEN principal_txs.type_id = $5 THEN (
SELECT abi
FROM smart_contracts
WHERE smart_contracts.contract_id = principal_txs.contract_call_contract_id
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
LIMIT 1
)
END as abi,
(COUNT(*) OVER())::integer as count
FROM principal_txs
${args.atSingleBlock ? 'WHERE block_height = $4' : 'WHERE block_height <= $4'}
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
LIMIT $2
OFFSET $3
`,
[args.stxAddress, args.limit, args.offset, args.blockHeight]
[args.stxAddress, args.limit, args.offset, args.blockHeight, DbTxTypeId.ContractCall]
);
const count = resultQuery.rowCount > 0 ? resultQuery.rows[0].count : 0;
const parsed = resultQuery.rows.map(r => this.parseTxQueryResult(r));

View File

@@ -0,0 +1,146 @@
/* eslint-disable @typescript-eslint/camelcase */
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
export const shorthands: ColumnDefinitions | undefined = undefined;
export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.dropMaterializedView('latest_contract_txs', { ifExists: true, cascade: true });
pgm.createMaterializedView('latest_contract_txs', {}, `
WITH contract_txs AS (
SELECT
contract_call_contract_id AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
contract_call_contract_id IS NOT NULL
AND canonical = TRUE
AND microblock_canonical = TRUE
UNION
SELECT
smart_contract_contract_id AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
smart_contract_contract_id IS NOT NULL
AND canonical = TRUE
AND microblock_canonical = TRUE
UNION
SELECT
sender_address AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
sender_address LIKE '%.%'
AND canonical = TRUE
AND microblock_canonical = TRUE
UNION
SELECT
token_transfer_recipient_address AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
token_transfer_recipient_address LIKE '%.%'
AND canonical = TRUE
AND microblock_canonical = TRUE
),
numbered_txs AS (
SELECT
ROW_NUMBER() OVER (
PARTITION BY contract_id
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
) AS r,
COUNT(*) OVER (
PARTITION BY contract_id
)::integer AS count,
contract_txs.*
FROM contract_txs
)
SELECT
numbered_txs.contract_id,
txs.*,
CASE
WHEN txs.type_id = 2 THEN (
SELECT abi
FROM smart_contracts
WHERE smart_contracts.contract_id = txs.contract_call_contract_id
ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC
LIMIT 1
)
END as abi,
numbered_txs.count
FROM numbered_txs
INNER JOIN txs USING (tx_id)
WHERE numbered_txs.r <= 50
`);
pgm.createIndex('latest_contract_txs', 'contract_id');
pgm.createIndex('latest_contract_txs', [
{ name: 'block_height', sort: 'DESC' },
{ name: 'microblock_sequence', sort: 'DESC'},
{ name: 'tx_index', sort: 'DESC' }
]);
}
export async function down(pgm: MigrationBuilder): Promise<void> {
// Go back to the previous materialized view version, otherwise `pgm` complains it can't infer the down migration.
pgm.dropMaterializedView('latest_contract_txs', { ifExists: true, cascade: true });
pgm.createMaterializedView('latest_contract_txs', {}, `
WITH contract_txs AS (
SELECT
contract_call_contract_id AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
contract_call_contract_id IS NOT NULL
AND canonical = TRUE
AND microblock_canonical = TRUE
UNION
SELECT
smart_contract_contract_id AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
smart_contract_contract_id IS NOT NULL
AND canonical = TRUE
AND microblock_canonical = TRUE
UNION
SELECT
sender_address AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
sender_address LIKE '%.%'
AND canonical = TRUE
AND microblock_canonical = TRUE
UNION
SELECT
token_transfer_recipient_address AS contract_id, tx_id,
block_height, microblock_sequence, tx_index
FROM txs
WHERE
token_transfer_recipient_address LIKE '%.%'
AND canonical = TRUE
AND microblock_canonical = TRUE
),
numbered_txs AS (
SELECT
ROW_NUMBER() OVER (
PARTITION BY contract_id
ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC
) AS r,
contract_txs.*
FROM contract_txs
)
SELECT numbered_txs.contract_id, txs.*
FROM numbered_txs
INNER JOIN txs USING (tx_id)
WHERE numbered_txs.r <= 50
`);
pgm.createIndex('latest_contract_txs', 'contract_id');
pgm.createIndex('latest_contract_txs', [
{ name: 'block_height', sort: 'DESC' },
{ name: 'microblock_sequence', sort: 'DESC'},
{ name: 'tx_index', sort: 'DESC' }
]);
}

View File

@@ -1,6 +1,6 @@
import { getEnumDescription } from '../helpers';
import { StacksMessageParsingError, NotImplementedError } from '../errors';
import { ClarityValue, deserializeCV, BufferReader } from '@stacks/transactions';
import { ClarityValue, deserializeCV, BufferReader, serializeCV } from '@stacks/transactions';
const MICROBLOCK_HEADER_SIZE =
// 1-byte version number
@@ -430,6 +430,17 @@ export function readClarityValueArray(input: BufferReader | Buffer): ClarityValu
return values;
}
export function createClarityValueArray(...input: ClarityValue[]): Buffer {
const buffers = new Array<Buffer>(input.length);
for (let i = 0; i < input.length; i++) {
buffers[i] = serializeCV(input[i]);
}
const valueCountBuffer = Buffer.alloc(4);
valueCountBuffer.writeUInt32BE(input.length);
buffers.unshift(valueCountBuffer);
return Buffer.concat(buffers);
}
function readString(reader: BufferReader): string {
const length = reader.readUInt32BE();
const str = reader.readString(length, 'ascii');

View File

@@ -16,9 +16,11 @@ import {
ChainID,
AnchorMode,
intCV,
uintCV,
stringAsciiCV,
} from '@stacks/transactions';
import * as BN from 'bn.js';
import { readTransaction } from '../p2p/tx';
import { createClarityValueArray, readTransaction } from '../p2p/tx';
import { getTxFromDataStore, getBlockFromDataStore } from '../api/controllers/db-controller';
import {
createDbTxFromCoreMsg,
@@ -3877,6 +3879,8 @@ describe('api tests', () => {
const testAddr2 = 'ST1HB64MAJ1MBV4CQ80GF01DZS4T1DSMX20ADCRA4';
const testContractAddr = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world';
const testAddr4 = 'ST3DWSXBPYDB484QXFTR81K4AWG4ZB5XZNFF3H70C';
const testAddr5 = 'ST3V11C6X2EBFN72RMS3B1NYQ1BX98F61GVYRDRXW';
const testAddr6 = 'ST2F8G7616B2F8PYG216BX9AJCHP7YRK7ND7M0ZN3';
const block: DbBlock = {
block_hash: '0x1234',
@@ -4098,6 +4102,88 @@ describe('api tests', () => {
createNFtEvents(testAddr2, testAddr1, 'tendies', 100),
];
const contractJsonAbi = {
maps: [],
functions: [
{
args: [
{ type: 'uint128', name: 'amount' },
{ type: 'string-ascii', name: 'desc' },
],
name: 'test-contract-fn',
access: 'public',
outputs: {
type: {
response: {
ok: 'uint128',
error: 'none',
},
},
},
},
],
variables: [],
fungible_tokens: [],
non_fungible_tokens: [],
};
const contractLogEvent1: DbSmartContractEvent = {
event_index: 4,
tx_id: '0x421234',
tx_index: 0,
block_height: block.block_height,
canonical: true,
event_type: DbEventTypeId.SmartContractLog,
contract_identifier: testContractAddr,
topic: 'some-topic',
value: serializeCV(bufferCVFromString('some val')),
};
const smartContract1: DbSmartContract = {
tx_id: '0x421234',
canonical: true,
block_height: block.block_height,
contract_id: testContractAddr,
source_code: '(some-contract-src)',
abi: JSON.stringify(contractJsonAbi),
};
const contractCall: DbTx = {
tx_id: '0x1232',
tx_index: 5,
anchor_mode: 3,
nonce: 0,
raw_tx: Buffer.alloc(0),
index_block_hash: block.index_block_hash,
block_hash: block.block_hash,
block_height: block.block_height,
burn_block_time: block.burn_block_time,
parent_burn_block_time: 1626122935,
type_id: DbTxTypeId.ContractCall,
coinbase_payload: Buffer.from('coinbase hi'),
status: 1,
raw_result: '0x0100000000000000000000000000000001', // u1
canonical: true,
microblock_canonical: true,
microblock_sequence: I32_MAX,
microblock_hash: '',
parent_index_block_hash: '',
parent_block_hash: '',
post_conditions: Buffer.from([0x01, 0xf5]),
fee_rate: 10n,
sponsored: false,
sponsor_address: testAddr1,
sender_address: testContractAddr,
origin_hash_mode: 1,
event_count: 5,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
contract_call_contract_id: testContractAddr,
contract_call_function_name: 'test-contract-fn',
contract_call_function_args: createClarityValueArray(uintCV(123456), stringAsciiCV('hello')),
abi: JSON.stringify(contractJsonAbi),
};
const dataStoreTxs = txs.map(dbTx => {
return {
tx: dbTx,
@@ -4117,6 +4203,30 @@ describe('api tests', () => {
stxEvents: events,
ftEvents: ftEvents,
nftEvents: nftEvents.flat(),
contractLogEvents: [contractLogEvent1],
smartContracts: [smartContract1],
names: [],
namespaces: [],
});
dataStoreTxs.push({
tx: contractCall,
stxLockEvents: [],
stxEvents: [
{
canonical: true,
event_type: DbEventTypeId.StxAsset,
asset_event_type_id: DbAssetEventTypeId.Transfer,
event_index: 0,
tx_id: contractCall.tx_id,
tx_index: contractCall.tx_index,
block_height: contractCall.block_height,
amount: 4321n,
sender: testAddr5,
recipient: testAddr6,
},
],
ftEvents: [],
nftEvents: [],
contractLogEvents: [],
smartContracts: [],
names: [],
@@ -4186,10 +4296,10 @@ describe('api tests', () => {
expect(fetchAddrBalance2.type).toBe('application/json');
const expectedResp2 = {
stx: {
balance: '101',
balance: '91',
total_sent: '15',
total_received: '1350',
total_fees_sent: '1234',
total_fees_sent: '1244',
total_miner_rewards_received: '0',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
@@ -4221,10 +4331,10 @@ describe('api tests', () => {
expect(fetchAddrStxBalance1.status).toBe(200);
expect(fetchAddrStxBalance1.type).toBe('application/json');
const expectedStxResp1 = {
balance: '101',
balance: '91',
total_sent: '15',
total_received: '1350',
total_fees_sent: '1234',
total_fees_sent: '1244',
total_miner_rewards_received: '0',
burnchain_lock_height: 0,
burnchain_unlock_height: 0,
@@ -4361,7 +4471,7 @@ describe('api tests', () => {
const expectedResp4 = {
limit: 20,
offset: 0,
total: 3,
total: 4,
results: [
{
tx_id: '0x12340005',
@@ -4403,6 +4513,62 @@ describe('api tests', () => {
execution_cost_write_count: 0,
execution_cost_write_length: 0,
},
{
tx_id: '0x1232',
tx_status: 'success',
tx_result: {
hex: '0x0100000000000000000000000000000001', // u1
repr: 'u1',
},
tx_type: 'contract_call',
fee_rate: '10',
is_unanchored: false,
nonce: 0,
anchor_mode: 'any',
sender_address: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
sponsor_address: 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1',
sponsored: false,
post_condition_mode: 'allow',
post_conditions: [],
block_hash: '0x1234',
block_height: 1,
burn_block_time: 39486,
burn_block_time_iso: '1970-01-01T10:58:06.000Z',
canonical: true,
microblock_canonical: true,
microblock_hash: '',
microblock_sequence: I32_MAX,
parent_block_hash: '',
parent_burn_block_time: 1626122935,
parent_burn_block_time_iso: '2021-07-12T20:48:55.000Z',
tx_index: 5,
contract_call: {
contract_id: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
function_name: 'test-contract-fn',
function_signature:
'(define-public (test-contract-fn (amount uint) (desc string-ascii)))',
function_args: [
{
hex: '0x010000000000000000000000000001e240',
name: 'amount',
repr: 'u123456',
type: 'uint',
},
{
hex: '0x0d0000000568656c6c6f',
name: 'desc',
repr: '"hello"',
type: 'string-ascii',
},
],
},
event_count: 5,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
},
{
tx_id: '0x12340003',
tx_status: 'success',
@@ -4486,6 +4652,76 @@ describe('api tests', () => {
],
};
expect(JSON.parse(fetchAddrTx1.text)).toEqual(expectedResp4);
const fetchAddrTx2 = await supertest(api.server).get(
`/extended/v1/address/${testAddr5}/transactions`
);
expect(fetchAddrTx2.status).toBe(200);
expect(fetchAddrTx2.type).toBe('application/json');
const expectedResp5 = {
limit: 20,
offset: 0,
total: 1,
results: [
{
tx_id: '0x1232',
tx_status: 'success',
tx_result: {
hex: '0x0100000000000000000000000000000001', // u1
repr: 'u1',
},
tx_type: 'contract_call',
fee_rate: '10',
is_unanchored: false,
nonce: 0,
anchor_mode: 'any',
sender_address: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
sponsor_address: 'ST3J8EVYHVKH6XXPD61EE8XEHW4Y2K83861225AB1',
sponsored: false,
post_condition_mode: 'allow',
post_conditions: [],
block_hash: '0x1234',
block_height: 1,
burn_block_time: 39486,
burn_block_time_iso: '1970-01-01T10:58:06.000Z',
canonical: true,
microblock_canonical: true,
microblock_hash: '',
microblock_sequence: I32_MAX,
parent_block_hash: '',
parent_burn_block_time: 1626122935,
parent_burn_block_time_iso: '2021-07-12T20:48:55.000Z',
tx_index: 5,
contract_call: {
contract_id: 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world',
function_name: 'test-contract-fn',
function_signature:
'(define-public (test-contract-fn (amount uint) (desc string-ascii)))',
function_args: [
{
hex: '0x010000000000000000000000000001e240',
name: 'amount',
repr: 'u123456',
type: 'uint',
},
{
hex: '0x0d0000000568656c6c6f',
name: 'desc',
repr: '"hello"',
type: 'string-ascii',
},
],
},
event_count: 5,
execution_cost_read_count: 0,
execution_cost_read_length: 0,
execution_cost_runtime: 0,
execution_cost_write_count: 0,
execution_cost_write_length: 0,
},
],
};
expect(JSON.parse(fetchAddrTx2.text)).toEqual(expectedResp5);
});
test('list contract log events', async () => {