From a57edc42bee279abc1f3925acd130cba29cd239a Mon Sep 17 00:00:00 2001 From: Jaswinder Date: Wed, 24 Jul 2024 21:37:06 +0530 Subject: [PATCH] feat(wip): removed signer from viewMethods by dropping NAJ account.ViewFunction (#35) * feat(wip): removed signer from viewMethods by dropping accounts.ViewFunction * chore: fixed docs and removed comments * chore: fixed linting issues * fix: removed the use of Buffer as it won't work in browser environments * fix: put try/catch for btoa --- docs/advanced/reading-data.mdx | 12 +- docs/api-reference/social.mdx | 3 +- docs/api-reference/types.mdx | 4 +- src/constants/Config.ts | 7 ++ src/constants/index.ts | 1 + src/controllers/Social.get.test.ts | 16 +-- src/controllers/Social.getVersion.test.ts | 18 +-- .../Social.grantWritePermission.test.ts | 6 +- .../Social.isWritePermissionGranted.test.ts | 12 +- src/controllers/Social.set.test.ts | 6 +- src/controllers/Social.storageDeposit.test.ts | 1 - src/controllers/Social.ts | 107 +++++++++++++----- src/types/IDefaultViewOptions.ts | 7 +- src/types/NetworkIds.ts | 3 + src/types/index.ts | 1 + src/utils/rpcQueries/index.ts | 1 + src/utils/rpcQueries/types/IOptions.ts | 8 ++ src/utils/rpcQueries/types/index.ts | 1 + src/utils/rpcQueries/viewFunction.test.ts | 88 ++++++++++++++ src/utils/rpcQueries/viewFunction.ts | 59 ++++++++++ 20 files changed, 281 insertions(+), 80 deletions(-) create mode 100644 src/constants/Config.ts create mode 100644 src/types/NetworkIds.ts create mode 100644 src/utils/rpcQueries/index.ts create mode 100644 src/utils/rpcQueries/types/IOptions.ts create mode 100644 src/utils/rpcQueries/types/index.ts create mode 100644 src/utils/rpcQueries/viewFunction.test.ts create mode 100644 src/utils/rpcQueries/viewFunction.ts diff --git a/docs/advanced/reading-data.mdx b/docs/advanced/reading-data.mdx index 6c87eb7..2dd04c6 100644 --- a/docs/advanced/reading-data.mdx +++ b/docs/advanced/reading-data.mdx @@ -56,7 +56,7 @@ Getting specific values, like the example from above, you can use a set of keys: 'alice.near/profile/image/url', 'bob.near/profile/name', ], - signer, // an initialized near-api-js account + rpcURL //rpc url for your network. }); console.log(result); @@ -95,7 +95,7 @@ Getting specific values, like the example from above, you can use a set of keys: 'alice.near/profile/image/url', 'bob.near/profile/name', ], - signer, // an initialized near-api-js account + rpcURL //rpc url for your network. }).then((result) => { console.log(result); /* @@ -135,7 +135,7 @@ Getting specific values, like the example from above, you can use a set of keys: 'alice.near/profile/image/url', 'bob.near/profile/name', ], - signer, // an initialized near-api-js account + rpcURL //rpc url for your network. }); console.log(result); @@ -186,7 +186,7 @@ The [`get`](../api-reference/social#getoptions) function also supports wildcard keys: [ 'alice.near/profile/**', ], - signer, // an initialized near-api-js account + rpcURL //rpc url for your network. }); console.log(result); @@ -214,7 +214,7 @@ The [`get`](../api-reference/social#getoptions) function also supports wildcard keys: [ 'alice.near/profile/**', ], - signer, // an initialized near-api-js account + rpcURL, //rpc url for your network. }).then((result) => { console.log(result); /* @@ -243,7 +243,7 @@ The [`get`](../api-reference/social#getoptions) function also supports wildcard keys: [ 'alice.near/profile/**' ], - signer, // an initialized near-api-js account + rpcURL, //rpc url for your network. }); console.log(result); diff --git a/docs/api-reference/social.mdx b/docs/api-reference/social.mdx index 8425ecc..f2972d4 100644 --- a/docs/api-reference/social.mdx +++ b/docs/api-reference/social.mdx @@ -41,7 +41,7 @@ import TOCInline from '@theme/TOCInline'; | Name | Type | Required | Default | Description | |---------|------------------------------------|----------|---------|------------------------------------------------------------| -| options | [`IGetOptions`](types#igetoptions) | yes | - | Options that include the signer account and a set of keys. | +| options | [`IGetOptions`](types#igetoptions) | yes | - | Options that include the rpc url and a set of keys. | #### Returns @@ -78,7 +78,6 @@ import TOCInline from '@theme/TOCInline'; ### `isWritePermissionGranted(options)` > Checks if an account, specified in `options.granteeAccountId`, has been granted write access for a key. -> If the signer and the supplied `options.granteeAccountId` match, true will be returned. #### Parameters diff --git a/docs/api-reference/types.mdx b/docs/api-reference/types.mdx index b3f47df..412ab15 100644 --- a/docs/api-reference/types.mdx +++ b/docs/api-reference/types.mdx @@ -12,7 +12,7 @@ import TOCInline from '@theme/TOCInline'; | Name | Type | Required | Default | Description | |-----------------|------------------------------------------------------------------------------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| | keys | `string[]` | yes | - | A set of key patterns to return. Each key is in the form of a pattern to return data from. For example `alice.near/profile/**` or `alice.near/profile/name`. | -| signer | [`Account`](https://near.github.io/near-api-js/classes/near_api_js.account.Account.html) | yes | - | An account that will read the data. For details on getting the account object see the NEAR API [docs](https://docs.near.org/tools/near-api-js/account). | +| rpcURL | `string` | yes | - | URL to your current network's RPC node, for example on mainnet, you connect to https://rpc.mainnet.near.org/ | | returnDeleted | `boolean` | no | `false` | If true, will include deleted keys with the value `null`. | | withBlockHeight | `boolean` | no | `false` | If true, for every value and a node will add the block height of the data with the key `:block`. | | withNodeId | `boolean` | no | `false` | If true, for every node will add the node index with the key `:node`. | @@ -34,7 +34,7 @@ import TOCInline from '@theme/TOCInline'; |------------------|------------------------------------------------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------| | granteeAccountId | `string` | yes | - | The account ID to check if it has write permissions for a key. | | key | `string` | yes | - | The key to check if the grantee has write permission. | -| signer | [`Account`](https://near.github.io/near-api-js/classes/near_api_js.account.Account.html) | yes | - | An account that will read the data. For details on getting the account object see the NEAR API [docs](https://docs.near.org/tools/near-api-js/account). | +| rpcURL | `string` | yes | - | URL to your current network's RPC node, for example on mainnet, you connect to https://rpc.mainnet.near.org/ | ### `INewSocialOptions` diff --git a/src/constants/Config.ts b/src/constants/Config.ts new file mode 100644 index 0000000..4b3d527 --- /dev/null +++ b/src/constants/Config.ts @@ -0,0 +1,7 @@ +export const networkRPCs = { + testnet: 'https://rpc.testnet.near.org', + mainnet: 'https://rpc.mainnet.near.org', + betanet: 'https://rpc.betanet.near.org', + localnet: 'http://localhost:3030', + // Add more networks if needed +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index bcdfd91..2788c3c 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1 +1,2 @@ export * from './Fees'; +export * from './Config'; diff --git a/src/controllers/Social.get.test.ts b/src/controllers/Social.get.test.ts index ed2ba58..f101758 100644 --- a/src/controllers/Social.get.test.ts +++ b/src/controllers/Social.get.test.ts @@ -1,23 +1,11 @@ -import type { Account } from 'near-api-js'; - // credentials import { account_id as socialContractAccountId } from '@test/credentials/localnet/social.test.near.json'; // controllers import Social from './Social'; - -// helpers -import createEphemeralAccount from '@test/helpers/createEphemeralAccount'; +import { networkRPCs } from '@app/constants'; describe(`${Social.name}#get`, () => { - let signer: Account; - - beforeEach(async () => { - const result = await createEphemeralAccount(); - - signer = result.account; - }); - it('should return an empty object when the contract does not know the account', async () => { // arrange const client = new Social({ @@ -26,7 +14,7 @@ describe(`${Social.name}#get`, () => { // act const result = await client.get({ keys: ['unknown.test.near/profile/name'], - signer, + rpcURL: networkRPCs.localnet, }); // assert diff --git a/src/controllers/Social.getVersion.test.ts b/src/controllers/Social.getVersion.test.ts index b359857..1e6f84e 100644 --- a/src/controllers/Social.getVersion.test.ts +++ b/src/controllers/Social.getVersion.test.ts @@ -1,30 +1,20 @@ -import type { Account } from 'near-api-js'; - // credentials import { account_id as socialContractAccountId } from '@test/credentials/localnet/social.test.near.json'; // controllers import Social from './Social'; - -// helpers -import createEphemeralAccount from '@test/helpers/createEphemeralAccount'; +import { networkRPCs } from '@app/constants'; describe(`${Social.name}#getVersion`, () => { - let signer: Account; - - beforeEach(async () => { - const result = await createEphemeralAccount(); - - signer = result.account; - }); - it('should return the version of the social contract', async () => { // arrange const client = new Social({ contractId: socialContractAccountId, }); // act - const version = await client.getVersion({ signer }); + const version = await client.getVersion({ + rpcURL: networkRPCs.localnet, + }); // assert expect(version).toMatchSnapshot(); diff --git a/src/controllers/Social.grantWritePermission.test.ts b/src/controllers/Social.grantWritePermission.test.ts index cda0e5a..800f02e 100644 --- a/src/controllers/Social.grantWritePermission.test.ts +++ b/src/controllers/Social.grantWritePermission.test.ts @@ -7,6 +7,8 @@ import { account_id as socialContractAccountId } from '@test/credentials/localne // controllers import Social from './Social'; +import { networkRPCs } from '@app/constants'; + // enums import { ErrorCodeEnum } from '@app/enums'; @@ -173,7 +175,7 @@ describe(`${Social.name}#grantWritePermission`, () => { result = await client.isWritePermissionGranted({ granteeAccountId: granteeAccount.accountId, key, - signer: granteeAccount, + rpcURL: networkRPCs.localnet, }); expect(result).toBe(true); @@ -203,7 +205,7 @@ describe(`${Social.name}#grantWritePermission`, () => { result = await client.isWritePermissionGranted({ granteePublicKey: granteeKeyPair.getPublicKey(), key, - signer: granteeAccount, + rpcURL: networkRPCs.localnet, }); expect(result).toBe(true); diff --git a/src/controllers/Social.isWritePermissionGranted.test.ts b/src/controllers/Social.isWritePermissionGranted.test.ts index b56facf..8afbbeb 100644 --- a/src/controllers/Social.isWritePermissionGranted.test.ts +++ b/src/controllers/Social.isWritePermissionGranted.test.ts @@ -7,6 +7,8 @@ import { account_id as socialContractAccountId } from '@test/credentials/localne // controllers import Social from './Social'; +import { networkRPCs } from '@app/constants'; + // enums import { ErrorCodeEnum } from '@app/enums'; @@ -87,7 +89,7 @@ describe(`${Social.name}#isWritePermissionGranted`, () => { await client.isWritePermissionGranted({ granteeAccountId: invalidGranteeAccountId, key, - signer: granterAccount, + rpcURL: networkRPCs.localnet, }); } catch (error) { // assert @@ -106,7 +108,7 @@ describe(`${Social.name}#isWritePermissionGranted`, () => { const result = await client.isWritePermissionGranted({ granteeAccountId: granterAccount.accountId, key, - signer: granterAccount, + rpcURL: networkRPCs.localnet, }); // assert @@ -119,7 +121,7 @@ describe(`${Social.name}#isWritePermissionGranted`, () => { const result = await client.isWritePermissionGranted({ granteeAccountId: granteeAccount.accountId, key, - signer: granterAccount, + rpcURL: networkRPCs.localnet, }); // assert @@ -144,7 +146,7 @@ describe(`${Social.name}#isWritePermissionGranted`, () => { const result = await client.isWritePermissionGranted({ granteeAccountId: granteeAccount.accountId, key, - signer: granterAccount, + rpcURL: networkRPCs.localnet, }); // assert @@ -169,7 +171,7 @@ describe(`${Social.name}#isWritePermissionGranted`, () => { const result = await client.isWritePermissionGranted({ granteePublicKey: granteeKeyPair.getPublicKey(), key, - signer: granterAccount, + rpcURL: networkRPCs.localnet, }); // assert diff --git a/src/controllers/Social.set.test.ts b/src/controllers/Social.set.test.ts index cfdbb28..46651f4 100644 --- a/src/controllers/Social.set.test.ts +++ b/src/controllers/Social.set.test.ts @@ -10,6 +10,8 @@ import { MINIMUM_STORAGE_IN_BYTES } from '@app/constants'; // controllers import Social from './Social'; +import { networkRPCs } from '@app/constants'; + // enums import { ErrorCodeEnum } from '@app/enums'; @@ -119,7 +121,7 @@ describe(`${Social.name}#set`, () => { result = await client.get({ keys: [`${signer.accountId}/profile/name`], - signer, + rpcURL: networkRPCs.localnet, }); expect(result).toEqual(data); @@ -154,7 +156,7 @@ describe(`${Social.name}#set`, () => { result = await client.get({ keys: [`${signer.accountId}/profile/name`], - signer, + rpcURL: networkRPCs.localnet, }); expect(result).toEqual(data); diff --git a/src/controllers/Social.storageDeposit.test.ts b/src/controllers/Social.storageDeposit.test.ts index 2eae83a..48bf7bf 100644 --- a/src/controllers/Social.storageDeposit.test.ts +++ b/src/controllers/Social.storageDeposit.test.ts @@ -170,7 +170,6 @@ describe(`${Social.name}#storageDeposit`, () => { contractId: socialContractAccountId, methodName: ViewMethodEnum.StorageBalanceOf, }); - console.log(result); expect(result.total).toEqual(deposit); }); }); diff --git a/src/controllers/Social.ts b/src/controllers/Social.ts index 117d390..81edae5 100644 --- a/src/controllers/Social.ts +++ b/src/controllers/Social.ts @@ -8,7 +8,11 @@ import { } from 'near-api-js'; // constants -import { GAS_FEE_IN_ATOMIC_UNITS, ONE_YOCTO } from '@app/constants'; +import { + GAS_FEE_IN_ATOMIC_UNITS, + networkRPCs, + ONE_YOCTO, +} from '@app/constants'; // enums import { ChangeMethodEnum, ViewMethodEnum } from '@app/enums'; @@ -40,12 +44,14 @@ import type { ISocialDBContractIsWritePermissionGrantedArgs, ISocialDBContractStorageWithdrawArgs, ISocialDBContractStorageDepositArgs, + NetworkIds, } from '@app/types'; // utils import calculateRequiredDeposit from '@app/utils/calculateRequiredDeposit'; import parseKeysFromData from '@app/utils/parseKeysFromData'; import validateAccountId from '@app/utils/validateAccountId'; +import viewFunction from '@app/utils/rpcQueries/viewFunction'; export default class Social { private contractId: string; @@ -92,15 +98,37 @@ export default class Social { private async _storageBalanceOf({ accountId, - signer, + rpcURL, }: IStorageBalanceOfOptions): Promise { - return await signer.viewFunction({ + const result = await viewFunction({ + contractId: this.contractId, + method: ViewMethodEnum.StorageBalanceOf, args: { account_id: accountId, }, - contractId: this.contractId, - methodName: ViewMethodEnum.StorageBalanceOf, + rpcURL, }); + + if (this._isStorageBalance(result)) { + return result; + } else if (result === null) { + return null; + } else { + throw new Error('Unexpected response format from storage_balance_of'); + } + } + + private _isStorageBalance( + data: unknown + ): data is ISocialDBContractStorageBalance { + return ( + typeof data === 'object' && + data !== null && + 'total' in data && + 'available' in data && + typeof (data as ISocialDBContractStorageBalance).total === 'string' && + typeof (data as ISocialDBContractStorageBalance).available === 'string' + ); } private _uniqueAccountIdsFromKeys(keys: string[]): string[] { @@ -119,41 +147,53 @@ export default class Social { /** * Reads the data for given set of keys. - * @param {IGetOptions} options - the signer and a set of keys to read. + * @param {IGetOptions} options - the set of keys to read and other options. * @returns {Promise>} a promise that resolves to the given data. */ public async get({ - signer, keys, returnDeleted, withBlockHeight, withNodeId, + rpcURL, }: IGetOptions): Promise> { - return await signer.viewFunction({ + const args: ISocialDBContractGetArgs = { + keys, + ...((returnDeleted || withBlockHeight || withNodeId) && { + options: { + with_block_height: withBlockHeight, + with_node_id: withNodeId, + return_deleted: returnDeleted, + }, + }), + }; + + return (await viewFunction({ contractId: this.contractId, - methodName: ViewMethodEnum.Get, - args: { - keys, - ...((returnDeleted || withBlockHeight || withNodeId) && { - options: { - with_block_height: withBlockHeight, - with_node_id: withNodeId, - return_deleted: returnDeleted, - }, - }), - } as ISocialDBContractGetArgs, - }); + method: ViewMethodEnum.Get, + args, + rpcURL, + })) as Record; } /** * Gets the current version of the social contract. + * @param {IGetVersionOptions} options - Optional parameters including rpcURL. * @returns {Promise} a promise that resolves to the current version of the contract. */ - public async getVersion({ signer }: IGetVersionOptions): Promise { - return await signer.viewFunction({ + public async getVersion({ rpcURL }: IGetVersionOptions): Promise { + const version = await viewFunction({ contractId: this.contractId, - methodName: ViewMethodEnum.GetVersion, + method: ViewMethodEnum.GetVersion, + rpcURL, }); + + if (typeof version !== 'string') { + throw new Error( + `Unexpected response format from get_version: ${JSON.stringify(version)}` + ); + } + return version; } /** @@ -265,7 +305,7 @@ export default class Social { | IIsWritePermissionGrantedWithAccountIdOptions | IIsWritePermissionGrantedWithPublicKeyOptions ): Promise { - const { key, signer } = options; + const { key, rpcURL } = options; if ( (options as IIsWritePermissionGrantedWithAccountIdOptions) @@ -294,9 +334,9 @@ export default class Social { } } - return await signer.viewFunction({ + const result = await viewFunction({ contractId: this.contractId, - methodName: ViewMethodEnum.IsWritePermissionGranted, + method: ViewMethodEnum.IsWritePermissionGranted, args: { key, ...((options as IIsWritePermissionGrantedWithAccountIdOptions) @@ -312,7 +352,16 @@ export default class Social { ).granteePublicKey.toString(), }), } as ISocialDBContractIsWritePermissionGrantedArgs, + rpcURL, }); + + if (typeof result !== 'boolean') { + throw new Error( + `Unexpected response format from isWritePermissionGranted: ${JSON.stringify(result)}` + ); + } + + return result; } /** @@ -354,6 +403,8 @@ export default class Social { _nonce = accessKeyView.nonce + BigInt(1); // increment nonce as this will be a new transaction for the access key } + const networkId = signer.connection.networkId as NetworkIds; + const rpcURL = networkRPCs[networkId]; // for each key, check if the signer has been granted write permission for the key for (let i = 0; i < keys.length; i++) { if ( @@ -361,7 +412,7 @@ export default class Social { !(await this.isWritePermissionGranted({ granteePublicKey: publicKey, key: keys[i], - signer, + rpcURL, })) ) { throw new KeyNotAllowedError( @@ -379,7 +430,7 @@ export default class Social { ) { storageBalance = await this._storageBalanceOf({ accountId: signer.accountId, - signer, + rpcURL, }); deposit = calculateRequiredDeposit({ diff --git a/src/types/IDefaultViewOptions.ts b/src/types/IDefaultViewOptions.ts index 740c6ea..371c556 100644 --- a/src/types/IDefaultViewOptions.ts +++ b/src/types/IDefaultViewOptions.ts @@ -1,6 +1,5 @@ -// types -import type IDefaultOptions from './IDefaultOptions'; - -type IDefaultViewOptions = IDefaultOptions; +interface IDefaultViewOptions { + rpcURL: string; +} export default IDefaultViewOptions; diff --git a/src/types/NetworkIds.ts b/src/types/NetworkIds.ts new file mode 100644 index 0000000..d7a9949 --- /dev/null +++ b/src/types/NetworkIds.ts @@ -0,0 +1,3 @@ +type NetworkIds = 'testnet' | 'mainnet' | 'betanet' | 'localnet'; + +export default NetworkIds; diff --git a/src/types/index.ts b/src/types/index.ts index 20f7e67..06cb7f6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,6 +2,7 @@ export type { default as IDefaultChangeOptions } from './IDefaultChangeOptions'; export type { default as IDefaultOptions } from './IDefaultOptions'; export type { default as IDefaultViewOptions } from './IDefaultViewOptions'; export type { default as IGetOptions } from './IGetOptions'; +export type { default as NetworkIds } from './NetworkIds'; export type { default as IGrantWritePermissionWithAccountIdOptions } from './IGrantWritePermissionWithAccountIdOptions'; export type { default as IGrantWritePermissionWithPublicKeyOptions } from './IGrantWritePermissionWithPublicKeyOptions'; export type { default as IIsWritePermissionGrantedWithAccountIdOptions } from './IIsWritePermissionGrantedWithAccountIdOptions'; diff --git a/src/utils/rpcQueries/index.ts b/src/utils/rpcQueries/index.ts new file mode 100644 index 0000000..02837f7 --- /dev/null +++ b/src/utils/rpcQueries/index.ts @@ -0,0 +1 @@ +export { default } from './viewFunction'; diff --git a/src/utils/rpcQueries/types/IOptions.ts b/src/utils/rpcQueries/types/IOptions.ts new file mode 100644 index 0000000..1b0e354 --- /dev/null +++ b/src/utils/rpcQueries/types/IOptions.ts @@ -0,0 +1,8 @@ +interface IOptions { + contractId: string; + method: string; + rpcURL?: string; + args?: unknown; +} + +export default IOptions; diff --git a/src/utils/rpcQueries/types/index.ts b/src/utils/rpcQueries/types/index.ts new file mode 100644 index 0000000..68e7001 --- /dev/null +++ b/src/utils/rpcQueries/types/index.ts @@ -0,0 +1 @@ +export type { default as IOptions } from './IOptions'; diff --git a/src/utils/rpcQueries/viewFunction.test.ts b/src/utils/rpcQueries/viewFunction.test.ts new file mode 100644 index 0000000..cac2f2f --- /dev/null +++ b/src/utils/rpcQueries/viewFunction.test.ts @@ -0,0 +1,88 @@ +import { providers } from 'near-api-js'; +import viewFunction from './viewFunction'; // Adjust the import path as needed + +// Mock the near-api-js providers +jest.mock('near-api-js', () => ({ + providers: { + JsonRpcProvider: jest.fn().mockImplementation(() => ({ + query: jest.fn(), + })), + }, +})); + +describe('viewFunction', () => { + let mockQuery: jest.Mock; + + beforeEach(() => { + mockQuery = jest.fn(); + (providers.JsonRpcProvider as jest.Mock).mockImplementation(() => ({ + query: mockQuery, + })); + }); + + it('should call the provider with correct parameters and return parsed result', async () => { + const mockResult = { + result: Buffer.from(JSON.stringify({ value: 'test' })).toJSON().data, + }; + mockQuery.mockResolvedValue(mockResult); + + const options = { + contractId: 'test.near', + method: 'get_value', + args: { key: 'someKey' }, + }; + + const result = await viewFunction(options); + + expect(providers.JsonRpcProvider).toHaveBeenCalledWith({ + url: 'https://rpc.mainnet.near.org', + }); + expect(mockQuery).toHaveBeenCalledWith({ + request_type: 'call_function', + account_id: 'test.near', + method_name: 'get_value', + args_base64: Buffer.from(JSON.stringify({ key: 'someKey' })).toString( + 'base64' + ), + finality: 'optimistic', + }); + expect(result).toEqual({ value: 'test' }); + }); + + it('should use custom RPC URL if provided', async () => { + mockQuery.mockResolvedValue({ + result: Buffer.from('{}').toString('base64'), + }); + + const options = { + contractId: 'test.near', + method: 'get_value', + rpcURL: 'https://custom-rpc.near.org', + }; + + await viewFunction(options); + + expect(providers.JsonRpcProvider).toHaveBeenCalledWith({ + url: 'https://custom-rpc.near.org', + }); + }); + + it('should handle empty args', async () => { + mockQuery.mockResolvedValue({ + result: Buffer.from('{}').toString('base64'), + }); + + const options = { + contractId: 'test.near', + method: 'get_value', + }; + + await viewFunction(options); + + expect(mockQuery).toHaveBeenCalledWith( + expect.objectContaining({ + args_base64: Buffer.from('{}').toString('base64'), + }) + ); + }); +}); diff --git a/src/utils/rpcQueries/viewFunction.ts b/src/utils/rpcQueries/viewFunction.ts new file mode 100644 index 0000000..1e01f74 --- /dev/null +++ b/src/utils/rpcQueries/viewFunction.ts @@ -0,0 +1,59 @@ +import { providers } from 'near-api-js'; +import { printTxOutcomeLogs } from '@near-js/utils'; + +// types +import type { IOptions } from './types'; + +type ViewFunctionResult = + | string + | number + | boolean + | null + | object + | unknown[] + | undefined; + +function parseJsonFromRawResponse(response: Uint8Array): ViewFunctionResult { + return JSON.parse(new TextDecoder().decode(response)); +} + +function base64Encode(str: string): string { + try { + return btoa(str); + } catch (err) { + return Buffer.from(str).toString('base64'); + } +} + +export default async function viewFunction({ + contractId, + method, + rpcURL, + args = {}, +}: IOptions): Promise { + const url = rpcURL || `https://rpc.mainnet.near.org`; + const provider = new providers.JsonRpcProvider({ url }); + + const res = await provider.query({ + request_type: 'call_function', + account_id: contractId, + method_name: method, + args_base64: base64Encode(JSON.stringify(args)), + finality: 'optimistic', + }); + + if ('logs' in res && Array.isArray(res.logs) && res.logs.length > 0) { + printTxOutcomeLogs({ contractId, logs: res.logs }); + } + + if ( + 'result' in res && + Array.isArray(res.result) && + res.result.length > 0 && + res.result !== undefined + ) { + return parseJsonFromRawResponse(new Uint8Array(res.result)); + } + + return undefined; +}