Skip to content

Commit

Permalink
transaction history: add input addresses, metadata and slots filter
Browse files Browse the repository at this point in the history
update .nvmrc

make input addresses and metadata optional (as an arg)

reformat slotBoundsPagination.sql

Co-authored-by: Sebastien Guillemot <[email protected]>

add more comments in the code

update slotBOundsPagination.queries.ts after reformatting

update .nvmrc to lts/hydrogen

optimize slot filter tx filter query

add support to lower slot limit as -1 to get the full range

update openapi.json

update tx_count change to cml multiera after rebase

fix genesis_block task: missing tx_count

assetUtxo and delegationForPool: paginate by tx,block

refactor common slotLimits functionality

migrated projected nft range endpoint to block,tx pagination

update openapi.json

add missing endpoint level docs

also paginate mint burn history endpoint
  • Loading branch information
ecioppettini authored and SebastienGllmt committed Mar 16, 2024
1 parent e5c5ab1 commit fcadc70
Show file tree
Hide file tree
Showing 28 changed files with 1,296 additions and 978 deletions.
552 changes: 294 additions & 258 deletions docs/bin/openapi.json

Large diffs are not rendered by default.

113 changes: 91 additions & 22 deletions webserver/server/app/controllers/AssetUtxosController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,23 @@ import type { IAssetUtxosResult } from '../models/asset/assetUtxos.queries';
import { bech32 } from 'bech32';
import { ASSET_UTXOS_LIMIT } from '../../../shared/constants';
import { Address } from '@dcspark/cardano-multiplatform-lib-nodejs';
import {
adjustToSlotLimits,
resolvePageStart,
resolveUntilTransaction,
} from '../services/PaginationService';
import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries';
import { expectType } from 'tsd';

const route = Routes.assetUtxos;

@Route('asset/utxos')
export class AssetUtxosController extends Controller {
/**
* Returns utxo entries filtered either by cip 14 fingerprint or by policy id.
*
* This is useful to keep track of the utxo set of a particular asset.
*/
@SuccessResponse(`${StatusCodes.OK}`)
@Post()
public async assetUtxos(
Expand All @@ -40,9 +52,50 @@ export class AssetUtxosController extends Controller {
);
}

const response = await tx<AssetUtxosResponse>(pool, async dbTx => {
const response = await tx<ErrorShape | AssetUtxosResponse>(pool, async dbTx => {
const [until, pageStart, slotBounds] = await Promise.all([
resolveUntilTransaction({
block_hash: Buffer.from(requestBody.untilBlock, 'hex'),
dbTx,
}),
requestBody.after == null
? Promise.resolve(undefined)
: resolvePageStart({
after_block: Buffer.from(requestBody.after.block, 'hex'),
after_tx: Buffer.from(requestBody.after.tx, 'hex'),
dbTx,
}),
!requestBody.slotLimits
? Promise.resolve(undefined)
: slotBoundsPagination.run(
{ low: requestBody.slotLimits.from, high: requestBody.slotLimits.to },
dbTx
),
]);

if (until == null) {
return genErrorMessage(Errors.BlockHashNotFound, {
untilBlock: requestBody.untilBlock,
});
}
if (requestBody.after != null && pageStart == null) {
return genErrorMessage(Errors.PageStartNotFound, {
blockHash: requestBody.after.block,
txHash: requestBody.after.tx,
});
}

const pageStartWithSlot = adjustToSlotLimits(
pageStart,
until,
requestBody.slotLimits,
slotBounds
);

const data = await getAssetUtxos({
range: requestBody.range,
after: pageStartWithSlot?.tx_id || 0,
until: until.tx_id,
limit: requestBody.limit || ASSET_UTXOS_LIMIT.DEFAULT_PAGE_SIZE,
fingerprints: requestBody.fingerprints?.map(asset => {
const decoded = bech32.decode(asset);
const payload = bech32.fromWords(decoded.words);
Expand All @@ -53,31 +106,47 @@ export class AssetUtxosController extends Controller {
dbTx,
});

return data.map((data: IAssetUtxosResult): AssetUtxosResponse[0] => {
const address = Address.from_bytes(Uint8Array.from(data.address_raw));
return data.map((data: IAssetUtxosResult) => {
return {
txId: data.tx as string,
block: data.block,
payload: (data.payload as { [key: string]: string | number }[]).map(x => {
const address = Address.from_bytes(
Uint8Array.from(Buffer.from(x.addressRaw as string, 'hex'))
);

const paymentCred = address.payment_cred();
const addressBytes = paymentCred?.to_bytes();
const paymentCred = address.payment_cred();
const addressBytes = paymentCred?.to_bytes();

address.free();
paymentCred?.free();
address.free();
paymentCred?.free();

return {
txId: data.tx_hash as string,
utxo: {
index: data.output_index,
tx: data.output_tx_hash as string,
},
paymentCred: Buffer.from(addressBytes as Uint8Array).toString('hex'),
amount: data.amount ? data.amount : undefined,
slot: data.slot,
cip14Fingerprint: bech32.encode('asset', bech32.toWords(data.cip14_fingerprint)),
policyId: Buffer.from(data.policy_id).toString('hex'),
assetName: Buffer.from(data.asset_name).toString('hex'),
};
return {
utxo: {
index: x.outputIndex,
tx: x.outputTxHash,
},
paymentCred: Buffer.from(addressBytes as Uint8Array).toString('hex'),
amount: x.amount ? x.amount : undefined,
slot: x.slot,
cip14Fingerprint: bech32.encode(
'asset',
bech32.toWords(Buffer.from(x.cip14Fingerprint as string, 'hex'))
),
policyId: x.policyId,
assetName: x.assetName,
};
}),
} as AssetUtxosResponse[0];
});
});

if ('code' in response) {
expectType<Equals<typeof response, ErrorShape>>(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return errorResponse(StatusCodes.CONFLICT, response);
}

return response;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const route = Routes.delegationForAddress;

@Route('delegation/address')
export class DelegationForAddressController extends Controller {
/**
* Returns the pool of the last delegation for this address.
*
* Note: the tx can be in the current epoch, so the delegation may not be in
* effect yet.
*/
@SuccessResponse(`${StatusCodes.OK}`)
@Post()
public async delegationForAddress(
Expand Down
87 changes: 69 additions & 18 deletions webserver/server/app/controllers/DelegationForPoolController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@ import { Routes } from '../../../shared/routes';
import { delegationsForPool } from '../services/DelegationForPool';
import type { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool';
import { POOL_DELEGATION_LIMIT } from '../../../shared/constants';
import {
adjustToSlotLimits,
resolvePageStart,
resolveUntilTransaction,
} from '../services/PaginationService';
import { slotBoundsPagination } from '../models/pagination/slotBoundsPagination.queries';
import { expectType } from 'tsd';

const route = Routes.delegationForPool;

@Route('delegation/pool')
export class DelegationForPoolController extends Controller {
/**
* Returns the list of delegations for the provided pools. The pool field in
* the response will be null when the address was previously delegating to a
* pool in the input, but now the delegation is moved to a pool outside the
* list, or when the staking key is unregistered.
*
* This is useful to keep track of the delegators for a particular pool.
*/
@SuccessResponse(`${StatusCodes.OK}`)
@Post()
public async delegationForPool(
Expand All @@ -35,33 +50,69 @@ export class DelegationForPoolController extends Controller {
);
}

const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot;
if (slotRangeSize > POOL_DELEGATION_LIMIT.SLOT_RANGE) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return errorResponse(
StatusCodes.BAD_REQUEST,
genErrorMessage(Errors.SlotRangeLimitExceeded, {
limit: POOL_DELEGATION_LIMIT.SLOT_RANGE,
found: slotRangeSize,
})
// note: we use a SQL transaction to make sure the pagination check works properly
// otherwise, a rollback could happen between getting the pagination info and the history query
const response = await tx<ErrorShape | DelegationForPoolResponse>(pool, async dbTx => {
const [until, pageStart, slotBounds] = await Promise.all([
resolveUntilTransaction({
block_hash: Buffer.from(requestBody.untilBlock, 'hex'),
dbTx,
}),
requestBody.after == null
? Promise.resolve(undefined)
: resolvePageStart({
after_block: Buffer.from(requestBody.after.block, 'hex'),
after_tx: Buffer.from(requestBody.after.tx, 'hex'),
dbTx,
}),
!requestBody.slotLimits
? Promise.resolve(undefined)
: slotBoundsPagination.run(
{ low: requestBody.slotLimits.from, high: requestBody.slotLimits.to },
dbTx
),
]);

if (until == null) {
return genErrorMessage(Errors.BlockHashNotFound, {
untilBlock: requestBody.untilBlock,
});
}
if (requestBody.after != null && pageStart == null) {
return genErrorMessage(Errors.PageStartNotFound, {
blockHash: requestBody.after.block,
txHash: requestBody.after.tx,
});
}

const pageStartWithSlot = adjustToSlotLimits(
pageStart,
until,
requestBody.slotLimits,
slotBounds
);
}

const response = await tx<DelegationForPoolResponse>(pool, async dbTx => {
const data = await delegationsForPool({
const response = await delegationsForPool({
pools: requestBody.pools.map(poolId => Buffer.from(poolId, 'hex')),
range: requestBody.range,
after: pageStartWithSlot?.tx_id || 0,
until: until.tx_id,
limit: requestBody.limit || POOL_DELEGATION_LIMIT.DEFAULT_PAGE_SIZE,
dbTx,
});

return data.map(data => ({
credential: data.credential,
pool: data.pool,
txId: data.tx_id,
slot: data.slot,
return response.map(x => ({
txId: x.tx_id,
block: x.block,
payload: x.payload as DelegationForPoolResponse[0]['payload'],
}));
});

if ('code' in response) {
expectType<Equals<typeof response, ErrorShape>>(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return errorResponse(StatusCodes.CONFLICT, response);
}

return response;
}
}
Loading

0 comments on commit fcadc70

Please sign in to comment.