Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block helper functions for successful function calls #60

Closed
wants to merge 2 commits into from
Closed
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
5 changes: 5 additions & 0 deletions .changeset/quiet-singers-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@near-lake/framework": patch
---

Upgrade near-primitives to 0.5.0
5 changes: 5 additions & 0 deletions .changeset/ten-suits-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@near-lake/primitives": minor
---

Block helper methods for successfulFunctionCalls and socialOperations
93,062 changes: 93,062 additions & 0 deletions blocks/117969098.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/near-lake-framework/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@near-lake/framework",
"version": "0.1.3",
"version": "0.1.4",
"description": "JS Library to connect to the NEAR Lake S3 and stream the data",
"author": "NEAR Inc <[email protected]>",
"keywords": [
Expand All @@ -27,6 +27,6 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.32.0",
"@near-lake/primitives": "^0.4.0"
"@near-lake/primitives": "^0.5.0"
}
}
2 changes: 1 addition & 1 deletion packages/near-lake-primitives/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@near-lake/primitives",
"version": "0.4.0",
"version": "0.5.0",
"keywords": [
"lake-primitives",
"near-indexer"
Expand Down
103 changes: 98 additions & 5 deletions packages/near-lake-primitives/src/types/block.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Action, Receipt } from './receipts';
import { StreamerMessage, ValidatorStakeView } from './core/types';
import { Action, Receipt, Operation, FunctionCall, FunctionCallWrapper } from './receipts';
import { StreamerMessage, ValidatorStakeView } from "./core/types";
import { Transaction } from './transactions';
import { Event, RawEvent, Log } from './events';
import { StateChange } from './stateChanges';

export type DecodedFunctionCall = {
methodName: string;
args: Record<string, any>;
receiptId: string;
}

/**
* The `Block` type is used to represent a block in the NEAR Lake Framework.
*
Expand Down Expand Up @@ -79,11 +85,14 @@ export class Block {
/**
* Returns an Array of `Actions` executed in the block.
*/
actions(): Action[] {
actions(successfulOnly?: boolean): Action[] {
const actions: Action[] = this.streamerMessage.shards
.flatMap((shard) => shard.receiptExecutionOutcomes)
.filter((exeuctionOutcomeWithReceipt) => Action.isActionReceipt(exeuctionOutcomeWithReceipt.receipt))
.map((exeuctionOutcomeWithReceipt) => Action.fromReceiptView(exeuctionOutcomeWithReceipt.receipt))
.filter((executionOutcomeWithReceipt) =>
Action.isActionReceipt(executionOutcomeWithReceipt.receipt) &&
(!successfulOnly || (executionOutcomeWithReceipt.executionOutcome.outcome.status as {SuccessValue: any}))
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion doesn't actually check if the action was only successful - it only checks if status exists, which it does for both successful/failure. as is just a type cast, and won't affect the outcome of this.

We need to check for the existence of status.SuccessValue, and perhaps status.SuccessReceiptId too, instead.

Copy link
Author

@gabehamilton gabehamilton Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkudinov hopefully your function is better than mine here.

.map((executionOutcomeWithReceipt) => Action.fromReceiptView(executionOutcomeWithReceipt.receipt))
.filter((action): action is Action => action !== null)
.map(action => action)
return actions
Expand Down Expand Up @@ -157,6 +166,90 @@ export class Block {
});
}

/**
* Decodes base64 encoded JSON data. Returns `undefined` if the data is not in the expected format.
* @param encodedValue
*/
base64decode(encodedValue: any) {
try {
const buff = Buffer.from(encodedValue, "base64");
const str = buff.toString("utf-8").replace(/\\xa0/g, " ");
return JSON.parse(str);
} catch (error) {
console.error(
'Error parsing base64 JSON - skipping data',
error
);
}
Comment on lines +179 to +182
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like the idea of libraries logging things, because they can't be suppressed.

Let's just return undefined explicitly here, without the log.

}

/**
* Returns an Array of `DecodedFunctionCall` for the given `contract` and `method` if provided.
* If the `method` is not provided, it returns all the `DecodedFunctionCall`s for the given `contract`.
* Arguments to the function call are decoded from base64 to JSON.
* @param contract
* @param method
*/
successfulFunctionCalls(contract: string, method?: string) : DecodedFunctionCall[] {
return this
.actions(true)
.filter((action) => action.receiverId === contract)
.flatMap((action: Action) =>
action.operations
.map((operation: Operation): FunctionCall => (operation as FunctionCallWrapper)?.FunctionCall)
.filter((functionCallOperation) => functionCallOperation && (!method || functionCallOperation?.methodName === method))
.map((functionCallOperation) => ({
...functionCallOperation,
args: this.base64decode(functionCallOperation.args),
receiptId: action.receiptId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
args: this.base64decode(functionCallOperation.args),
decoded_args: this.base64decode(functionCallOperation.args)

It may be better to expose this as decoded_args, as well as the raw args. base64decode will silently fail, so having both allows developers to use the raw args if decoded_args is undefined.

}))
);
};

/**
* Returns data that follows the social.near contract template for key value data.
* @param operation The top level key to search for in each user's account space
* @param contract Defaults to 'social.near', pass in a different contract if needed
* @param method Defaults to 'set', pass in a different method if needed
*/
socialOperations(operation: string, contract: string = "social.near", method: string = "set") {
return this.successfulFunctionCalls(contract, method)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a return type here please?

.filter((functionCall) => {
if (
!functionCall ||
!functionCall.args ||
!functionCall.args.data ||
!Object.keys(functionCall.args.data) ||
!Object.keys(functionCall.args.data)[0]
) {
console.error(
"Set operation did not have arg data in expected format"
);
return;
}
const accountId = Object.keys(functionCall.args.data)[0];
if (!functionCall.args.data[accountId]) {
Comment on lines +218 to +230
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (
!functionCall ||
!functionCall.args ||
!functionCall.args.data ||
!Object.keys(functionCall.args.data) ||
!Object.keys(functionCall.args.data)[0]
) {
console.error(
"Set operation did not have arg data in expected format"
);
return;
}
const accountId = Object.keys(functionCall.args.data)[0];
const accountId = Object.keys(functionCall.args.data ?? {})[0];
if (!accountId) {
return false;
}

I believe this is the same?

Same comment around logging, can we please remove? Also we should be returning false here and below?

console.error("Set operation did not have arg data for accountId");
return;
}
const accountData = functionCall.args.data[accountId];
if (!accountData) {
console.error(
"Set operation did not have arg data for accountId in expected format"
);
return;
}
return accountData[operation];
})
.map((functionCall) => {
const accountId = Object.keys(functionCall.args.data)[0];
return {
accountId,
data: functionCall.args.data[accountId][operation],
};
});
};

private buildActionsHashmap() {
const actions = new Map<string, Action>();
this.actions().forEach(action => {
Expand Down
6 changes: 4 additions & 2 deletions packages/near-lake-primitives/src/types/receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ class DeployContract {
constructor(readonly code: Uint8Array) { }
};

class FunctionCall {
export type FunctionCallWrapper = {FunctionCall: FunctionCall};

export class FunctionCall {
constructor(readonly methodName: string, readonly args: Uint8Array, readonly gas: number, readonly deposit: string) { }
};

Expand Down Expand Up @@ -167,7 +169,7 @@ class DeleteAccount {
export type Operation =
| 'CreateAccount'
| DeployContract
| FunctionCall
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this incorrect? Is this a breaking change?

Copy link
Author

@gabehamilton gabehamilton Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkudinov This file is where I ran into the Operations types not matching the data. I only changed FunctionCall but they should probably all be corrected.

| FunctionCallWrapper
| Transfer
| Stake
| AddKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@ exports[`Block parses event logs 1`] = `
]
`;

exports[`Block parses social operations 1`] = `
[
{
"accountId": "flatirons.near",
"data": "{"near":{"modelProvider":{"openai":{"displayName":"OpenAI","logoUrl":{"cid":"bafkreicc3vj3dmcyedrlsb57uq72ojnppsoh2sp3ozcxko347ytvd4amxi","name":"OpenAI_Logo.png","type":"image/png","size":8735},"tags":null,"documentationUrl":""}}}}",
},
]
`;

exports[`Block parses successful function calls 1`] = `
[
{
"args": {
"data": {
"flatirons.near": {
"entities": "{"near":{"modelProvider":{"openai":{"displayName":"OpenAI","logoUrl":{"cid":"bafkreicc3vj3dmcyedrlsb57uq72ojnppsoh2sp3ozcxko347ytvd4amxi","name":"OpenAI_Logo.png","type":"image/png","size":8735},"tags":null,"documentationUrl":""}}}}",
},
},
},
"deposit": "0",
"gas": 100000000000000,
"methodName": "set",
"receiptId": "DoY9niR8tgMxHve1a7dCq36Eo3uRMJZtvMRsPozr1mYS",
},
]
`;

exports[`Block serializes meta transactions 1`] = `
{
"Delegate": {
Expand Down
20 changes: 20 additions & 0 deletions packages/near-lake-primitives/test/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,24 @@ describe("Block", () => {
);
expect(authorToPostId).toMatchSnapshot();
});

it("parses successful function calls", async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test for actions(successfulOnly) too please?

let streamerMessageBuffer = await readFile(
`${__dirname}/../../../blocks/117969098.json`
);
let streamerMessage = JSON.parse(streamerMessageBuffer.toString());
let block = Block.fromStreamerMessage(streamerMessage);

expect(block.successfulFunctionCalls("social.near")).toMatchSnapshot();
});

it("parses social operations", async () => {
let streamerMessageBuffer = await readFile(
`${__dirname}/../../../blocks/117969098.json`
);
let streamerMessage = JSON.parse(streamerMessageBuffer.toString());
let block = Block.fromStreamerMessage(streamerMessage);

expect(block.socialOperations("entities")).toMatchSnapshot();
});
});
Loading