Skip to content

Commit

Permalink
feat: add isReadOnly helper for functions (#2008)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dhaiwat10 authored Apr 11, 2024
1 parent 155b6f2 commit 3e60d34
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-peaches-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/program": minor
---

feat: add `isReadOnly` helper for functions
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DocSnippetProjectsEnum } from '../../../test/fixtures/forc-projects';
import { createAndDeployContractFromProject } from '../../utils';

/**
* @group node
*/
test('isReadOnly returns true for read-only functions', async () => {
const contract = await createAndDeployContractFromProject(DocSnippetProjectsEnum.COUNTER);

// #region is-function-readonly-1
const isReadOnly = contract.functions.get_count.isReadOnly();

if (isReadOnly) {
await contract.functions.get_count().get();
} else {
await contract.functions.get_count().call();
}
// #endregion is-function-readonly-1

expect(isReadOnly).toBe(true);
});
3 changes: 2 additions & 1 deletion apps/docs/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,5 @@ CLIs
scaffolded
programmatically
BigNumber
Gwei
Gwei
onchain
10 changes: 10 additions & 0 deletions apps/docs/src/guide/contracts/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ The `call` method should be used to submit a real contract call transaction to t
Real resources are consumed, and any operations executed by the contract function will be processed on the blockchain.

<<< @/../../docs-snippets/src/guide/contracts/interacting-with-contracts.test.ts#interacting-with-contracts-4{ts:line-numbers}

## `isReadOnly` (utility)

If you want to figure out whether a function is read-only, you can use the `isReadOnly` method:

<<< @/../../docs-snippets/src/guide/contracts/is-function-readonly.test.ts#is-function-readonly-1{ts:line-numbers}

If the function is read-only, you can use the `get` method to retrieve onchain data without spending gas.

If the function is not read-only you will have to use the `call` method to submit a transaction onchain which incurs a gas fee.
10 changes: 10 additions & 0 deletions packages/abi-coder/src/FunctionFragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,14 @@ export class FunctionFragment<

return coder.decode(bytes, 0) as [DecodedValue | undefined, number];
}

/**
* Checks if the function is read-only i.e. it only reads from storage, does not write to it.
*
* @returns True if the function is read-only or pure, false otherwise.
*/
isReadOnly(): boolean {
const storageAttribute = this.attributes.find((attr) => attr.name === 'storage');
return !storageAttribute?.arguments.includes('write');
}
}
32 changes: 32 additions & 0 deletions packages/fuel-gauge/src/is-function-readonly.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FuelGaugeProjectsEnum } from '../test/fixtures';

import { getSetupContract } from './utils';

/**
* @group node
*/
describe('isReadOnly', () => {
test('isReadOnly returns true for a read-only function', async () => {
const contract = await getSetupContract(FuelGaugeProjectsEnum.STORAGE_TEST_CONTRACT)();

const isReadOnly = contract.functions.counter.isReadOnly();

expect(isReadOnly).toBe(true);
});

test('isReadOnly returns false for a function containing write operations', async () => {
const contract = await getSetupContract(FuelGaugeProjectsEnum.STORAGE_TEST_CONTRACT)();

const isReadOnly = contract.functions.increment_counter.isReadOnly();

expect(isReadOnly).toBe(false);
});

test('isReadOnly does not throw a runtime error for a function that does not use storage', async () => {
const contract = await getSetupContract(FuelGaugeProjectsEnum.STORAGE_TEST_CONTRACT)();

const isReadOnly = contract.functions.return_true.isReadOnly();

expect(isReadOnly).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ abi StorageTestContract {
fn return_var4() -> bool;
#[storage(read)]
fn return_var5() -> StructValidation;
fn return_true() -> bool;
}

const COUNTER_KEY: b256 = 0x0000000000000000000000000000000000000000000000000000000000000000;
Expand Down Expand Up @@ -88,4 +89,7 @@ impl StorageTestContract for Contract {
fn return_var5() -> StructValidation {
storage.var5.read()
}
fn return_true() -> bool {
true
}
}
14 changes: 12 additions & 2 deletions packages/program/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { AbstractAddress, AbstractContract, BytesLike } from '@fuel-ts/inte

import { FunctionInvocationScope } from './functions/invocation-scope';
import { MultiCallInvocationScope } from './functions/multicall-scope';
import type { InvokeFunctions } from './types';
import type { InvokeFunction, InvokeFunctions } from './types';

/**
* `Contract` provides a way to interact with the contract program type.
Expand Down Expand Up @@ -89,7 +89,17 @@ export default class Contract implements AbstractContract {
* @returns A function that creates a FunctionInvocationScope.
*/
buildFunction(func: FunctionFragment) {
return (...args: Array<unknown>) => new FunctionInvocationScope(this, func, args);
return (() => {
const funcInvocationScopeCreator = (...args: Array<unknown>) =>
new FunctionInvocationScope(this, func, args);

Object.defineProperty(funcInvocationScopeCreator, 'isReadOnly', {
value: () => func.isReadOnly(),
writable: false,
});

return funcInvocationScopeCreator;
})() as InvokeFunction;
}

/**
Expand Down
7 changes: 4 additions & 3 deletions packages/program/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ export type CallConfig<T = unknown> = {
* @template TReturn - Type of the function's return value.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InvokeFunction<TArgs extends Array<any> = Array<any>, TReturn = any> = (
...args: TArgs
) => FunctionInvocationScope<TArgs, TReturn>;
export interface InvokeFunction<TArgs extends Array<any> = Array<any>, TReturn = any> {
(...args: TArgs): FunctionInvocationScope<TArgs, TReturn>;
isReadOnly: () => boolean;
}

/**
* Represents a collection of functions that can be invoked.
Expand Down

0 comments on commit 3e60d34

Please sign in to comment.