Skip to content

Commit

Permalink
feat: /account/:stake_addr/utxos
Browse files Browse the repository at this point in the history
  • Loading branch information
slowbackspace committed Oct 26, 2024
1 parent 8b502b0 commit 8312933
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `/account/:stake_addr/utxos` for retrieving utxos associated with a stake account

### Fixed

- Added instant rewards, reserves, treasury and proposal_refund to the calculation of delegators' total amount in `/governance/dreps/:drep/delegators`
Expand Down
1 change: 1 addition & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const start = (options = {}): FastifyInstance => {
registerRoute(app, import('./routes/accounts/stake-address/mirs.js'));
registerRoute(app, import('./routes/accounts/stake-address/registrations.js'));
registerRoute(app, import('./routes/accounts/stake-address/rewards.js'));
registerRoute(app, import('./routes/accounts/stake-address/utxos.js'));
registerRoute(app, import('./routes/accounts/stake-address/withdrawals.js'));

// assets
Expand Down
92 changes: 92 additions & 0 deletions src/routes/accounts/stake-address/utxos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { validateStakeAddress } from '../../../utils/validation.js';
// import { getSchemaForEndpoint } from '@blockfrost/openapi';
import { FastifyInstance, FastifyRequest } from 'fastify';
import { SQLQuery } from '../../../sql/index.js';
// import * as ResponseTypes from '../../../types/responses/accounts.js';
import * as QueryTypes from '../../../types/queries/accounts.js';
import { getDbSync, gracefulRelease } from '../../../utils/database.js';
import { isUnpaged } from '../../../utils/routes.js';
import { toJSONStream } from '../../../utils/string-utils.js';
import { handle400Custom, handle404 } from '../../../utils/error-handler.js';

async function route(fastify: FastifyInstance) {
fastify.route({
url: '/accounts/:stake_address/utxos',
method: 'GET',
// TODO
// schema: getSchemaForEndpoint('/addresses/{address}/utxos'),
handler: async (request: FastifyRequest<QueryTypes.RequestAccountsQueryParameters>, reply) => {
const clientDbSync = await getDbSync(fastify);

try {
// Check stake address format. Return 400 on non-valid stake address
const isStakeAddressValid = validateStakeAddress(request.params.stake_address);

if (!isStakeAddressValid) {
gracefulRelease(clientDbSync);
return handle400Custom(reply, 'Invalid or malformed stake address format.');
}

const query404 = await clientDbSync.query<QueryTypes.ResultFound>(
SQLQuery.get('accounts_404'),
[request.params.stake_address],
);

if (query404.rows.length === 0) {
return handle404(reply);
}

const unpaged = isUnpaged(request);
const { rows } = unpaged
? await clientDbSync.query<QueryTypes.AccountUtxosQuery>(
SQLQuery.get('accounts_stake_address_rewards_unpaged'),
[request.query.order, request.params.stake_address],
)
: await clientDbSync.query<QueryTypes.AccountUtxosQuery>(
SQLQuery.get('accounts_stake_address_utxos'),
[
request.query.order,
request.query.count,
request.query.page,
request.params.stake_address,
],
);

gracefulRelease(clientDbSync);

const result = rows.map(row => ({
address: row.address,
tx_hash: row.tx_hash,
tx_index: row.tx_index,
output_index: row.output_index,
amount: [
{
unit: 'lovelace',
quantity: row.amount_lovelace,
},
...(row.amount ?? []),
],
block: row.block,
data_hash: row.data_hash,
inline_datum: row.inline_datum,
reference_script_hash: row.reference_script_hash,
}));

if (unpaged) {
// Use of Reply.raw functions is at your own risk as you are skipping all the Fastify logic of handling the HTTP response
// https://www.fastify.io/docs/latest/Reference/Reply/#raw
reply.raw.writeHead(200, { 'Content-Type': 'application/json' });
await toJSONStream(result, reply.raw);
return reply;
} else {
return reply.send(result);
}
} catch (error) {
gracefulRelease(clientDbSync);
throw error;
}
},
});
}

export default route;
56 changes: 56 additions & 0 deletions src/sql/accounts/accounts_stake_address_utxos.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- Same as addresses_address_utxos.sql except this has JOIN stake_address sa ON (txo.stake_address_id = sa.id)
-- and additional WHERE condition sa.view = $4
SELECT txo.address AS "address",
encode(tx.hash, 'hex') AS "tx_hash",
txo.index AS "tx_index",
txo.index AS "output_index",
txo.value::TEXT AS "amount_lovelace", -- cast to TEXT to avoid number overflow
(
SELECT json_agg(
json_build_object(
'unit',
CONCAT(encode(ma.policy, 'hex'), encode(ma.name, 'hex')),
'quantity',
mto.quantity::TEXT -- cast to TEXT to avoid number overflow
)
)
FROM ma_tx_out mto
JOIN multi_asset ma ON (mto.ident = ma.id)
WHERE mto.tx_out_id = txo.id
) AS "amount",
encode(b.hash, 'hex') AS "block",
encode(data_hash, 'hex') AS "data_hash",
encode(dat.bytes, 'hex') AS "inline_datum",
encode(scr.hash, 'hex') AS "reference_script_hash"
FROM tx
JOIN tx_out txo ON (tx.id = txo.tx_id)
JOIN stake_address sa ON (txo.stake_address_id = sa.id)
LEFT JOIN tx_in txi ON (txo.tx_id = txi.tx_out_id)
AND (txo.index = txi.tx_out_index)
JOIN block b ON (b.id = tx.block_id)
LEFT JOIN datum dat ON (txo.inline_datum_id = dat.id)
LEFT JOIN script scr ON (txo.reference_script_id = scr.id)
WHERE txi.tx_in_id IS NULL
AND sa.view = $4
ORDER BY CASE
WHEN LOWER($1) = 'desc' THEN txo.id
END DESC,
CASE
WHEN LOWER($1) <> 'desc'
OR $1 IS NULL THEN txo.id
END ASC
LIMIT CASE
WHEN $2 >= 1
AND $2 <= 100 THEN $2
ELSE 100
END OFFSET CASE
WHEN $3 > 1
AND $3 < 2147483647 THEN ($3 - 1) * (
CASE
WHEN $2 >= 1
AND $2 <= 100 THEN $2
ELSE 100
END
)
ELSE 0
END
41 changes: 41 additions & 0 deletions src/sql/accounts/unpaged/accounts_stake_address_utxos.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Same as addresses_address_utxos.sql except this has JOIN stake_address sa ON (txo.stake_address_id = sa.id)
-- and additional WHERE condition sa.view = $4
SELECT txo.address AS "address",
encode(tx.hash, 'hex') AS "tx_hash",
txo.index AS "tx_index",
txo.index AS "output_index",
txo.value::TEXT AS "amount_lovelace", -- cast to TEXT to avoid number overflow
(
SELECT json_agg(
json_build_object(
'unit',
CONCAT(encode(ma.policy, 'hex'), encode(ma.name, 'hex')),
'quantity',
mto.quantity::TEXT -- cast to TEXT to avoid number overflow
)
)
FROM ma_tx_out mto
JOIN multi_asset ma ON (mto.ident = ma.id)
WHERE mto.tx_out_id = txo.id
) AS "amount",
encode(b.hash, 'hex') AS "block",
encode(data_hash, 'hex') AS "data_hash",
encode(dat.bytes, 'hex') AS "inline_datum",
encode(scr.hash, 'hex') AS "reference_script_hash"
FROM tx
JOIN tx_out txo ON (tx.id = txo.tx_id)
JOIN stake_address sa ON (txo.stake_address_id = sa.id)
LEFT JOIN tx_in txi ON (txo.tx_id = txi.tx_out_id)
AND (txo.index = txi.tx_out_index)
JOIN block b ON (b.id = tx.block_id)
LEFT JOIN datum dat ON (txo.inline_datum_id = dat.id)
LEFT JOIN script scr ON (txo.reference_script_id = scr.id)
WHERE txi.tx_in_id IS NULL
AND sa.view = $4
ORDER BY CASE
WHEN LOWER($1) = 'desc' THEN txo.id
END DESC,
CASE
WHEN LOWER($1) <> 'desc'
OR $1 IS NULL THEN txo.id
END ASC
2 changes: 2 additions & 0 deletions src/sql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const QUERY_FILES = {
accounts_stake_address_registrations: 'accounts/accounts_stake_address_registrations.sql',
accounts_stake_address_registrations_unpaged:
'accounts/unpaged/accounts_stake_address_registrations.sql',
accounts_stake_address_utxos: 'accounts/accounts_stake_address_utxos.sql',
accounts_stake_address_utxos_unpaged: 'accounts/unpaged/accounts_stake_address_utxos.sql',
accounts_stake_address_rewards: 'accounts/accounts_stake_address_rewards.sql',
accounts_stake_address_rewards_unpaged: 'accounts/unpaged/accounts_stake_address_rewards.sql',
accounts_stake_address_withdrawals: 'accounts/accounts_stake_address_withdrawals.sql',
Expand Down
13 changes: 13 additions & 0 deletions src/types/queries/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,16 @@ export interface AccountAddressesTotal {
received_amount: Amount[];
tx_count: number;
}

export interface AccountUtxosQuery {
address: string;
tx_hash: string;
tx_index: number;
output_index: number;
amount_lovelace: string;
amount: Amount[];
block: string;
data_hash: string;
inline_datum: string;
reference_script_hash: string;
}
1 change: 1 addition & 0 deletions src/types/responses/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export type AccountAssets = {
export type AccountAddresses = OpenApiResponseTypes['account_addresses_content'];

export type AccountAddressesTotal = OpenApiResponseTypes['account_addresses_total'];
export type AccountUtxos = OpenApiResponseTypes['address_utxo_content'];
9 changes: 9 additions & 0 deletions src/utils/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@ export const getDbSync = async (fastify: FastifyInstance): Promise<PoolClient> =
throw error;
}
};

export const gracefulRelease = (clientDbSync: PoolClient) => {
if (!clientDbSync) return;
try {
clientDbSync.release();
} catch (error) {
console.warn(error);
}
};

0 comments on commit 8312933

Please sign in to comment.