Skip to content

Commit

Permalink
feat: filter BRC-20 activity by address (#226)
Browse files Browse the repository at this point in the history
* feat: filter activity by address

* fix: block height filter should be at block

* test: block height filter counts
  • Loading branch information
rafaelcr committed Sep 18, 2023
1 parent f6bc735 commit 55eaeac
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 3 deletions.
5 changes: 4 additions & 1 deletion src/api/routes/brc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,14 @@ export const Brc20Routes: FastifyPluginCallback<
schema: {
operationId: 'getBrc20Activity',
summary: 'BRC-20 Activity',
description: 'Retrieves BRC-20 activity',
description:
'Retrieves BRC-20 activity filtered by ticker, address, operation, or at a specific block height',
tags: ['BRC-20'],
querystring: Type.Object({
ticker: Type.Optional(Brc20TickersParam),
block_height: Type.Optional(BlockHeightParam),
operation: Type.Optional(Brc20OperationsParam),
address: Type.Optional(AddressParam),
// Pagination
offset: Type.Optional(OffsetParam),
limit: Type.Optional(LimitParam),
Expand All @@ -223,6 +225,7 @@ export const Brc20Routes: FastifyPluginCallback<
? parseInt(request.query.block_height)
: undefined,
operation: request.query.operation,
address: request.query.address,
}
);
await reply.send({
Expand Down
13 changes: 11 additions & 2 deletions src/pg/brc20/brc20-pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,7 @@ export class Brc20PgStore extends BasePgStoreModule {
ticker?: string[];
block_height?: number;
operation?: string[];
address?: string;
}
): Promise<DbPaginatedResult<DbBrc20Activity>> {
objRemoveUndefinedValues(filters);
Expand Down Expand Up @@ -710,14 +711,22 @@ export class Brc20PgStore extends BasePgStoreModule {
FROM brc20_events AS e
INNER JOIN brc20_deploys AS d ON e.brc20_deploy_id = d.id
INNER JOIN locations AS l ON e.genesis_location_id = l.id
${
filters.address
? this.sql`LEFT JOIN brc20_transfers AS t ON t.id = e.transfer_id`
: this.sql``
}
WHERE TRUE
${
filters.operation ? this.sql`AND operation IN ${this.sql(filters.operation)}` : this.sql``
}
${tickerConditions ? this.sql`AND (${tickerConditions})` : this.sql``}
${
filters.block_height
? this.sql`AND l.block_height <= ${filters.block_height}`
filters.block_height ? this.sql`AND l.block_height = ${filters.block_height}` : this.sql``
}
${
filters.address
? this.sql`AND (l.address = ${filters.address} OR t.from_address = ${filters.address})`
: this.sql``
}
ORDER BY l.block_height DESC, l.tx_index DESC
Expand Down
155 changes: 155 additions & 0 deletions tests/brc20.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2595,6 +2595,33 @@ describe('BRC-20', () => {
} as Brc20ActivityResponse),
])
);
response = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`,
});
expect(response.statusCode).toBe(200);
json = response.json();
expect(json.total).toBe(2);
expect(json.results).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: 'deploy',
ticker: 'PEPE',
address: addressA,
deploy: expect.objectContaining({
max_supply: '21000000.000000000000000000',
}),
} as Brc20ActivityResponse),
expect.objectContaining({
operation: 'mint',
ticker: 'PEPE',
address: addressA,
mint: {
amount: '1000.000000000000000000',
},
} as Brc20ActivityResponse),
])
);

// Step 3: B mints 2000 of the token
await db.updateInscriptions(
Expand Down Expand Up @@ -2684,6 +2711,27 @@ describe('BRC-20', () => {
} as Brc20ActivityResponse),
])
);
response = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`,
});
expect(response.statusCode).toBe(200);
json = response.json();
expect(json.total).toBe(3);
expect(json.results).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: 'transfer',
ticker: 'PEPE',
address: addressA,
tx_id: transferHashAB,
transfer: {
amount: '1000.000000000000000000',
from_address: addressA,
},
} as Brc20ActivityResponse),
])
);

// Step 5: B creates a transfer to C
const transferHashBC = randomHash();
Expand Down Expand Up @@ -2748,6 +2796,52 @@ describe('BRC-20', () => {
})
.build()
);
// A gets the transfer send in its feed
response = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressA}`,
});
expect(response.statusCode).toBe(200);
json = response.json();
expect(json.total).toBe(4);
expect(json.results).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: 'transfer_send',
ticker: 'PEPE',
tx_id: expect.not.stringMatching(transferHashAB),
address: addressB,
transfer_send: {
amount: '1000.000000000000000000',
from_address: addressA,
to_address: addressB,
},
} as Brc20ActivityResponse),
])
);
// B gets the transfer send in its feed
response = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressB}`,
});
expect(response.statusCode).toBe(200);
json = response.json();
expect(json.total).toBe(3);
expect(json.results).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: 'transfer_send',
ticker: 'PEPE',
tx_id: expect.not.stringMatching(transferHashAB),
address: addressB,
transfer_send: {
amount: '1000.000000000000000000',
from_address: addressA,
to_address: addressB,
},
} as Brc20ActivityResponse),
])
);

// Verify that the PEPE transfer_send is in the activity feed
response = await fastify.inject({
Expand Down Expand Up @@ -2814,6 +2908,52 @@ describe('BRC-20', () => {
} as Brc20ActivityResponse),
])
);
// B gets the transfer send in its feed
response = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressB}`,
});
expect(response.statusCode).toBe(200);
json = response.json();
expect(json.total).toBe(4);
expect(json.results).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: 'transfer_send',
ticker: 'PEPE',
tx_id: expect.not.stringMatching(transferHashBC),
address: addressC,
transfer_send: {
amount: '2000.000000000000000000',
from_address: addressB,
to_address: addressC,
},
} as Brc20ActivityResponse),
])
);
// C gets the transfer send in its feed
response = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?ticker=PEPE&address=${addressC}`,
});
expect(response.statusCode).toBe(200);
json = response.json();
expect(json.total).toBe(1);
expect(json.results).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: 'transfer_send',
ticker: 'PEPE',
tx_id: expect.not.stringMatching(transferHashBC),
address: addressC,
transfer_send: {
amount: '2000.000000000000000000',
from_address: addressB,
to_address: addressC,
},
} as Brc20ActivityResponse),
])
);
});

test('activity for multiple token creation', async () => {
Expand Down Expand Up @@ -3209,6 +3349,14 @@ describe('BRC-20', () => {
expect(json.total).toBe(6);
expect(json.results).toHaveLength(6);
expect(json.results[0].operation).toBe('mint');
request = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?block_height=775622`,
});
json = request.json();
expect(json.total).toBe(1);
expect(json.results).toHaveLength(1);
expect(json.results[0].operation).toBe('mint');

// Rollback 1: 🔥 is un-minted
await db.updateInscriptions(
Expand Down Expand Up @@ -3264,6 +3412,13 @@ describe('BRC-20', () => {
expect(json.total).toBe(5);
expect(json.results).toHaveLength(5);
expect(json.results[0].operation).toBe('deploy');
request = await fastify.inject({
method: 'GET',
url: `/ordinals/brc-20/activity?block_height=775622`,
});
json = request.json();
expect(json.total).toBe(0);
expect(json.results).toHaveLength(0);

// Rollback 2: 🔥 is un-deployed
await db.updateInscriptions(
Expand Down

0 comments on commit 55eaeac

Please sign in to comment.