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

feat: implement local execution of view methods #1172

Merged
merged 8 commits into from
Nov 15, 2023
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
5 changes: 5 additions & 0 deletions .changeset/bright-nails-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@near-js/accounts": patch
---

Implement local execution of contract view methods
1 change: 1 addition & 0 deletions packages/accounts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"bn.js": "5.2.1",
"borsh": "1.0.0",
"depd": "^2.0.0",
"lru_map": "^0.4.1",
"near-abi": "0.1.1"
},
"devDependencies": {
Expand Down
24 changes: 23 additions & 1 deletion packages/accounts/src/contract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getTransactionLastResult } from '@near-js/utils';
import { ArgumentTypeError, PositionalArgsError } from '@near-js/types';
import { LocalViewExecution } from './local-view-execution';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import BN from 'bn.js';
Expand Down Expand Up @@ -99,6 +100,11 @@ export interface ContractMethods {
* ABI defining this contract's interface.
*/
abi?: AbiRoot;

/**
* Executes view methods locally. This flag is useful when multiple view calls will be made for the same blockId
*/
useLocalViewExecution: boolean;
}

/**
Expand Down Expand Up @@ -138,6 +144,7 @@ export interface ContractMethods {
export class Contract {
readonly account: Account;
readonly contractId: string;
readonly lve: LocalViewExecution;

/**
* @param account NEAR account to sign change method transactions
Expand All @@ -147,7 +154,8 @@ export class Contract {
constructor(account: Account, contractId: string, options: ContractMethods) {
this.account = account;
this.contractId = contractId;
const { viewMethods = [], changeMethods = [], abi: abiRoot } = options;
this.lve = new LocalViewExecution(account);
const { viewMethods = [], changeMethods = [], abi: abiRoot, useLocalViewExecution } = options;

let viewMethodsWithAbi = viewMethods.map((name) => ({ name, abi: null as AbiFunction }));
let changeMethodsWithAbi = changeMethods.map((name) => ({ name, abi: null as AbiFunction }));
Expand Down Expand Up @@ -177,6 +185,20 @@ export class Contract {
validateArguments(args, abi, ajv, abiRoot);
}

if (useLocalViewExecution) {
try {
return await this.lve.viewFunction({
contractId: this.contractId,
methodName: name,
args,
...options,
});
} catch (error) {
console.warn(`Local view execution failed with: "${error.message}"`);
console.warn(`Fallback to normal RPC call`);
}
}

return this.account.viewFunction({
contractId: this.contractId,
methodName: name,
Expand Down
84 changes: 84 additions & 0 deletions packages/accounts/src/local-view-execution/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { BlockReference, ContractCodeView } from '@near-js/types';
import { printTxOutcomeLogs } from '@near-js/utils';
import { Account, FunctionCallOptions } from '../account';
import { Storage } from './storage';
import { Runtime } from './runtime';
import { ContractState } from './types';

interface ViewFunctionCallOptions extends FunctionCallOptions {
blockQuery?: BlockReference
}

export class LocalViewExecution {
private readonly account: Account;
private readonly storage: Storage;

constructor(account: Account) {
this.account = account;
this.storage = new Storage();
}

private async fetchContractCode(contractId: string, blockQuery: BlockReference) {
const result = await this.account.connection.provider.query<ContractCodeView>({
request_type: 'view_code',
account_id: contractId,
...blockQuery,
});

return result.code_base64;
}

private async fetchContractState(blockQuery: BlockReference): Promise<ContractState> {
return this.account.viewState('', blockQuery);
}

private async fetch(contractId: string, blockQuery: BlockReference) {
const block = await this.account.connection.provider.block(blockQuery);
const blockHash = block.header.hash;
const blockHeight = block.header.height;
const blockTimestamp = block.header.timestamp;

const contractCode = await this.fetchContractCode(contractId, blockQuery);
const contractState = await this.fetchContractState(blockQuery);

return {
blockHash,
blockHeight,
blockTimestamp,
contractCode,
contractState,
};
}

private async loadOrFetch(contractId: string, blockQuery: BlockReference) {
const stored = this.storage.load(blockQuery);

if (stored) {
return stored;
}

const { blockHash, ...fetched } = await this.fetch(contractId, blockQuery);

this.storage.save(blockHash, fetched);

return fetched;
}

public async viewFunction({ contractId, methodName, args = {}, blockQuery = { finality: 'optimistic' }, ...ignored }: ViewFunctionCallOptions) {
const methodArgs = JSON.stringify(args);

const { contractCode, contractState, blockHeight, blockTimestamp } = await this.loadOrFetch(
contractId,
blockQuery
);
const runtime = new Runtime({ contractId, contractCode, contractState, blockHeight, blockTimestamp, methodArgs });

const { result, logs } = await runtime.execute(methodName);

if (logs) {
printTxOutcomeLogs({ contractId, logs });
}

return JSON.parse(Buffer.from(result).toString());
}
}
Loading
Loading