mirror of
https://github.com/alexgo-io/bitcoin-indexer.git
synced 2026-01-12 08:34:17 +08:00
feat(brc20): add transferable-inscriptions endpoint
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
NotFoundResponse,
|
||||
OffsetParam,
|
||||
PaginatedResponse,
|
||||
Brc20TransferableInscriptionsResponseSchema,
|
||||
} from '../schemas';
|
||||
import { handleInscriptionTransfersCache } from '../util/cache';
|
||||
import {
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
parseBrc20Holders,
|
||||
parseBrc20Supply,
|
||||
parseBrc20Tokens,
|
||||
parseBrc20TransferableInscriptions,
|
||||
} from '../util/helpers';
|
||||
|
||||
export const Brc20Routes: FastifyPluginCallback<
|
||||
@@ -237,5 +239,49 @@ export const Brc20Routes: FastifyPluginCallback<
|
||||
}
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/brc-20/balances/:address/transferable',
|
||||
{
|
||||
schema: {
|
||||
operationId: 'getBrc20TransferableInscriptions',
|
||||
summary: 'BRC-20 Transferable Inscriptions',
|
||||
description: 'Retrieves BRC-20 transferable inscriptions for a Bitcoin address',
|
||||
tags: ['BRC-20'],
|
||||
params: Type.Object({
|
||||
address: AddressParam,
|
||||
}),
|
||||
querystring: Type.Object({
|
||||
ticker: Type.Optional(Brc20TickersParam),
|
||||
// Pagination
|
||||
offset: Type.Optional(OffsetParam),
|
||||
limit: Type.Optional(LimitParam),
|
||||
}),
|
||||
response: {
|
||||
200: PaginatedResponse(
|
||||
Brc20TransferableInscriptionsResponseSchema,
|
||||
'Paginated BRC-20 Transferable Inscriptions Response'
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const limit = request.query.limit ?? DEFAULT_API_LIMIT;
|
||||
const offset = request.query.offset ?? 0;
|
||||
const balances = await fastify.brc20Db.getTransferableInscriptions({
|
||||
limit,
|
||||
offset,
|
||||
address: request.params.address,
|
||||
ticker: request.query.ticker,
|
||||
});
|
||||
|
||||
await reply.send({
|
||||
limit,
|
||||
offset,
|
||||
total: balances.total,
|
||||
results: parseBrc20TransferableInscriptions(balances.results),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
@@ -487,6 +487,19 @@ export const Brc20ActivityResponseSchema = Type.Object({
|
||||
});
|
||||
export type Brc20ActivityResponse = Static<typeof Brc20ActivityResponseSchema>;
|
||||
|
||||
export const Brc20TransferableInscriptionsResponseSchema = Type.Object({
|
||||
inscription_number: Type.Integer({ examples: [1095397] }),
|
||||
inscription_id: Type.String({
|
||||
examples: ['ffb9df4532e05fe514765bcbeebb75c38d8f1774a291f9d6f123d68820cc39cdi0'],
|
||||
}),
|
||||
ordinal_number: Type.String({ examples: ['300000'] }),
|
||||
amount: Type.String({ examples: ['30000000000000000000'] }),
|
||||
ticker: Type.String({ examples: ['meme'] }),
|
||||
});
|
||||
export type Brc20TransferableInscriptionsResponse = Static<
|
||||
typeof Brc20TransferableInscriptionsResponseSchema
|
||||
>;
|
||||
|
||||
export const Brc20TokenResponseSchema = Type.Object(
|
||||
{
|
||||
id: Type.String({
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DbBrc20Holder,
|
||||
DbBrc20Token,
|
||||
DbBrc20TokenWithSupply,
|
||||
DbBrc20TransferableInscription,
|
||||
} from '../../pg/brc20/types';
|
||||
import {
|
||||
DbFullyLocatedInscriptionResult,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
Brc20HolderResponse,
|
||||
Brc20Supply,
|
||||
Brc20TokenResponse,
|
||||
Brc20TransferableInscriptionsResponse,
|
||||
InscriptionLocationResponse,
|
||||
InscriptionResponseType,
|
||||
} from '../schemas';
|
||||
@@ -244,6 +246,18 @@ export function parseBrc20Holders(items: DbBrc20Holder[]): Brc20HolderResponse[]
|
||||
}));
|
||||
}
|
||||
|
||||
export function parseBrc20TransferableInscriptions(
|
||||
items: DbBrc20TransferableInscription[]
|
||||
): Brc20TransferableInscriptionsResponse[] {
|
||||
return items.map(i => ({
|
||||
inscription_number: parseInt(i.inscription_number),
|
||||
inscription_id: i.inscription_id,
|
||||
amount: i.amount,
|
||||
ticker: i.ticker,
|
||||
ordinal_number: i.ordinal_number,
|
||||
}));
|
||||
}
|
||||
|
||||
export function parseSatPoint(satpoint: string): {
|
||||
tx_id: string;
|
||||
vout: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DbBrc20Holder,
|
||||
DbBrc20Token,
|
||||
DbBrc20TokenWithSupply,
|
||||
DbBrc20TransferableInscription,
|
||||
} from './types';
|
||||
import { Brc20TokenOrderBy } from '../../api/schemas';
|
||||
import { objRemoveUndefinedValues } from '../helpers';
|
||||
@@ -88,13 +89,13 @@ export class Brc20PgStore extends BasePgStore {
|
||||
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
|
||||
b.avail_balance, b.trans_balance, b.total_balance, COUNT(*) OVER() AS total
|
||||
${
|
||||
args.block_height
|
||||
? this.sql`
|
||||
FROM balances_history b
|
||||
INNER JOIN (
|
||||
SELECT ticker, address, MAX(block_height) as max_block_height
|
||||
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
|
||||
@@ -121,6 +122,40 @@ export class Brc20PgStore extends BasePgStore {
|
||||
};
|
||||
}
|
||||
|
||||
async getTransferableInscriptions(
|
||||
args: {
|
||||
address: string;
|
||||
ticker?: string[];
|
||||
} & DbInscriptionIndexPaging
|
||||
): Promise<DbPaginatedResult<DbBrc20TransferableInscription>> {
|
||||
const results = await this.sql<(DbBrc20TransferableInscription & { total: number })[]>`
|
||||
SELECT
|
||||
o1.inscription_number, o1.inscription_id, o1.ordinal_number, o1.amount, o1.ticker, COUNT(*) OVER() AS total
|
||||
FROM operations AS o1
|
||||
WHERE o1.operation = 'transfer'
|
||||
AND o1.address = ${args.address}
|
||||
${
|
||||
args.ticker
|
||||
? this.sql`AND LOWER(o1.ticker) IN (${args.ticker.map(t => t.toLowerCase())})`
|
||||
: this.sql``
|
||||
}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM operations o2
|
||||
WHERE o2.inscription_id = o1.inscription_id
|
||||
AND o2.operation = 'transfer_send'
|
||||
)
|
||||
ORDER BY o1.block_height DESC, o1.tx_index DESC
|
||||
LIMIT ${args.limit}
|
||||
OFFSET ${args.offset}
|
||||
`;
|
||||
|
||||
return {
|
||||
total: results[0]?.total ?? 0,
|
||||
results: results ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getToken(args: { ticker: string }): Promise<DbBrc20TokenWithSupply | undefined> {
|
||||
const result = await this.sql<DbBrc20TokenWithSupply[]>`
|
||||
WITH token AS (
|
||||
|
||||
@@ -63,3 +63,11 @@ export type DbBrc20Activity = {
|
||||
deploy_max: string;
|
||||
deploy_limit: string | null;
|
||||
};
|
||||
|
||||
export type DbBrc20TransferableInscription = {
|
||||
inscription_number: string;
|
||||
inscription_id: string;
|
||||
amount: string;
|
||||
ticker: string;
|
||||
ordinal_number: string;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { buildApiServer } from '../../src/api/init';
|
||||
import { Brc20TokenResponse, Brc20ActivityResponse } from '../../src/api/schemas';
|
||||
import {
|
||||
Brc20TokenResponse,
|
||||
Brc20ActivityResponse,
|
||||
Brc20TransferableInscriptionsResponse,
|
||||
} from '../../src/api/schemas';
|
||||
import { Brc20PgStore } from '../../src/pg/brc20/brc20-pg-store';
|
||||
import { PgStore } from '../../src/pg/pg-store';
|
||||
import {
|
||||
@@ -1627,4 +1631,479 @@ describe('BRC-20 API', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/brc-20/balances/:address/transferable', () => {
|
||||
test('returns 202 if address has no transferable inscriptions', async () => {
|
||||
const response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td/transferable`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const json = response.json();
|
||||
expect(json.total).toBe(0);
|
||||
expect(json.results).toEqual([]);
|
||||
});
|
||||
|
||||
test('transferable inscriptions are accurate', async () => {
|
||||
// This test verifies that the transferable inscriptions API correctly tracks BRC-20 tokens
|
||||
// that are available for transfer by an address. The test follows this flow:
|
||||
// 1. Address A deploys two tokens: 'pepe' and 'meme'
|
||||
// 2. Address A mints 10000 'pepe' and 20000 'meme'
|
||||
// 3. Address B mints 10000 'pepe'
|
||||
// 4. Address A creates a transfer inscription for 9000 'pepe'
|
||||
// 5. Verify that A's transferable inscriptions include this 'pepe' transfer
|
||||
// 6. A sends the 'pepe' transfer inscription to B
|
||||
// 7. Verify that A no longer has any transferable inscriptions
|
||||
// 8. A creates transfer inscriptions for 500 'pepe' and 2000 'meme'
|
||||
// 9. Verify that A has both transferable inscriptions
|
||||
// 11. Verify that A only has the 'meme' transferable inscription if querying for 'meme' ticker
|
||||
// 12. Verify that A only has the 'pepe' transferable inscription if querying for 'pepe' ticker
|
||||
// 13. Verify that A only has no transferable inscription if querying for 'rere' ticker
|
||||
// 14. A sends the 500 'pepe' transfer to B
|
||||
// 15. Verify that A only has the 'meme' transferable inscription left
|
||||
// 16. A sends the 2000 'meme' transfer to B
|
||||
// 17. Verify that A has no transferable inscriptions left
|
||||
|
||||
// Setup
|
||||
const blockHeights = incrementing(767430);
|
||||
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: blockHeights.next().value.toString(),
|
||||
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 deploys meme
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20TokenDeploy(brc20Db.sql, {
|
||||
ticker: 'meme',
|
||||
display_ticker: 'meme',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
block_height: blockHeights.next().value.toString(),
|
||||
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: '30000',
|
||||
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: blockHeights.next().value.toString(),
|
||||
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 20000 meme
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'meme',
|
||||
operation: 'mint',
|
||||
inscription_id: `${transferHash}i0`,
|
||||
inscription_number: numbers.next().value.toString(),
|
||||
ordinal_number: '300000',
|
||||
block_height: blockHeights.next().value.toString(),
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '20000000000000000000000',
|
||||
});
|
||||
|
||||
// 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: blockHeights.next().value.toString(),
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressB,
|
||||
to_address: null,
|
||||
amount: '10000000000000000000000',
|
||||
});
|
||||
|
||||
// A creates transfer of 9000 pepe
|
||||
blockHash = randomHash();
|
||||
const inscriptionNumberMocked = numbers.next().value.toString();
|
||||
const blockHeightMocked = blockHeights.next().value.toString();
|
||||
const transferHashMocked = randomHash();
|
||||
transferHash = transferHashMocked;
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer',
|
||||
inscription_id: `${transferHashMocked}i0`,
|
||||
inscription_number: inscriptionNumberMocked,
|
||||
ordinal_number: '200000',
|
||||
block_height: blockHeightMocked,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHashMocked,
|
||||
tx_index: 0,
|
||||
output: `${transferHashMocked}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '9000000000000000000000',
|
||||
});
|
||||
|
||||
let response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
let json = response.json();
|
||||
expect(json.total).toBe(1);
|
||||
expect(json.results).toEqual([
|
||||
{
|
||||
inscription_id: `${transferHash}i0`,
|
||||
ticker: 'pepe',
|
||||
inscription_number: parseInt(inscriptionNumberMocked),
|
||||
ordinal_number: '200000',
|
||||
amount: '9000000000000000000000',
|
||||
} as Brc20TransferableInscriptionsResponse,
|
||||
]);
|
||||
|
||||
// A sends transfer inscription to B (aka transfer/sale)
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
const blockHeightMockedSend = blockHeights.next().value.toString();
|
||||
const inscriptionNumberMockedSend = numbers.next().value.toString();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer_send',
|
||||
inscription_id: `${transferHashMocked}i0`,
|
||||
inscription_number: inscriptionNumberMockedSend,
|
||||
ordinal_number: '200000',
|
||||
block_height: blockHeightMockedSend,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: addressB,
|
||||
amount: '9000000000000000000000',
|
||||
});
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer_receive',
|
||||
inscription_id: `${transferHashMocked}i0`,
|
||||
inscription_number: inscriptionNumberMockedSend,
|
||||
ordinal_number: '200000',
|
||||
block_height: blockHeightMockedSend,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressB,
|
||||
to_address: null,
|
||||
amount: '9000000000000000000000',
|
||||
});
|
||||
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(0);
|
||||
expect(json.results).toEqual([]);
|
||||
|
||||
// A creates transfer of 500 pepe and 2000 meme
|
||||
blockHash = randomHash();
|
||||
const inscriptionNumberMocked2 = numbers.next().value.toString();
|
||||
const blockHeightMocked2 = blockHeights.next().value.toString();
|
||||
const transferHashMocked2 = randomHash();
|
||||
const inscriptionNumberMocked3 = numbers.next().value.toString();
|
||||
const blockHeightMocked3 = blockHeights.next().value.toString();
|
||||
const transferHashMocked3 = randomHash();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer',
|
||||
inscription_id: `${transferHashMocked2}i0`,
|
||||
inscription_number: inscriptionNumberMocked2,
|
||||
ordinal_number: '200000',
|
||||
block_height: blockHeightMocked2,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHashMocked2,
|
||||
tx_index: 0,
|
||||
output: `${transferHashMocked2}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '500000000000000000000',
|
||||
});
|
||||
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'meme',
|
||||
operation: 'transfer',
|
||||
inscription_id: `${transferHashMocked3}i0`,
|
||||
inscription_number: inscriptionNumberMocked3,
|
||||
ordinal_number: '300000',
|
||||
block_height: blockHeightMocked3,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHashMocked3,
|
||||
tx_index: 0,
|
||||
output: `${transferHashMocked3}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: null,
|
||||
amount: '2000000000000000000000',
|
||||
});
|
||||
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable`,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(2);
|
||||
expect(json.results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
inscription_id: `${transferHashMocked2}i0`,
|
||||
ticker: 'pepe',
|
||||
inscription_number: parseInt(inscriptionNumberMocked2),
|
||||
ordinal_number: '200000',
|
||||
amount: '500000000000000000000',
|
||||
} as Brc20TransferableInscriptionsResponse),
|
||||
expect.objectContaining({
|
||||
inscription_id: `${transferHashMocked3}i0`,
|
||||
ticker: 'meme',
|
||||
inscription_number: parseInt(inscriptionNumberMocked3),
|
||||
ordinal_number: '300000',
|
||||
amount: '2000000000000000000000',
|
||||
} as Brc20TransferableInscriptionsResponse),
|
||||
])
|
||||
);
|
||||
|
||||
// Verify that A only has the 'meme' transferable inscription if querying for 'meme' ticker
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable?ticker=meme`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(1);
|
||||
expect(json.results).toEqual([
|
||||
expect.objectContaining({
|
||||
inscription_id: `${transferHashMocked3}i0`,
|
||||
ticker: 'meme',
|
||||
inscription_number: parseInt(inscriptionNumberMocked3),
|
||||
ordinal_number: '300000',
|
||||
amount: '2000000000000000000000',
|
||||
} as Brc20TransferableInscriptionsResponse),
|
||||
]);
|
||||
|
||||
// Verify that A only has the 'pepe' transferable inscription if querying for 'pepe' ticker
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable?ticker=pepe`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(1);
|
||||
expect(json.results).toEqual([
|
||||
expect.objectContaining({
|
||||
inscription_id: `${transferHashMocked2}i0`,
|
||||
ticker: 'pepe',
|
||||
inscription_number: parseInt(inscriptionNumberMocked2),
|
||||
ordinal_number: '200000',
|
||||
amount: '500000000000000000000',
|
||||
} as Brc20TransferableInscriptionsResponse),
|
||||
]);
|
||||
|
||||
// Verify that A only has no transferable inscription if querying for 'rere' ticker
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable?ticker=rere`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(0);
|
||||
expect(json.results).toEqual([]);
|
||||
|
||||
// A sends transfer inscription to B (aka transfer/sale) of pepe
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
const blockHeightMockedSend4 = blockHeights.next().value.toString();
|
||||
const inscriptionNumberMockedSend4 = numbers.next().value.toString();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer_send',
|
||||
inscription_id: `${transferHashMocked2}i0`,
|
||||
inscription_number: inscriptionNumberMockedSend4,
|
||||
ordinal_number: '200000',
|
||||
block_height: blockHeightMockedSend4,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: addressB,
|
||||
amount: '500000000000000000000',
|
||||
});
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer_receive',
|
||||
inscription_id: `${transferHashMocked2}i0`,
|
||||
inscription_number: inscriptionNumberMockedSend4,
|
||||
ordinal_number: '200000',
|
||||
block_height: blockHeightMockedSend4,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressB,
|
||||
to_address: null,
|
||||
amount: '500000000000000000000',
|
||||
});
|
||||
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(1);
|
||||
expect(json.results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
inscription_id: `${transferHashMocked3}i0`,
|
||||
ticker: 'meme',
|
||||
inscription_number: parseInt(inscriptionNumberMocked3),
|
||||
ordinal_number: '300000',
|
||||
amount: '2000000000000000000000',
|
||||
} as Brc20TransferableInscriptionsResponse),
|
||||
])
|
||||
);
|
||||
|
||||
// A sends transfer inscription to B (aka transfer/sale) of meme
|
||||
transferHash = randomHash();
|
||||
blockHash = randomHash();
|
||||
const blockHeightMockedSend5 = blockHeights.next().value.toString();
|
||||
const inscriptionNumberMockedSend5 = numbers.next().value.toString();
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'pepe',
|
||||
operation: 'transfer_send',
|
||||
inscription_id: `${transferHashMocked3}i0`,
|
||||
inscription_number: inscriptionNumberMockedSend5,
|
||||
ordinal_number: '300000',
|
||||
block_height: blockHeightMockedSend5,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressA,
|
||||
to_address: addressB,
|
||||
amount: '2000000000000000000000',
|
||||
});
|
||||
await brc20Operation(brc20Db.sql, {
|
||||
ticker: 'meme',
|
||||
operation: 'transfer_receive',
|
||||
inscription_id: `${transferHashMocked3}i0`,
|
||||
inscription_number: inscriptionNumberMockedSend5,
|
||||
ordinal_number: '300000',
|
||||
block_height: blockHeightMockedSend5,
|
||||
block_hash: blockHash,
|
||||
tx_id: transferHash,
|
||||
tx_index: 0,
|
||||
output: `${transferHash}:0`,
|
||||
offset: '0',
|
||||
timestamp: 1677803510,
|
||||
address: addressB,
|
||||
to_address: null,
|
||||
amount: '2000000000000000000000',
|
||||
});
|
||||
|
||||
response = await fastify.inject({
|
||||
method: 'GET',
|
||||
url: `/ordinals/brc-20/balances/${addressA}/transferable`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
json = response.json();
|
||||
expect(json.total).toBe(0);
|
||||
expect(json.results).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user