Skip to content

Commit aca148f

Browse files
authored
Merge pull request #2034 from hirosystems/develop
Cut release v7.13.0
2 parents 3bfe1d6 + 1d9d0a6 commit aca148f

14 files changed

+1315
-33
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"limit": 100,
3+
"offset": 0,
4+
"total": 3,
5+
"total_supply": "11700",
6+
"results": [
7+
{
8+
"address": "SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA2K",
9+
"balance": "10000"
10+
},
11+
{
12+
"address": "SP3WJYXJZ4QK2V5V9VX2VXVZ6VXVZ6V2V5V2V2V2V",
13+
"balance": "900"
14+
},
15+
{
16+
"address": "SP3WJYXJZ4QK2V5V9VX2VXVZ6VXVZ6V2V5V2V2V2V",
17+
"balance": "800"
18+
}
19+
]
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"description": "List of Fungible Token holders",
3+
"title": "FungibleTokenHolderList",
4+
"type": "object",
5+
"required": [
6+
"total_supply",
7+
"results",
8+
"limit",
9+
"offset",
10+
"total"
11+
],
12+
"additionalProperties": false,
13+
"properties": {
14+
"limit": {
15+
"type": "integer",
16+
"maximum": 200,
17+
"description": "The number of holders to return"
18+
},
19+
"offset": {
20+
"type": "integer",
21+
"description": "The number to holders to skip (starting at `0`)"
22+
},
23+
"total": {
24+
"type": "integer",
25+
"description": "The number of holders available"
26+
},
27+
"total_supply": {
28+
"type": "string",
29+
"description": "The total supply of the token (the sum of all balances)"
30+
},
31+
"results": {
32+
"type": "array",
33+
"items": {
34+
"$ref": "../../entities/tokens/ft-holder-entry.schema.json"
35+
}
36+
}
37+
}
38+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"type": "object",
3+
"title": "FtHolderEntry",
4+
"required": ["address", "balance"],
5+
"additionalProperties": false,
6+
"properties": {
7+
"address": {
8+
"type": "string"
9+
},
10+
"balance": {
11+
"type": "string"
12+
}
13+
}
14+
}

docs/generated.d.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export type SchemaMergeRootStub =
103103
| PoxCycleSignerStackersListResponse
104104
| PoxCycleSignersListResponse
105105
| PoxCycleListResponse
106+
| FungibleTokenHolderList
106107
| {
107108
[k: string]: unknown | undefined;
108109
}
@@ -193,6 +194,7 @@ export type SchemaMergeRootStub =
193194
| PoxCycle
194195
| PoxSigner
195196
| PoxStacker
197+
| FtHolderEntry
196198
| NonFungibleTokenHistoryEventWithTxId
197199
| NonFungibleTokenHistoryEventWithTxMetadata
198200
| NonFungibleTokenHistoryEvent
@@ -3437,6 +3439,32 @@ export interface PoxCycle {
34373439
total_stacked_amount: string;
34383440
total_signers: number;
34393441
}
3442+
/**
3443+
* List of Fungible Token holders
3444+
*/
3445+
export interface FungibleTokenHolderList {
3446+
/**
3447+
* The number of holders to return
3448+
*/
3449+
limit: number;
3450+
/**
3451+
* The number to holders to skip (starting at `0`)
3452+
*/
3453+
offset: number;
3454+
/**
3455+
* The number of holders available
3456+
*/
3457+
total: number;
3458+
/**
3459+
* The total supply of the token (the sum of all balances)
3460+
*/
3461+
total_supply: string;
3462+
results: FtHolderEntry[];
3463+
}
3464+
export interface FtHolderEntry {
3465+
address: string;
3466+
balance: string;
3467+
}
34403468
/**
34413469
* List of Non-Fungible Token history events
34423470
*/

docs/openapi.yaml

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,9 +1938,11 @@ paths:
19381938
get:
19391939
summary: Get account transactions
19401940
description: |
1941-
Retrieves a list of all Transactions for a given Address or Contract Identifier. More information on Transaction types can be found [here](https://docs.stacks.co/understand-stacks/transactions#types).
1941+
**NOTE:** This endpoint is deprecated in favor of [Get address transactions](/api/get-address-transactions).
19421942
1943-
If you need to actively monitor new transactions for an address or contract id, we highly recommend subscribing to [WebSockets or Socket.io](https://github.com/hirosystems/stacks-blockchain-api/tree/master/client) for real-time updates.
1943+
Retrieves a list of all Transactions for a given Address or Contract Identifier. More information on Transaction types can be found [here](https://docs.stacks.co/understand-stacks/transactions#types).
1944+
1945+
If you need to actively monitor new transactions for an address or contract id, we highly recommend subscribing to [WebSockets or Socket.io](https://github.com/hirosystems/stacks-blockchain-api/tree/master/client) for real-time updates.
19441946
deprecated: true
19451947
tags:
19461948
- Accounts
@@ -2002,7 +2004,10 @@ paths:
20022004
/extended/v1/address/{principal}/{tx_id}/with_transfers:
20032005
get:
20042006
summary: Get account transaction information for specific transaction
2005-
description: Retrieves transaction details for a given Transaction Id `tx_id`, for a given account or contract Identifier.
2007+
description: |
2008+
**NOTE:** This endpoint is deprecated in favor of [Get events for an address transaction](/api/get-address-transaction-events).
2009+
2010+
Retrieves transaction details for a given Transaction Id `tx_id`, for a given account or contract Identifier.
20062011
deprecated: true
20072012
tags:
20082013
- Accounts
@@ -3678,6 +3683,38 @@ paths:
36783683
value:
36793684
$ref: ./api/tokens/get-non-fungible-token-mints-tx-metadata.example.schema.json
36803685

3686+
/extended/v1/tokens/ft/{token}/holders:
3687+
get:
3688+
operationId: get_ft_holders
3689+
summary: Fungible token holders
3690+
description: |
3691+
Retrieves the list of Fungible Token holders for a given token ID. Specify `stx` for the `token` parameter to get the list of STX holders.
3692+
tags:
3693+
- Fungible Tokens
3694+
parameters:
3695+
- name: token
3696+
in: path
3697+
description: fungible token identifier
3698+
required: true
3699+
schema:
3700+
type: string
3701+
examples:
3702+
stx:
3703+
value: stx
3704+
summary: STX token
3705+
ft:
3706+
value: SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-aeusdc::aeUSDC
3707+
summary: fungible token
3708+
responses:
3709+
200:
3710+
description: Fungible Token holders
3711+
content:
3712+
application/json:
3713+
schema:
3714+
$ref: ./api/tokens/get-ft-holders.schema.json
3715+
example:
3716+
$ref: ./api/tokens/get-ft-holders.example.json
3717+
36813718
/extended/v1/fee_rate:
36823719
post:
36833720
operationId: fetch_fee_rate
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
2+
exports.up = pgm => {
3+
pgm.createTable('ft_balances', {
4+
id: {
5+
type: 'bigserial',
6+
primaryKey: true,
7+
},
8+
address: {
9+
type: 'text',
10+
notNull: true,
11+
},
12+
token: {
13+
type: 'text',
14+
notNull: true,
15+
},
16+
balance: {
17+
type: 'numeric',
18+
notNull: true,
19+
}
20+
});
21+
22+
pgm.addConstraint('ft_balances', 'unique_address_token', `UNIQUE(address, token)`);
23+
24+
// Speeds up "grab the addresses with the highest balance for a given token" queries
25+
pgm.createIndex('ft_balances', [{ name: 'token' }, { name: 'balance', sort: 'DESC' }]);
26+
27+
// Speeds up "get the total supply of a given token" queries
28+
pgm.createIndex('ft_balances', 'token');
29+
30+
// Populate the table with the current stx balances
31+
pgm.sql(`
32+
WITH all_balances AS (
33+
SELECT sender AS address, -SUM(amount) AS balance_change
34+
FROM stx_events
35+
WHERE asset_event_type_id IN (1, 3) -- Transfers and Burns affect the sender's balance
36+
AND canonical = true AND microblock_canonical = true
37+
GROUP BY sender
38+
UNION ALL
39+
SELECT recipient AS address, SUM(amount) AS balance_change
40+
FROM stx_events
41+
WHERE asset_event_type_id IN (1, 2) -- Transfers and Mints affect the recipient's balance
42+
AND canonical = true AND microblock_canonical = true
43+
GROUP BY recipient
44+
),
45+
net_balances AS (
46+
SELECT address, SUM(balance_change) AS balance
47+
FROM all_balances
48+
GROUP BY address
49+
),
50+
fees AS (
51+
SELECT address, SUM(total_fees) AS total_fees
52+
FROM (
53+
SELECT sender_address AS address, SUM(fee_rate) AS total_fees
54+
FROM txs
55+
WHERE canonical = true AND microblock_canonical = true AND sponsored = false
56+
GROUP BY sender_address
57+
UNION ALL
58+
SELECT sponsor_address AS address, SUM(fee_rate) AS total_fees
59+
FROM txs
60+
WHERE canonical = true AND microblock_canonical = true AND sponsored = true
61+
GROUP BY sponsor_address
62+
) AS subquery
63+
GROUP BY address
64+
),
65+
rewards AS (
66+
SELECT
67+
recipient AS address,
68+
SUM(
69+
coinbase_amount + tx_fees_anchored + tx_fees_streamed_confirmed + tx_fees_streamed_produced
70+
) AS total_rewards
71+
FROM miner_rewards
72+
WHERE canonical = true
73+
GROUP BY recipient
74+
),
75+
all_addresses AS (
76+
SELECT address FROM net_balances
77+
UNION
78+
SELECT address FROM fees
79+
UNION
80+
SELECT address FROM rewards
81+
)
82+
INSERT INTO ft_balances (address, balance, token)
83+
SELECT
84+
aa.address,
85+
COALESCE(nb.balance, 0) - COALESCE(f.total_fees, 0) + COALESCE(r.total_rewards, 0) AS balance,
86+
'stx' AS token
87+
FROM all_addresses aa
88+
LEFT JOIN net_balances nb ON aa.address = nb.address
89+
LEFT JOIN fees f ON aa.address = f.address
90+
LEFT JOIN rewards r ON aa.address = r.address
91+
`);
92+
93+
// Populate the table with the current FT balances
94+
pgm.sql(`
95+
WITH all_balances AS (
96+
SELECT sender AS address, asset_identifier, -SUM(amount) AS balance_change
97+
FROM ft_events
98+
WHERE asset_event_type_id IN (1, 3) -- Transfers and Burns affect the sender's balance
99+
AND canonical = true
100+
AND microblock_canonical = true
101+
GROUP BY sender, asset_identifier
102+
UNION ALL
103+
SELECT recipient AS address, asset_identifier, SUM(amount) AS balance_change
104+
FROM ft_events
105+
WHERE asset_event_type_id IN (1, 2) -- Transfers and Mints affect the recipient's balance
106+
AND canonical = true
107+
AND microblock_canonical = true
108+
GROUP BY recipient, asset_identifier
109+
),
110+
net_balances AS (
111+
SELECT address, asset_identifier, SUM(balance_change) AS balance
112+
FROM all_balances
113+
GROUP BY address, asset_identifier
114+
)
115+
INSERT INTO ft_balances (address, balance, token)
116+
SELECT address, balance, asset_identifier AS token
117+
FROM net_balances
118+
`);
119+
120+
};
121+
122+
/** @param { import("node-pg-migrate").MigrationBuilder } pgm */
123+
exports.down = pgm => {
124+
pgm.dropTable('ft_balances');
125+
};

src/api/pagination.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export enum ResourceType {
3838
BurnBlock,
3939
Signer,
4040
PoxCycle,
41+
TokenHolders,
4142
}
4243

4344
export const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; maxLimit: number }> = {
@@ -89,6 +90,10 @@ export const pagingQueryLimits: Record<ResourceType, { defaultLimit: number; max
8990
defaultLimit: 20,
9091
maxLimit: 60,
9192
},
93+
[ResourceType.TokenHolders]: {
94+
defaultLimit: 100,
95+
maxLimit: 200,
96+
},
9297
};
9398

9499
export function getPagingQueryLimit(

src/api/routes/tokens.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { asyncHandler } from '../async-handler';
22
import * as express from 'express';
33
import {
4+
FungibleTokenHolderList,
45
NonFungibleTokenHistoryEvent,
56
NonFungibleTokenHistoryEventList,
67
NonFungibleTokenHolding,
@@ -228,5 +229,25 @@ export function createTokenRouter(db: PgStore): express.Router {
228229
})
229230
);
230231

232+
router.get(
233+
'/ft/:token/holders',
234+
cacheHandler,
235+
asyncHandler(async (req, res) => {
236+
const token = req.params.token;
237+
const limit = getPagingQueryLimit(ResourceType.TokenHolders, req.query.limit);
238+
const offset = parsePagingQueryInput(req.query.offset ?? 0);
239+
const { results, total, totalSupply } = await db.getTokenHolders({ token, limit, offset });
240+
const response: FungibleTokenHolderList = {
241+
limit: limit,
242+
offset: offset,
243+
total: total,
244+
total_supply: totalSupply,
245+
results: results,
246+
};
247+
setETagCacheHeaders(res);
248+
res.status(200).json(response);
249+
})
250+
);
251+
231252
return router;
232253
}

src/datastore/pg-store-v2.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,6 @@ export class PgStoreV2 extends BasePgStoreModule {
417417
args: AddressParams & TransactionPaginationQueryParams
418418
): Promise<DbPaginatedResult<DbTxWithAddressTransfers>> {
419419
return await this.sqlTransaction(async sql => {
420-
await assertAddressExists(sql, args.address);
421420
const limit = args.limit ?? TransactionLimitParamSchema.default;
422421
const offset = args.offset ?? 0;
423422

@@ -461,7 +460,12 @@ export class PgStoreV2 extends BasePgStoreModule {
461460
SELECT COALESCE(SUM(amount), 0)
462461
FROM stx_events
463462
WHERE ${eventCond} AND sender = ${args.address}
464-
) + txs.fee_rate AS stx_sent,
463+
) +
464+
CASE
465+
WHEN (txs.sponsored = false AND txs.sender_address = ${args.address})
466+
OR (txs.sponsored = true AND txs.sponsor_address = ${args.address})
467+
THEN txs.fee_rate ELSE 0
468+
END AS stx_sent,
465469
(
466470
SELECT COALESCE(SUM(amount), 0)
467471
FROM stx_events
@@ -526,7 +530,6 @@ export class PgStoreV2 extends BasePgStoreModule {
526530
args: AddressTransactionParams & TransactionPaginationQueryParams
527531
): Promise<DbPaginatedResult<DbAddressTransactionEvent>> {
528532
return await this.sqlTransaction(async sql => {
529-
await assertAddressExists(sql, args.address);
530533
await assertTxIdExists(sql, args.tx_id);
531534
const limit = args.limit ?? TransactionLimitParamSchema.default;
532535
const offset = args.offset ?? 0;

0 commit comments

Comments
 (0)