Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rebase: pagination based on txs #177

Merged
merged 1 commit into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading