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

Commit

Permalink
Merge pull request #107 from ar-io/PE-5826-function-filter
Browse files Browse the repository at this point in the history
feat(interactions): allow filtering interactions by function name
  • Loading branch information
dtfiedler authored Mar 18, 2024
2 parents 217ec70 + e22f03a commit 1f86b6e
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 17 deletions.
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 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
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 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
);
mismatchedInteractionCount.inc();
}

return {
...interaction,
valid: validity[id] ?? false,
Expand All @@ -143,6 +146,20 @@ export async function contractInteractionsHandler(ctx: KoaContext) {
},
);

// 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
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

0 comments on commit 1f86b6e

Please sign in to comment.