diff --git a/packages/core/src/lib/wallet-manager.spec.ts b/packages/core/src/lib/wallet-manager.spec.ts deleted file mode 100644 index e3e66f3df..000000000 --- a/packages/core/src/lib/wallet-manager.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { WalletManager } from "./wallet-manager"; -import { setupWalletSelector } from "./wallet-selector"; -import type { - WalletSelector, - WalletSelectorParams, -} from "./wallet-selector.types"; - -// Mock implementations for required modules -const _state: Record = {}; - -global.localStorage = { - getItem: jest.fn((key) => _state[key] || null), - setItem: jest.fn((key, value) => { - _state[key] = value; - }), - removeItem: jest.fn((key) => { - delete _state[key]; - }), - clear: jest.fn(() => { - for (const key in _state) { - delete _state[key]; - } - }), - get length() { - return Object.keys(_state).length; - }, - key: jest.fn((index) => Object.keys(_state)[index] || null), -}; - -jest.mock("./wallet-selector", () => { - const originalModule = jest.requireActual("./wallet-selector"); - return { - ...originalModule, - setupWalletSelector: jest.fn(), - }; -}); - -describe("WalletManager", () => { - let isSelectorResolved: boolean; - - beforeEach(() => { - jest.clearAllMocks(); - - isSelectorResolved = false; - }); - - it("waits for selector to be resolved before getAccountId returns a result", async () => { - const mockedSetupWalletSelector = - setupWalletSelector as jest.MockedFunction; - - mockedSetupWalletSelector.mockImplementationOnce(async () => { - await new Promise((res) => setTimeout(res, 100)); - - isSelectorResolved = true; - - return { - isSignedIn() { - return true; - }, - store: { - getState() { - return { accounts: [{ accountId: "test" }] }; - }, - }, - } as WalletSelector; - }); - - const wallet = new WalletManager({} as WalletSelectorParams); - expect(isSelectorResolved).toBe(false); - - const accountId = await wallet.getAccountId(); - - expect(isSelectorResolved).toBe(true); - expect(accountId).toBe("test"); - }); -}); diff --git a/packages/core/src/lib/wallet-manager.ts b/packages/core/src/lib/wallet-manager.ts deleted file mode 100644 index 72f65d55f..000000000 --- a/packages/core/src/lib/wallet-manager.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { providers, utils } from "near-api-js"; -import type { WalletSelectorState } from "./store.types"; -import { setupWalletSelector } from "./wallet-selector"; -import type { - WalletSelector, - WalletSelectorParams, -} from "./wallet-selector.types"; -import type { FinalExecutionOutcome } from "near-api-js/lib/providers"; -import type { Transaction } from "./wallet"; -import type { - AccessKeyList, - AccessKeyView, - AccountView, - CodeResult, - RpcQueryRequest, - ViewStateResult, -} from "near-api-js/lib/providers/provider"; - -type OnAccountChange = (account: string) => void; - -const THIRTY_TGAS = "30000000000000"; -const NO_DEPOSIT = "0"; - -export class WalletManager { - public readonly selector: Promise; - - constructor(public readonly params: WalletSelectorParams) { - this.selector = setupWalletSelector(this.params); - } - - public async subscribeOnAccountChange( - onAccountChangeFn: OnAccountChange - ): Promise { - const selector = await this.selector; - - selector.store.observable.subscribe(async (state: WalletSelectorState) => { - const signedAccount = state?.accounts.find( - (account) => account.active - )?.accountId; - - onAccountChangeFn(signedAccount || ""); - }); - } - - /** - * - * @returns {string} empty string if not signed in, otherwise Account ID - */ - public async getAccountId(): Promise { - const selector = await this.selector; - - const isSignedIn = selector.isSignedIn(); - - if (!isSignedIn) { - return ""; - } - - return selector.store.getState().accounts[0].accountId; - } - - public signIn = async ( - contractId: string, - methodNames?: Array - ): Promise => { - const selector = await this.selector; - const wallet = await selector.wallet(); - - await wallet.signIn({ - contractId: contractId, - methodNames: methodNames, - // required for hardware wallets - accounts: [], - }); - }; - - public async signOut(): Promise { - const selector = await this.selector; - const wallet = await selector.wallet(); - - await wallet.signOut(); - } - - public async viewMethod(contractId: string, method: string, args = {}) { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const request: RpcQueryRequest = { - request_type: "call_function", - account_id: contractId, - method_name: method, - args_base64: Buffer.from(JSON.stringify(args)).toString("base64"), - finality: "optimistic", - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await provider.query(request); - - return JSON.parse(Buffer.from(response.result).toString()); - } - - public async callMethod( - contractId: string, - method: string, - args = {}, - gas = THIRTY_TGAS, - deposit = NO_DEPOSIT - ) { - const selector = await this.selector; - const wallet = await selector.wallet(); - - const outcome = await wallet.signAndSendTransaction({ - receiverId: contractId, - actions: [ - { - type: "FunctionCall", - params: { - methodName: method, - args, - gas, - deposit, - }, - }, - ], - }); - - return providers.getTransactionLastResult(outcome as FinalExecutionOutcome); - } - - public async getTransactionResult( - txHash: string | Uint8Array, - signerAccountId: string - ) { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const transaction = await provider.txStatus(txHash, signerAccountId); - return providers.getTransactionLastResult(transaction); - } - - public signAndSendTransaction = async (transaction: Transaction) => { - const selector = await this.selector; - const wallet = await selector.wallet(); - - const outcome = await wallet.signAndSendTransaction(transaction); - - if (!outcome) { - throw new Error(`Transaction wasn't delivered`); - } - - return providers.getTransactionLastResult(outcome); - }; - - public signAndSendTransactions = async (transactions: Array) => { - const selector = await this.selector; - const wallet = await selector.wallet(); - - const outcomes = await wallet.signAndSendTransactions({ - transactions: transactions, - }); - - if (!outcomes) { - throw new Error(`Transactions weren't delivered`); - } - - return outcomes.map((outcome) => - providers.getTransactionLastResult(outcome) - ); - }; - - public async getBalance(accountId: string): Promise { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const request: RpcQueryRequest = { - request_type: "view_account", - account_id: accountId, - finality: "final", - }; - const account = await provider.query(request); - - return account.amount || "0"; - } - - public async getFormattedBalance( - accountId: string, - fracDigits?: number - ): Promise { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const request: RpcQueryRequest = { - request_type: "view_account", - account_id: accountId, - finality: "final", - }; - const account = await provider.query(request); - - if (!account.amount) { - return "0"; - } - - return utils.format.formatNearAmount(account.amount, fracDigits); - } - - public async viewAccessKey(accountId: string, publicKey: string) { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const request: RpcQueryRequest = { - request_type: "view_access_key", - account_id: accountId, - public_key: publicKey, - finality: "final", - }; - return await provider.query(request); - } - - public async viewAccessKeys(accountId: string) { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const request: RpcQueryRequest = { - request_type: "view_access_key_list", - account_id: accountId, - finality: "final", - }; - return await provider.query(request); - } - - public async viewState(accountId: string, prefix?: string) { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const prefixBase64 = prefix ? Buffer.from(prefix).toString("base64") : ""; - const request: RpcQueryRequest = { - request_type: "view_state", - account_id: accountId, - prefix_base64: prefixBase64, - finality: "final", - }; - return await provider.query(request); - } - - public async viewContractCode(accountId: string) { - const selector = await this.selector; - const { network } = selector.options; - const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); - - const request: RpcQueryRequest = { - request_type: "view_code", - account_id: accountId, - finality: "final", - }; - return await provider.query(request); - } -} diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index c21bcb871..6b8e4aab5 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -7,8 +7,14 @@ import type { } from "./wallet-selector.types"; import { EventEmitter, Logger, WalletModules, Provider } from "./services"; import type { Wallet } from "./wallet"; -import type { Store } from "./store.types"; +import type { Store, WalletSelectorState } from "./store.types"; import type { NetworkId, Options } from "./options.types"; +import type { + AccountView, + FinalExecutionOutcome, + RpcQueryRequest, +} from "near-api-js/lib/providers/provider"; +import { providers } from "near-api-js"; let walletSelectorInstance: WalletSelector | null = null; @@ -16,7 +22,8 @@ const createSelector = ( options: Options, store: Store, walletModules: WalletModules, - emitter: EventEmitter + emitter: EventEmitter, + provider: Provider ): WalletSelector => { return { options, @@ -68,6 +75,77 @@ const createSelector = ( off: (eventName, callback) => { emitter.off(eventName, callback); }, + async getSignedInAccountBalance() { + const accountId = this.getAccountId(); + + if (!accountId) { + throw new Error(`Not signed in`); + } + + const request: RpcQueryRequest = { + request_type: "view_account", + account_id: accountId, + finality: "final", + }; + const account = await provider.query(request); + + return account.amount || "0"; + }, + subscribeOnAccountChange(onAccountChangeFn) { + this.store.observable.subscribe(async (state: WalletSelectorState) => { + const signedAccount = state?.accounts.find( + (account) => account.active + )?.accountId; + + onAccountChangeFn(signedAccount || ""); + }); + }, + async viewMethod(contractId, method, args) { + const request: RpcQueryRequest = { + request_type: "call_function", + account_id: contractId, + method_name: method, + args_base64: Buffer.from(JSON.stringify(args)).toString("base64"), + finality: "optimistic", + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await provider.query(request); + + return JSON.parse(Buffer.from(response.result).toString()); + }, + async callMethod(contractId, method, args, gas, deposit) { + const wallet = await this.wallet(); + + const outcome = await wallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: "FunctionCall", + params: { + methodName: method, + args, + gas, + deposit, + }, + }, + ], + }); + + return providers.getTransactionLastResult( + outcome as FinalExecutionOutcome + ); + }, + getAccountId() { + const { accounts } = store.getState(); + + if (accounts.length === 0) { + return undefined; + } + + const { accountId } = accounts.at(0)!; + + return accountId; + }, }; }; @@ -94,19 +172,20 @@ export const setupWalletSelector = async ( ? params.fallbackRpcUrls : [network.nodeUrl]; + const provider = new Provider(rpcProviderUrls); const walletModules = new WalletModules({ factories: params.modules, storage, options, store, emitter, - provider: new Provider(rpcProviderUrls), + provider, }); await walletModules.setup(); if (params.allowMultipleSelectors) { - return createSelector(options, store, walletModules, emitter); + return createSelector(options, store, walletModules, emitter, provider); } if (!walletSelectorInstance) { @@ -114,7 +193,8 @@ export const setupWalletSelector = async ( options, store, walletModules, - emitter + emitter, + provider ); } diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index c26a37308..ac289750e 100644 --- a/packages/core/src/lib/wallet-selector.types.ts +++ b/packages/core/src/lib/wallet-selector.types.ts @@ -118,4 +118,39 @@ export interface WalletSelector { eventName: EventName, callback: (event: WalletSelectorEvents[EventName]) => void ): void; + + /** + * Sets up a callback function that triggers whenever the accountId is updated. + */ + subscribeOnAccountChange(onAccountChangeFn: (account: string) => void): void; + /** + * Executes a view function on a specified smart contract. + */ + viewMethod( + contractId: string, + method: string, + args: Record + ): Promise; + /** + * Executes a mutable function on a specified smart contract. + * Requires to be signed in. + * @throws {Error} if a user isn't signed in + */ + callMethod( + contractId: string, + method: string, + args: Record, + gas: string, + deposit: string + ): Promise; + /** + * Retrieves the account's balance in yoctoNear by querying the specified account's state. + * Requires to be signed in. + * @throws {Error} if a user isn't signed in + */ + getSignedInAccountBalance(): Promise; + /** + * Retrieves signed in account's ID + */ + getAccountId(): string | undefined; }