mirror of
https://github.com/alexgo-io/bitcoin-indexer.git
synced 2026-01-12 16:52:57 +08:00
fix(brc20): historical token balance (#444)
* feat: start indexing brc20 balance history * fix: api support * style: revert * remove extra
This commit is contained in:
@@ -82,38 +82,36 @@ export class Brc20PgStore extends BasePgStore {
|
||||
): Promise<DbPaginatedResult<DbBrc20Balance>> {
|
||||
const ticker = sqlOr(
|
||||
this.sql,
|
||||
args.ticker?.map(t => this.sql`d.ticker LIKE LOWER(${t}) || '%'`)
|
||||
args.ticker?.map(t => this.sql`b.ticker LIKE LOWER(${t}) || '%'`)
|
||||
);
|
||||
// Change selection table depending if we're filtering by block height or not.
|
||||
const results = await this.sql<(DbBrc20Balance & { total: number })[]>`
|
||||
SELECT
|
||||
b.ticker, (SELECT decimals FROM tokens WHERE ticker = b.ticker) AS decimals,
|
||||
b.avail_balance, b.trans_balance, b.total_balance, COUNT(*) OVER() as total
|
||||
${
|
||||
args.block_height
|
||||
? this.sql`
|
||||
SELECT
|
||||
d.ticker, d.decimals,
|
||||
SUM(b.avail_balance) AS avail_balance,
|
||||
SUM(b.trans_balance) AS trans_balance,
|
||||
SUM(b.avail_balance + b.trans_balance) AS total_balance,
|
||||
COUNT(*) OVER() as total
|
||||
FROM operations AS b
|
||||
INNER JOIN tokens AS d ON d.ticker = b.ticker
|
||||
FROM balances_history b
|
||||
INNER JOIN (
|
||||
SELECT ticker, address, MAX(block_height) as max_block_height
|
||||
FROM balances_history
|
||||
WHERE address = ${args.address} AND block_height <= ${args.block_height}
|
||||
GROUP BY ticker, address
|
||||
) latest ON b.ticker = latest.ticker AND b.address = latest.address AND b.block_height = latest.max_block_height
|
||||
WHERE
|
||||
b.address = ${args.address}
|
||||
AND b.block_height <= ${args.block_height}
|
||||
b.total_balance > 0
|
||||
${ticker ? this.sql`AND ${ticker}` : this.sql``}
|
||||
GROUP BY d.ticker, d.decimals
|
||||
HAVING SUM(b.avail_balance + b.trans_balance) > 0
|
||||
`
|
||||
: this.sql`
|
||||
SELECT d.ticker, d.decimals, b.avail_balance, b.trans_balance, b.total_balance, COUNT(*) OVER() as total
|
||||
FROM balances AS b
|
||||
INNER JOIN tokens AS d ON d.ticker = b.ticker
|
||||
WHERE
|
||||
b.total_balance > 0
|
||||
AND b.address = ${args.address}
|
||||
${ticker ? this.sql`AND ${ticker}` : this.sql``}
|
||||
`
|
||||
}
|
||||
ORDER BY b.total_balance DESC
|
||||
LIMIT ${args.limit}
|
||||
OFFSET ${args.offset}
|
||||
`;
|
||||
|
||||
@@ -1400,4 +1400,231 @@ describe('BRC-20 API', () => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/brc-20/balances', () => {
|
||||
test('address balance history is accurate', async () => {
|
||||
// Setup
|
||||
const numbers = incrementing(0);
|
||||
const addressA = 'bc1q6uwuet65rm6xvlz7ztw2gvdmmay5uaycu03mqz';
|
||||
const addressB = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
|
||||
|
||||
// A deploys pepe
|
||||
let transferHash = randomHash();
|
||||
let blockHash = randomHash();
|
||||
await brc20TokenDeploy(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
display_ticker: 'pepe',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
block_height: '780000',
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
address: addressA,
|
||||
max: '21000000000000000000000000',
|
||||
limit: '21000000000000000000000000',
|
||||
decimals: 18,
|
||||
self_mint: false,
|
||||
minted_supply: '0',
|
||||
tx_count: 1,
|
||||
timestamp: 1677803510,
|
||||
operation: 'deploy',
|
||||
ordinal_number: '20000',
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
to_address: null,
|
||||
amount: '0',
|
||||
});
|
||||
// A mints 10000 pepe
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'mint',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
ordinal_number: '200000',
|
||||
block_height: '780050',
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '10000000000000000000000',
|
||||
});
|
||||
// A mints 10000 pepe again
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'mint',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
ordinal_number: '200000',
|
||||
block_height: '780060',
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '10000000000000000000000',
|
||||
});
|
||||
// B mints 10000 pepe
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'mint',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
ordinal_number: '200000',
|
||||
block_height: '780070',
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressB,
|
||||
to_address: null,
|
||||
amount: '10000000000000000000000',
|
||||
});
|
||||
|
||||
// A deploys test
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20TokenDeploy(brc20Db.sql, {
|
||||
ticker: 'test',
|
||||
display_ticker: 'test',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
block_height: '780100',
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
address: addressA,
|
||||
max: '21000000000000000000000000',
|
||||
limit: '21000000000000000000000000',
|
||||
decimals: 18,
|
||||
self_mint: false,
|
||||
minted_supply: '0',
|
||||
tx_count: 1,
|
||||
timestamp: 1677803510,
|
||||
operation: 'deploy',
|
||||
ordinal_number: '20000',
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
to_address: null,
|
||||
amount: '0',
|
||||
});
|
||||
// A mints 10000 test
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'test',
|
||||
operation: 'mint',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
ordinal_number: '200000',
|
||||
block_height: '780200',
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '10000000000000000000000',
|
||||
});
|
||||
|
||||
// Verify balance history across block intervals
|
||||
let response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
let json = response.json();
|
||||
expect(json.total).toBe(2);
|
||||
expect(json.results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
available_balance: '20000.000000000000000000',
|
||||
overall_balance: '20000.000000000000000000',
|
||||
ticker: 'pepe',
|
||||
transferrable_balance: '0.000000000000000000',
|
||||
},
|
||||
{
|
||||
available_balance: '10000.000000000000000000',
|
||||
overall_balance: '10000.000000000000000000',
|
||||
ticker: 'test',
|
||||
transferrable_balance: '0.000000000000000000',
|
||||
},
|
||||
])
|
||||
);
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}?block_height=780200`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(2);
|
||||
expect(json.results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
available_balance: '20000.000000000000000000',
|
||||
overall_balance: '20000.000000000000000000',
|
||||
ticker: 'pepe',
|
||||
transferrable_balance: '0.000000000000000000',
|
||||
},
|
||||
{
|
||||
available_balance: '10000.000000000000000000',
|
||||
overall_balance: '10000.000000000000000000',
|
||||
ticker: 'test',
|
||||
transferrable_balance: '0.000000000000000000',
|
||||
},
|
||||
])
|
||||
);
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}?block_height=780200&ticker=te`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(1);
|
||||
expect(json.results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
available_balance: '10000.000000000000000000',
|
||||
overall_balance: '10000.000000000000000000',
|
||||
ticker: 'test',
|
||||
transferrable_balance: '0.000000000000000000',
|
||||
},
|
||||
])
|
||||
);
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}?block_height=780050`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(1);
|
||||
expect(json.results).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
available_balance: '10000.000000000000000000',
|
||||
overall_balance: '10000.000000000000000000',
|
||||
ticker: 'pepe',
|
||||
transferrable_balance: '0.000000000000000000',
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -490,6 +490,20 @@ export async function brc20Operation(sql: PgSqlClient, operation: TestBrc20Opera
|
||||
trans_balance = balances.trans_balance + EXCLUDED.trans_balance,
|
||||
total_balance = balances.avail_balance + EXCLUDED.total_balance
|
||||
`;
|
||||
await sql`
|
||||
INSERT INTO balances_history
|
||||
(ticker, address, block_height, avail_balance, trans_balance, total_balance)
|
||||
(
|
||||
SELECT ticker, address, ${operation.block_height} AS block_height, avail_balance,
|
||||
trans_balance, total_balance
|
||||
FROM balances
|
||||
WHERE address = ${operation.address} AND ticker = ${operation.ticker}
|
||||
)
|
||||
ON CONFLICT (address, block_height, ticker) DO UPDATE SET
|
||||
avail_balance = EXCLUDED.avail_balance,
|
||||
trans_balance = EXCLUDED.trans_balance,
|
||||
total_balance = EXCLUDED.total_balance
|
||||
`;
|
||||
}
|
||||
|
||||
/** Generate a random hash like string for testing */
|
||||
|
||||
Reference in New Issue
Block a user