From 3e60d3476c19f207d94232856a48705e6322a1fc Mon Sep 17 00:00:00 2001 From: Dhaiwat Date: Thu, 11 Apr 2024 14:25:58 +0530 Subject: [PATCH] feat: add `isReadOnly` helper for functions (#2008) --- .changeset/flat-peaches-invite.md | 5 +++ .../contracts/is-function-readonly.test.ts | 21 ++++++++++++ apps/docs/spell-check-custom-words.txt | 3 +- apps/docs/src/guide/contracts/methods.md | 10 ++++++ packages/abi-coder/src/FunctionFragment.ts | 10 ++++++ .../src/is-function-readonly.test.ts | 32 +++++++++++++++++++ .../storage-test-contract/src/main.sw | 4 +++ packages/program/src/contract.ts | 14 ++++++-- packages/program/src/types.ts | 7 ++-- 9 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 .changeset/flat-peaches-invite.md create mode 100644 apps/docs-snippets/src/guide/contracts/is-function-readonly.test.ts create mode 100644 packages/fuel-gauge/src/is-function-readonly.test.ts diff --git a/.changeset/flat-peaches-invite.md b/.changeset/flat-peaches-invite.md new file mode 100644 index 00000000000..f0d9de6c2ba --- /dev/null +++ b/.changeset/flat-peaches-invite.md @@ -0,0 +1,5 @@ +--- +"@fuel-ts/program": minor +--- + +feat: add `isReadOnly` helper for functions diff --git a/apps/docs-snippets/src/guide/contracts/is-function-readonly.test.ts b/apps/docs-snippets/src/guide/contracts/is-function-readonly.test.ts new file mode 100644 index 00000000000..7b7a6ffba54 --- /dev/null +++ b/apps/docs-snippets/src/guide/contracts/is-function-readonly.test.ts @@ -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); +}); diff --git a/apps/docs/spell-check-custom-words.txt b/apps/docs/spell-check-custom-words.txt index 926dfef8515..1fd2c1354d8 100644 --- a/apps/docs/spell-check-custom-words.txt +++ b/apps/docs/spell-check-custom-words.txt @@ -305,4 +305,5 @@ CLIs scaffolded programmatically BigNumber -Gwei \ No newline at end of file +Gwei +onchain \ No newline at end of file diff --git a/apps/docs/src/guide/contracts/methods.md b/apps/docs/src/guide/contracts/methods.md index 79fa90552d5..7aef78b3c95 100644 --- a/apps/docs/src/guide/contracts/methods.md +++ b/apps/docs/src/guide/contracts/methods.md @@ -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. diff --git a/packages/abi-coder/src/FunctionFragment.ts b/packages/abi-coder/src/FunctionFragment.ts index ef190c267df..07bde755e6e 100644 --- a/packages/abi-coder/src/FunctionFragment.ts +++ b/packages/abi-coder/src/FunctionFragment.ts @@ -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'); + } } diff --git a/packages/fuel-gauge/src/is-function-readonly.test.ts b/packages/fuel-gauge/src/is-function-readonly.test.ts new file mode 100644 index 00000000000..bcbdf125c4d --- /dev/null +++ b/packages/fuel-gauge/src/is-function-readonly.test.ts @@ -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); + }); +}); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/storage-test-contract/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/storage-test-contract/src/main.sw index 611dbbc718f..7613b90c52e 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/storage-test-contract/src/main.sw +++ b/packages/fuel-gauge/test/fixtures/forc-projects/storage-test-contract/src/main.sw @@ -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; @@ -88,4 +89,7 @@ impl StorageTestContract for Contract { fn return_var5() -> StructValidation { storage.var5.read() } + fn return_true() -> bool { + true + } } diff --git a/packages/program/src/contract.ts b/packages/program/src/contract.ts index 37c52c2992c..1d2009e404d 100644 --- a/packages/program/src/contract.ts +++ b/packages/program/src/contract.ts @@ -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. @@ -89,7 +89,17 @@ export default class Contract implements AbstractContract { * @returns A function that creates a FunctionInvocationScope. */ buildFunction(func: FunctionFragment) { - return (...args: Array) => new FunctionInvocationScope(this, func, args); + return (() => { + const funcInvocationScopeCreator = (...args: Array) => + new FunctionInvocationScope(this, func, args); + + Object.defineProperty(funcInvocationScopeCreator, 'isReadOnly', { + value: () => func.isReadOnly(), + writable: false, + }); + + return funcInvocationScopeCreator; + })() as InvokeFunction; } /** diff --git a/packages/program/src/types.ts b/packages/program/src/types.ts index d010f896644..f98818efcb4 100644 --- a/packages/program/src/types.ts +++ b/packages/program/src/types.ts @@ -66,9 +66,10 @@ export type CallConfig = { * @template TReturn - Type of the function's return value. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type InvokeFunction = Array, TReturn = any> = ( - ...args: TArgs -) => FunctionInvocationScope; +export interface InvokeFunction = Array, TReturn = any> { + (...args: TArgs): FunctionInvocationScope; + isReadOnly: () => boolean; +} /** * Represents a collection of functions that can be invoked.