Skip to content

Commit 41438ac

Browse files
authored
fix(brc20): historical token balance (#444)
* feat: start indexing brc20 balance history * fix: api support * style: revert * remove extra
1 parent ebad427 commit 41438ac

File tree

5 files changed

+392
-121
lines changed

5 files changed

+392
-121
lines changed

api/ordinals/src/pg/brc20/brc20-pg-store.ts

+13-15
Original file line numberDiff line numberDiff line change
@@ -82,38 +82,36 @@ export class Brc20PgStore extends BasePgStore {
8282
): Promise<DbPaginatedResult<DbBrc20Balance>> {
8383
const ticker = sqlOr(
8484
this.sql,
85-
args.ticker?.map(t => this.sql`d.ticker LIKE LOWER(${t}) || '%'`)
85+
args.ticker?.map(t => this.sql`b.ticker LIKE LOWER(${t}) || '%'`)
8686
);
8787
// Change selection table depending if we're filtering by block height or not.
8888
const results = await this.sql<(DbBrc20Balance & { total: number })[]>`
89+
SELECT
90+
b.ticker, (SELECT decimals FROM tokens WHERE ticker = b.ticker) AS decimals,
91+
b.avail_balance, b.trans_balance, b.total_balance, COUNT(*) OVER() as total
8992
${
9093
args.block_height
9194
? this.sql`
92-
SELECT
93-
d.ticker, d.decimals,
94-
SUM(b.avail_balance) AS avail_balance,
95-
SUM(b.trans_balance) AS trans_balance,
96-
SUM(b.avail_balance + b.trans_balance) AS total_balance,
97-
COUNT(*) OVER() as total
98-
FROM operations AS b
99-
INNER JOIN tokens AS d ON d.ticker = b.ticker
95+
FROM balances_history b
96+
INNER JOIN (
97+
SELECT ticker, address, MAX(block_height) as max_block_height
98+
FROM balances_history
99+
WHERE address = ${args.address} AND block_height <= ${args.block_height}
100+
GROUP BY ticker, address
101+
) latest ON b.ticker = latest.ticker AND b.address = latest.address AND b.block_height = latest.max_block_height
100102
WHERE
101-
b.address = ${args.address}
102-
AND b.block_height <= ${args.block_height}
103+
b.total_balance > 0
103104
${ticker ? this.sql`AND ${ticker}` : this.sql``}
104-
GROUP BY d.ticker, d.decimals
105-
HAVING SUM(b.avail_balance + b.trans_balance) > 0
106105
`
107106
: this.sql`
108-
SELECT d.ticker, d.decimals, b.avail_balance, b.trans_balance, b.total_balance, COUNT(*) OVER() as total
109107
FROM balances AS b
110-
INNER JOIN tokens AS d ON d.ticker = b.ticker
111108
WHERE
112109
b.total_balance > 0
113110
AND b.address = ${args.address}
114111
${ticker ? this.sql`AND ${ticker}` : this.sql``}
115112
`
116113
}
114+
ORDER BY b.total_balance DESC
117115
LIMIT ${args.limit}
118116
OFFSET ${args.offset}
119117
`;

api/ordinals/tests/brc-20/api.test.ts

+227
Original file line numberDiff line numberDiff line change
@@ -1400,4 +1400,231 @@ describe('BRC-20 API', () => {
14001400
expect(response.statusCode).toBe(404);
14011401
});
14021402
});
1403+
1404+
describe('/brc-20/balances', () => {
1405+
test('address balance history is accurate', async () => {
1406+
// Setup
1407+
const numbers = incrementing(0);
1408+
const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz';
1409+
const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
1410+
1411+
// A deploys pepe
1412+
let transferHash = randomHash();
1413+
let blockHash = randomHash();
1414+
await brc20TokenDeploy(brc20Db.sql, {
1415+
ticker: 'pepe',
1416+
display_ticker: 'pepe',
1417+
inscription_id: `${transferHash}i0`,
1418+
inscription_number: numbers.next().value.toString(),
1419+
block_height: '780000',
1420+
block_hash: blockHash,
1421+
tx_id: transferHash,
1422+
tx_index: 0,
1423+
address: addressA,
1424+
max: '21000000000000000000000000',
1425+
limit: '21000000000000000000000000',
1426+
decimals: 18,
1427+
self_mint: false,
1428+
minted_supply: '0',
1429+
tx_count: 1,
1430+
timestamp: 1677803510,
1431+
operation: 'deploy',
1432+
ordinal_number: '20000',
1433+
output: `${transferHash}:0`,
1434+
offset: '0',
1435+
to_address: null,
1436+
amount: '0',
1437+
});
1438+
// A mints 10000 pepe
1439+
transferHash = randomHash();
1440+
blockHash = randomHash();
1441+
await brc20Operation(brc20Db.sql, {
1442+
ticker: 'pepe',
1443+
operation: 'mint',
1444+
inscription_id: `${transferHash}i0`,
1445+
inscription_number: numbers.next().value.toString(),
1446+
ordinal_number: '200000',
1447+
block_height: '780050',
1448+
block_hash: blockHash,
1449+
tx_id: transferHash,
1450+
tx_index: 0,
1451+
output: `${transferHash}:0`,
1452+
offset: '0',
1453+
timestamp: 1677803510,
1454+
address: addressA,
1455+
to_address: null,
1456+
amount: '10000000000000000000000',
1457+
});
1458+
// A mints 10000 pepe again
1459+
transferHash = randomHash();
1460+
blockHash = randomHash();
1461+
await brc20Operation(brc20Db.sql, {
1462+
ticker: 'pepe',
1463+
operation: 'mint',
1464+
inscription_id: `${transferHash}i0`,
1465+
inscription_number: numbers.next().value.toString(),
1466+
ordinal_number: '200000',
1467+
block_height: '780060',
1468+
block_hash: blockHash,
1469+
tx_id: transferHash,
1470+
tx_index: 0,
1471+
output: `${transferHash}:0`,
1472+
offset: '0',
1473+
timestamp: 1677803510,
1474+
address: addressA,
1475+
to_address: null,
1476+
amount: '10000000000000000000000',
1477+
});
1478+
// B mints 10000 pepe
1479+
transferHash = randomHash();
1480+
blockHash = randomHash();
1481+
await brc20Operation(brc20Db.sql, {
1482+
ticker: 'pepe',
1483+
operation: 'mint',
1484+
inscription_id: `${transferHash}i0`,
1485+
inscription_number: numbers.next().value.toString(),
1486+
ordinal_number: '200000',
1487+
block_height: '780070',
1488+
block_hash: blockHash,
1489+
tx_id: transferHash,
1490+
tx_index: 0,
1491+
output: `${transferHash}:0`,
1492+
offset: '0',
1493+
timestamp: 1677803510,
1494+
address: addressB,
1495+
to_address: null,
1496+
amount: '10000000000000000000000',
1497+
});
1498+
1499+
// A deploys test
1500+
transferHash = randomHash();
1501+
blockHash = randomHash();
1502+
await brc20TokenDeploy(brc20Db.sql, {
1503+
ticker: 'test',
1504+
display_ticker: 'test',
1505+
inscription_id: `${transferHash}i0`,
1506+
inscription_number: numbers.next().value.toString(),
1507+
block_height: '780100',
1508+
block_hash: blockHash,
1509+
tx_id: transferHash,
1510+
tx_index: 0,
1511+
address: addressA,
1512+
max: '21000000000000000000000000',
1513+
limit: '21000000000000000000000000',
1514+
decimals: 18,
1515+
self_mint: false,
1516+
minted_supply: '0',
1517+
tx_count: 1,
1518+
timestamp: 1677803510,
1519+
operation: 'deploy',
1520+
ordinal_number: '20000',
1521+
output: `${transferHash}:0`,
1522+
offset: '0',
1523+
to_address: null,
1524+
amount: '0',
1525+
});
1526+
// A mints 10000 test
1527+
transferHash = randomHash();
1528+
blockHash = randomHash();
1529+
await brc20Operation(brc20Db.sql, {
1530+
ticker: 'test',
1531+
operation: 'mint',
1532+
inscription_id: `${transferHash}i0`,
1533+
inscription_number: numbers.next().value.toString(),
1534+
ordinal_number: '200000',
1535+
block_height: '780200',
1536+
block_hash: blockHash,
1537+
tx_id: transferHash,
1538+
tx_index: 0,
1539+
output: `${transferHash}:0`,
1540+
offset: '0',
1541+
timestamp: 1677803510,
1542+
address: addressA,
1543+
to_address: null,
1544+
amount: '10000000000000000000000',
1545+
});
1546+
1547+
// Verify balance history across block intervals
1548+
let response = await fastify.inject({
1549+
method: 'GET',
1550+
url: `/ordinals/brc-20/balances/${addressA}`,
1551+
});
1552+
expect(response.statusCode).toBe(200);
1553+
let json = response.json();
1554+
expect(json.total).toBe(2);
1555+
expect(json.results).toEqual(
1556+
expect.arrayContaining([
1557+
{
1558+
available_balance: '20000.000000000000000000',
1559+
overall_balance: '20000.000000000000000000',
1560+
ticker: 'pepe',
1561+
transferrable_balance: '0.000000000000000000',
1562+
},
1563+
{
1564+
available_balance: '10000.000000000000000000',
1565+
overall_balance: '10000.000000000000000000',
1566+
ticker: 'test',
1567+
transferrable_balance: '0.000000000000000000',
1568+
},
1569+
])
1570+
);
1571+
response = await fastify.inject({
1572+
method: 'GET',
1573+
url: `/ordinals/brc-20/balances/${addressA}?block_height=780200`,
1574+
});
1575+
expect(response.statusCode).toBe(200);
1576+
json = response.json();
1577+
expect(json.total).toBe(2);
1578+
expect(json.results).toEqual(
1579+
expect.arrayContaining([
1580+
{
1581+
available_balance: '20000.000000000000000000',
1582+
overall_balance: '20000.000000000000000000',
1583+
ticker: 'pepe',
1584+
transferrable_balance: '0.000000000000000000',
1585+
},
1586+
{
1587+
available_balance: '10000.000000000000000000',
1588+
overall_balance: '10000.000000000000000000',
1589+
ticker: 'test',
1590+
transferrable_balance: '0.000000000000000000',
1591+
},
1592+
])
1593+
);
1594+
response = await fastify.inject({
1595+
method: 'GET',
1596+
url: `/ordinals/brc-20/balances/${addressA}?block_height=780200&ticker=te`,
1597+
});
1598+
expect(response.statusCode).toBe(200);
1599+
json = response.json();
1600+
expect(json.total).toBe(1);
1601+
expect(json.results).toEqual(
1602+
expect.arrayContaining([
1603+
{
1604+
available_balance: '10000.000000000000000000',
1605+
overall_balance: '10000.000000000000000000',
1606+
ticker: 'test',
1607+
transferrable_balance: '0.000000000000000000',
1608+
},
1609+
])
1610+
);
1611+
response = await fastify.inject({
1612+
method: 'GET',
1613+
url: `/ordinals/brc-20/balances/${addressA}?block_height=780050`,
1614+
});
1615+
expect(response.statusCode).toBe(200);
1616+
json = response.json();
1617+
expect(json.total).toBe(1);
1618+
expect(json.results).toEqual(
1619+
expect.arrayContaining([
1620+
{
1621+
available_balance: '10000.000000000000000000',
1622+
overall_balance: '10000.000000000000000000',
1623+
ticker: 'pepe',
1624+
transferrable_balance: '0.000000000000000000',
1625+
},
1626+
])
1627+
);
1628+
});
1629+
});
14031630
});

api/ordinals/tests/helpers.ts

+14
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,20 @@ export async function brc20Operation(sql: PgSqlClient, operation: TestBrc20Opera
490490
trans_balance = balances.trans_balance + EXCLUDED.trans_balance,
491491
total_balance = balances.avail_balance + EXCLUDED.total_balance
492492
`;
493+
await sql`
494+
INSERT INTO balances_history
495+
(ticker, address, block_height, avail_balance, trans_balance, total_balance)
496+
(
497+
SELECT ticker, address, ${operation.block_height} AS block_height, avail_balance,
498+
trans_balance, total_balance
499+
FROM balances
500+
WHERE address = ${operation.address} AND ticker = ${operation.ticker}
501+
)
502+
ON CONFLICT (address, block_height, ticker) DO UPDATE SET
503+
avail_balance = EXCLUDED.avail_balance,
504+
trans_balance = EXCLUDED.trans_balance,
505+
total_balance = EXCLUDED.total_balance
506+
`;
493507
}
494508

495509
/** Generate a random hash like string for testing */

0 commit comments

Comments
 (0)