Skip to content

Commit

Permalink
feat: implement local execution of view methods (#1172)
Browse files Browse the repository at this point in the history
  • Loading branch information
arrusev authored Nov 15, 2023
1 parent 61349ae commit 0f764ee
Show file tree
Hide file tree
Showing 14 changed files with 936 additions and 41 deletions.
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

0 comments on commit 0f764ee

Please sign in to comment.