Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

feat(interactions): allow filtering interactions by function name #107

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ components:
description: Evaluate the contract at up to a specific sort key. Only applicable if blockHeight is not provided.
schema:
type: string
function:
name: function
in: query
required: false
description: Filter contract interactions by provided function name.
example: 'evolve'
schema:
type: string
page:
name: page
in: query
Expand Down
7 changes: 7 additions & 0 deletions src/middleware/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const queryMiddleware = async (ctx: KoaContext, next: Next) => {
page = DEFAULT_PAGE,
pageSize = DEFAULT_PAGE_SIZE,
validity,
function: fn,
} = ctx.query;

logger.debug('Query params provided', {
Expand Down Expand Up @@ -88,5 +89,11 @@ export const queryMiddleware = async (ctx: KoaContext, next: Next) => {
ctx.state.validity = validity === 'true' || validity === '1';
}

// used for filtering functions on interactions
if (fn) {
logger.debug('Function provided', { fn });
ctx.state.fn = fn;
}

return next();
};
17 changes: 17 additions & 0 deletions src/routes/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@
blockHeight: requestedBlockHeight,
page: requestedPage,
pageSize: requestedPageSize = 100,
fn: requestedFunction,
} = ctx.state;
const { contractTxId, address } = ctx.params;

logger.debug('Fetching contract interactions', {
contractTxId,
sortKey: requestedSortKey,
blockHeight: requestedBlockHeight,
requestedFunction,
address,
});

Expand Down Expand Up @@ -134,6 +136,7 @@
);
mismatchedInteractionCount.inc();
}

return {
...interaction,
valid: validity[id] ?? false,
Expand All @@ -143,6 +146,20 @@
},
);

// TODO: allow other filters
if (requestedFunction) {
logger.debug('Filtering interactions by function', {
contractTxId,
sortKey: requestedSortKey,
blockHeight: requestedBlockHeight,
address,
requestedFunction,
});
mappedInteractions = mappedInteractions.filter(
(interaction) => interaction.input?.function === requestedFunction,
);
}

logger.debug('Sorting interactions', {
contractTxId,
sortKey: requestedSortKey,
Expand Down Expand Up @@ -473,8 +490,8 @@

const associatedRecords = Object.entries(records).reduce(
(
filteredRecords: { [x: string]: any },

Check warning on line 493 in src/routes/contract.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
[record, recordObj]: [string, any],

Check warning on line 494 in src/routes/contract.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
) => {
if (
!filteredContractTxIdsArray.length ||
Expand Down Expand Up @@ -571,7 +588,7 @@
}

const parsedInput = Object.entries(input).reduce(
(parsedParams: { [x: string]: any }, [key, value]) => {

Check warning on line 591 in src/routes/contract.ts

View workflow job for this annotation

GitHub Actions / build (lint:check)

Unexpected any. Specify a different type
// parse known integer values for parameters we care about
if (
queryParamsCastedToNumbers.includes(key) &&
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/arlocal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ export function handle(state, action) {
const input = action.input;
const caller = action.caller;

// test function
if (input.function === 'evolve') {
return { state };
}

if (input.function === 'transfer') {
const target = input.target;
const qty = input.qty;
Expand Down
121 changes: 104 additions & 17 deletions tests/integration/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,35 @@ describe('Integration tests', () => {
valid: true,
id: writeInteraction!.originalTxId,
});

await arweave.api.get('mine');

// another interaction to test filtering
const secondInteraction = await contract.writeInteraction(
{
function: 'evolve',
},
{
disableBundling: true,
},
);

const secondInteractionBlock = await arweave.blocks.getCurrent();
contractInteractions.push({
height: secondInteractionBlock.height,
input: { function: 'evolve' },
owner: walletAddress,
timestamp: Math.floor(secondInteractionBlock.timestamp / 1000),
sortKey: await interactionSorter.createSortKey(
secondInteractionBlock.indep_hash,
secondInteraction!.originalTxId,
secondInteractionBlock.height,
),
valid: true,
id: secondInteraction!.originalTxId,
});
// reverse the interactions to match the service behavior
contractInteractions.reverse();
});

describe('general routes', () => {
Expand Down Expand Up @@ -354,7 +383,7 @@ describe('Integration tests', () => {
});

it('should only return interactions up to a provided sort key height', async () => {
const knownSortKey = contractInteractions[0].sortKey;
const knownSortKey = contractInteractions.slice(1)[0].sortKey;
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions?sortKey=${knownSortKey}`,
);
Expand All @@ -363,7 +392,7 @@ describe('Integration tests', () => {
const { contractTxId, interactions, sortKey } = data;
expect(contractTxId).to.equal(id);
expect(sortKey).to.equal(knownSortKey);
expect(interactions).to.deep.equal([contractInteractions[0]]);
expect(interactions).to.deep.equal(contractInteractions.slice(1));
});

it('should return the first page of contract interactions when page and page size are provided', async () => {
Expand All @@ -377,28 +406,30 @@ describe('Integration tests', () => {
expect(pages).to.deep.equal({
page: 1,
pageSize: 1,
totalPages: 1,
totalPages: contractInteractions.length,
totalItems: contractInteractions.length,
hasNextPage: false,
hasNextPage: contractInteractions.length > 1,
});
expect(contractTxId).to.equal(id);
expect(interactions).to.deep.equal([contractInteractions[0]]);
expect(interactions).to.deep.equal(contractInteractions.slice(0, 1));
});

it('should return an empty array of contract interactions when page is greater than the total number of pages', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions?page=2&pageSize=1`,
`/v1/contract/${id}/interactions?page=${
contractInteractions.length + 1
}&pageSize=1`,
);
expect(status).to.equal(200);
expect(data).to.not.be.undefined;
const { contractTxId, interactions, sortKey, pages } = data;
expect(sortKey).to.not.be.undefined;
expect(pages).to.deep.equal({
page: 2,
page: contractInteractions.length + 1,
pageSize: 1,
totalPages: 1,
totalPages: contractInteractions.length,
totalItems: contractInteractions.length,
hasNextPage: false,
hasNextPage: contractInteractions.length > 2,
});
expect(contractTxId).to.equal(id);
expect(interactions).to.deep.equal([]);
Expand All @@ -415,6 +446,32 @@ describe('Integration tests', () => {
);
expect(status).to.equal(400);
});

it('should return interactions that match a provided function', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions?function=evolve`,
);
expect(status).to.equal(200);
expect(data).to.not.be.undefined;
const { contractTxId, interactions, sortKey } = data;
expect(contractTxId).to.equal(id);
expect(sortKey).not.be.undefined;
expect(interactions).to.deep.equal(
contractInteractions.filter((i) => i.input?.function === 'evolve'),
);
});

it('should return an empty array for function query parameter that does not match any interactions', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions?function=fake`,
);
expect(status).to.equal(200);
expect(data).to.not.be.undefined;
const { contractTxId, interactions, sortKey } = data;
expect(contractTxId).to.equal(id);
expect(sortKey).not.be.undefined;
expect(interactions).to.deep.equal([]);
});
});

describe('/:contractTxId/interactions/:address', () => {
Expand All @@ -428,7 +485,35 @@ describe('Integration tests', () => {
expect(contractTxId).to.equal(id);
expect(sortKey).not.be.undefined;
// TODO: filter out interactions specific to the wallet address
expect(interactions).to.deep.equal(contractInteractions);
expect(interactions).to.deep.equal(
contractInteractions.sort((a, b) => b.height - a.height),
);
});

it('should return interactions that match a provided function', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions/${walletAddress}?function=evolve`,
);
expect(status).to.equal(200);
expect(data).to.not.be.undefined;
const { contractTxId, interactions, sortKey } = data;
expect(contractTxId).to.equal(id);
expect(sortKey).not.be.undefined;
expect(interactions).to.deep.equal(
contractInteractions.filter((i) => i.input?.function === 'evolve'),
);
});

it('should return an empty array for function query parameter that does not match any interactions', async () => {
const { status, data } = await axios.get(
`/v1/contract/${id}/interactions/${walletAddress}?function=fake`,
);
expect(status).to.equal(200);
expect(data).to.not.be.undefined;
const { contractTxId, interactions, sortKey } = data;
expect(contractTxId).to.equal(id);
expect(sortKey).not.be.undefined;
expect(interactions).to.deep.equal([]);
});
});

Expand Down Expand Up @@ -874,7 +959,9 @@ describe('Integration tests', () => {
hasNextPage: false,
});
expect(contractTxId).to.equal(id);
expect(interactions).to.deep.equal(contractInteractions);
expect(interactions).to.deep.equal(
contractInteractions.sort((a, b) => b.height - a.height),
);
});
it('should return the first page of interactions when page and page size are provided', async () => {
const { status, data } = await axios.get(
Expand All @@ -888,12 +975,12 @@ describe('Integration tests', () => {
expect(pages).to.deep.equal({
page: 1,
pageSize: 1,
totalPages: 1,
totalPages: contractInteractions.length,
totalItems: contractInteractions.length,
hasNextPage: false,
hasNextPage: contractInteractions.length > 1,
});
expect(contractTxId).to.equal(id);
expect(interactions).to.deep.equal([contractInteractions[0]]);
expect(interactions).to.deep.equal(contractInteractions.slice(0, 1));
});
it('should return the second page of interactions when page and page size are provided', async () => {
const { status, data } = await axios.get(
Expand All @@ -907,12 +994,12 @@ describe('Integration tests', () => {
expect(pages).to.deep.equal({
page: 2,
pageSize: 1,
totalPages: 1,
totalPages: contractInteractions.length,
totalItems: contractInteractions.length,
hasNextPage: false,
hasNextPage: contractInteractions.length > 2,
});
expect(contractTxId).to.equal(id);
expect(interactions).to.deep.equal([]);
expect(interactions).to.deep.equal(contractInteractions.slice(1));
});
it('should return a bad request error when invalid page size is provided', async () => {
const { status } = await axios.get(
Expand Down
Loading