diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b7f4ee..6a0a025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Added pending transactions](https://github.com/multiversx/mx-sdk-dapp-core/pull/48) - [Added transaction manager](https://github.com/multiversx/mx-sdk-dapp-core/pull/41) - [Added custom web socket url support](https://github.com/multiversx/mx-sdk-dapp-core/pull/35) - [Metamask integration](https://github.com/multiversx/mx-sdk-dapp-core/pull/27) diff --git a/package.json b/package.json index 1f970d7..a4035a7 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@lifeomic/axios-fetch": "3.0.1", "@multiversx/sdk-extension-provider": "4.0.0", "@multiversx/sdk-hw-provider": "7.0.0", - "@multiversx/sdk-metamask-provider": "0.0.7", + "@multiversx/sdk-metamask-provider": "1.0.0", "@multiversx/sdk-native-auth-client": "^1.0.8", "@multiversx/sdk-opera-provider": "1.0.0-alpha.1", "@multiversx/sdk-wallet": "4.5.1", @@ -51,7 +51,7 @@ "peerDependencies": { "@multiversx/sdk-core": ">= 13.5.0", "@multiversx/sdk-dapp-utils": ">= 1.0.0", - "@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.1", + "@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.4", "axios": ">=1.6.5", "bignumber.js": "9.x", "immer": "10.x" @@ -66,7 +66,7 @@ "@eslint/js": "9.15.0", "@multiversx/sdk-core": ">= 13.5.0", "@multiversx/sdk-dapp-utils": "1.0.0", - "@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.1", + "@multiversx/sdk-web-wallet-cross-window-provider": ">= 2.0.4", "@swc/core": "^1.4.17", "@swc/jest": "^0.2.36", "@types/jest": "29.5.13", diff --git a/src/core/managers/PendingTransactionsStateManager/PendingTransactionsStateManagement.ts b/src/core/managers/PendingTransactionsStateManager/PendingTransactionsStateManagement.ts new file mode 100644 index 0000000..c6abb54 --- /dev/null +++ b/src/core/managers/PendingTransactionsStateManager/PendingTransactionsStateManagement.ts @@ -0,0 +1,57 @@ +import { IEventBus } from 'types/manager.types'; +import { + IPendingTransactionsModalData, + PendingTransactionsEventsEnum +} from './types'; + +export class PendingTransactionsStateManager< + T extends + IEventBus = IEventBus +> { + private static instance: PendingTransactionsStateManager< + IEventBus + > | null = null; + private eventBus: T; + + private initialData: IPendingTransactionsModalData = { + isPending: false, + title: '', + subtitle: '', + shouldClose: false + }; + + private data: IPendingTransactionsModalData = { ...this.initialData }; + + private constructor(eventBus: T) { + this.eventBus = eventBus; + } + + public static getInstance>( + eventBus: U + ): PendingTransactionsStateManager { + if (!PendingTransactionsStateManager.instance) { + PendingTransactionsStateManager.instance = + new PendingTransactionsStateManager(eventBus); + } + return PendingTransactionsStateManager.instance as PendingTransactionsStateManager; + } + + public closeAndReset(): void { + this.data.shouldClose = true; + this.notifyDataUpdate(); + this.resetData(); + } + + private resetData(): void { + this.data = { ...this.initialData }; + } + + public updateData(newData: IPendingTransactionsModalData): void { + this.data = { ...this.data, ...newData }; + this.notifyDataUpdate(); + } + + private notifyDataUpdate(): void { + this.eventBus.publish(PendingTransactionsEventsEnum.DATA_UPDATE, this.data); + } +} diff --git a/src/core/managers/PendingTransactionsStateManager/index.ts b/src/core/managers/PendingTransactionsStateManager/index.ts new file mode 100644 index 0000000..d941f9d --- /dev/null +++ b/src/core/managers/PendingTransactionsStateManager/index.ts @@ -0,0 +1,2 @@ +export * from './PendingTransactionsStateManagement'; +export * from './types'; diff --git a/src/core/managers/PendingTransactionsStateManager/types/index.ts b/src/core/managers/PendingTransactionsStateManager/types/index.ts new file mode 100644 index 0000000..4f9a4f3 --- /dev/null +++ b/src/core/managers/PendingTransactionsStateManager/types/index.ts @@ -0,0 +1 @@ +export * from './pendingTransactions.types'; diff --git a/src/core/managers/PendingTransactionsStateManager/types/pendingTransactions.types.ts b/src/core/managers/PendingTransactionsStateManager/types/pendingTransactions.types.ts new file mode 100644 index 0000000..8133161 --- /dev/null +++ b/src/core/managers/PendingTransactionsStateManager/types/pendingTransactions.types.ts @@ -0,0 +1,12 @@ +// types here need to be synced with the types in sdk-dapp-core-ui +export enum PendingTransactionsEventsEnum { + 'CLOSE' = 'CLOSE', + 'DATA_UPDATE' = 'DATA_UPDATE' +} + +export interface IPendingTransactionsModalData { + isPending: boolean; + title: string; + subtitle?: string; + shouldClose?: boolean; +} diff --git a/src/core/managers/WalletConnectStateManager/WalletConnectStateManager.ts b/src/core/managers/WalletConnectStateManager/WalletConnectStateManager.ts index 6468f6e..a33d9cf 100644 --- a/src/core/managers/WalletConnectStateManager/WalletConnectStateManager.ts +++ b/src/core/managers/WalletConnectStateManager/WalletConnectStateManager.ts @@ -2,13 +2,15 @@ import { IWalletConnectModalData, WalletConnectEventsEnum } from 'core/providers/strategies/WalletConnectProviderStrategy/types'; - -interface IEventBus { - publish(event: string, data: any): void; -} - -export class WalletConnectStateManager { - private static instance: WalletConnectStateManager | null = null; +import { IEventBus } from 'types/manager.types'; + +export class WalletConnectStateManager< + T extends + IEventBus = IEventBus +> { + private static instance: WalletConnectStateManager< + IEventBus + > | null = null; private eventBus: T; private initialData: IWalletConnectModalData = { @@ -22,7 +24,7 @@ export class WalletConnectStateManager { this.eventBus = eventBus; } - public static getInstance( + public static getInstance>( eventBus: U ): WalletConnectStateManager { if (!WalletConnectStateManager.instance) { diff --git a/src/core/managers/index.ts b/src/core/managers/index.ts index b561b76..952f086 100644 --- a/src/core/managers/index.ts +++ b/src/core/managers/index.ts @@ -2,3 +2,4 @@ export * from './LedgerConnectStateManager'; export * from './SignTransactionsStateManager'; export * from './TransactionManager'; export * from './WalletConnectStateManager'; +export * from './PendingTransactionsStateManager'; diff --git a/src/core/providers/strategies/CrossWindowProviderStrategy/CrossWindowProviderStrategy.ts b/src/core/providers/strategies/CrossWindowProviderStrategy/CrossWindowProviderStrategy.ts index 6b1b858..5eaa2a5 100644 --- a/src/core/providers/strategies/CrossWindowProviderStrategy/CrossWindowProviderStrategy.ts +++ b/src/core/providers/strategies/CrossWindowProviderStrategy/CrossWindowProviderStrategy.ts @@ -1,13 +1,19 @@ import { Message, Transaction } from '@multiversx/sdk-core/out'; import { isBrowserWithPopupConfirmation } from 'constants/browser.constants'; +import { + PendingTransactionsStateManager, + PendingTransactionsEventsEnum +} from 'core/managers'; import { getAccount } from 'core/methods/account/getAccount'; import { getAddress } from 'core/methods/account/getAddress'; import { IProvider } from 'core/providers/types/providerFactory.types'; +import { PendingTransactionsModal } from 'lib/sdkDappCoreUi'; import { CrossWindowProvider } from 'lib/sdkWebWalletCrossWindowProvider'; import { crossWindowConfigSelector } from 'store/selectors'; import { networkSelector } from 'store/selectors/networkSelectors'; import { getState } from 'store/store'; import { ProviderErrorsEnum } from 'types'; +import { createModalElement } from 'utils/createModalElement'; type CrossWindowProviderProps = { address?: string; @@ -83,13 +89,35 @@ export class CrossWindowProviderStrategy { throw new Error(ProviderErrorsEnum.notInitialized); } - this.setPopupConsent(); + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, onClose, manager } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); - const signedTransactions: Transaction[] = - (await this._signTransactions(transactions)) ?? []; + manager.updateData({ + isPending: true, + title: 'Confirm on MultiversX Web Wallet', + subtitle: 'Check your MultiversX Web Wallet to sign the transaction' + }); + + this.setPopupConsent(); - // Guarded Transactions or Signed Transactions - return this.getTransactions(signedTransactions); + try { + const signedTransactions: Transaction[] = + (await this._signTransactions(transactions)) ?? []; + + // Guarded Transactions or Signed Transactions + return this.getTransactions(signedTransactions); + } catch (error) { + this.provider.cancelAction(); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } }; private signMessage = async (message: Message) => { @@ -97,8 +125,33 @@ export class CrossWindowProviderStrategy { throw new Error(ProviderErrorsEnum.notInitialized); } + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, onClose, manager } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: 'Message Signing', + subtitle: 'Check your MultiversX Web Wallet to sign the message' + }); + this.setPopupConsent(); - return this._signMessage(message); + + try { + const signedMessage = await this._signMessage(message); + + return signedMessage; + } catch (error) { + this.provider.cancelAction(); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } }; private setPopupConsent = () => { @@ -159,4 +212,27 @@ export class CrossWindowProviderStrategy { Boolean(tx.getGuardianSignature().toString('hex')) ); }; + + private getModalHandlers = async (modalElement: PendingTransactionsModal) => { + const eventBus = await modalElement.getEventBus(); + + if (!eventBus) { + throw new Error(ProviderErrorsEnum.eventBusError); + } + + const manager = PendingTransactionsStateManager.getInstance(eventBus); + + const onClose = (cancelAction = true) => { + if (!this.provider) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + if (cancelAction) { + this.provider.cancelAction(); + } + + manager.closeAndReset(); + }; + return { eventBus, manager, onClose }; + }; } diff --git a/src/core/providers/strategies/CrossWindowProviderStrategy/index.ts b/src/core/providers/strategies/CrossWindowProviderStrategy/index.ts index abcd7d7..bdf237e 100644 --- a/src/core/providers/strategies/CrossWindowProviderStrategy/index.ts +++ b/src/core/providers/strategies/CrossWindowProviderStrategy/index.ts @@ -1 +1,2 @@ export * from './CrossWindowProviderStrategy'; +export * from './types'; diff --git a/src/core/providers/strategies/ExtensionProviderStrategy/ExtensionProviderStrategy.ts b/src/core/providers/strategies/ExtensionProviderStrategy/ExtensionProviderStrategy.ts index 273438a..d37c0eb 100644 --- a/src/core/providers/strategies/ExtensionProviderStrategy/ExtensionProviderStrategy.ts +++ b/src/core/providers/strategies/ExtensionProviderStrategy/ExtensionProviderStrategy.ts @@ -1,11 +1,24 @@ +import { Message, Transaction } from '@multiversx/sdk-core/out'; import { ExtensionProvider } from '@multiversx/sdk-extension-provider/out/extensionProvider'; +import { + PendingTransactionsStateManager, + PendingTransactionsEventsEnum +} from 'core/managers'; +import { getAccount } from 'core/methods/account/getAccount'; import { getAddress } from 'core/methods/account/getAddress'; import { IProvider } from 'core/providers/types/providerFactory.types'; + +import { PendingTransactionsModal } from 'lib/sdkDappCoreUi'; import { ProviderErrorsEnum } from 'types'; +import { createModalElement } from 'utils/createModalElement'; export class ExtensionProviderStrategy { private address: string = ''; private provider: ExtensionProvider | null = null; + private _signTransactions: + | ((transactions: Transaction[]) => Promise) + | null = null; + private _signMessage: ((message: Message) => Promise) | null = null; constructor(address?: string) { this.address = address || ''; @@ -19,16 +32,24 @@ export class ExtensionProviderStrategy { await this.provider.init(); } + this._signTransactions = this.provider.signTransactions.bind(this.provider); + this._signMessage = this.provider.signMessage.bind(this.provider); + return this.buildProvider(); }; private buildProvider = () => { + const { address } = getAccount(); + if (!this.provider) { throw new Error(ProviderErrorsEnum.notInitialized); } const provider = this.provider as unknown as IProvider; - provider.setAccount({ address: this.address }); + provider.signTransactions = this.signTransactions; + provider.signMessage = this.signMessage; + + provider.setAccount({ address: this.address || address }); return provider; }; @@ -45,4 +66,93 @@ export class ExtensionProviderStrategy { this.address = address; }; + + private signTransactions = async (transactions: Transaction[]) => { + if (!this.provider || !this._signTransactions) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, manager, onClose } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: 'Confirm on MultiversX DeFi Wallet', + subtitle: 'Check your MultiversX Wallet Extension to sign the transaction' + }); + try { + const signedTransactions: Transaction[] = + await this._signTransactions(transactions); + + return signedTransactions; + } catch (error) { + this.provider.cancelAction(); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } + }; + + private signMessage = async (message: Message) => { + if (!this.provider || !this._signMessage) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + + const { eventBus, manager, onClose } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: 'Message Signing', + subtitle: 'Check your MultiversX Wallet Extension to sign the message' + }); + + try { + const signedMessage = await this._signMessage(message); + + return signedMessage; + } catch (error) { + this.provider.cancelAction(); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } + }; + + private getModalHandlers = async (modalElement: PendingTransactionsModal) => { + const eventBus = await modalElement.getEventBus(); + + if (!eventBus) { + throw new Error(ProviderErrorsEnum.eventBusError); + } + + const manager = PendingTransactionsStateManager.getInstance(eventBus); + + const onClose = (cancelAction = true) => { + if (!this.provider) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + if (cancelAction) { + this.provider.cancelAction(); + } + + manager.closeAndReset(); + }; + + return { eventBus, manager, onClose }; + }; } diff --git a/src/core/providers/strategies/IFrameProviderStrategy/IFrameProviderStrategy.ts b/src/core/providers/strategies/IFrameProviderStrategy/IFrameProviderStrategy.ts index 1560c1e..7cb4952 100644 --- a/src/core/providers/strategies/IFrameProviderStrategy/IFrameProviderStrategy.ts +++ b/src/core/providers/strategies/IFrameProviderStrategy/IFrameProviderStrategy.ts @@ -1,17 +1,28 @@ +import { Message, Transaction } from '@multiversx/sdk-core/out'; import { IframeProvider } from '@multiversx/sdk-web-wallet-iframe-provider/out'; import { IframeLoginTypes } from '@multiversx/sdk-web-wallet-iframe-provider/out/constants'; +import { + PendingTransactionsStateManager, + PendingTransactionsEventsEnum +} from 'core/managers'; import { getAccount } from 'core/methods/account/getAccount'; import { getAddress } from 'core/methods/account/getAddress'; import { IProvider } from 'core/providers/types/providerFactory.types'; +import { PendingTransactionsModal } from 'lib/sdkDappCoreUi'; import { networkSelector } from 'store/selectors/networkSelectors'; import { getState } from 'store/store'; import { ProviderErrorsEnum } from 'types'; +import { createModalElement } from 'utils/createModalElement'; import { IFrameProviderType } from './types'; export class IFrameProviderStrategy { private provider: IframeProvider | null = null; private address?: string; private type: IframeLoginTypes | null = null; + private _signTransactions: + | ((transactions: Transaction[]) => Promise) + | null = null; + private _signMessage: ((message: Message) => Promise) | null = null; constructor({ type, address }: IFrameProviderType) { this.type = type; @@ -33,6 +44,8 @@ export class IFrameProviderStrategy { this.provider.setLoginType(this.type); this.provider.setWalletUrl(String(network.iframeWalletAddress)); + this._signTransactions = this.provider.signTransactions.bind(this.provider); + this._signMessage = this.provider.signMessage.bind(this.provider); return this.buildProvider(); }; @@ -47,7 +60,8 @@ export class IFrameProviderStrategy { const provider = this.provider as unknown as IProvider; provider.setAccount({ address: this.address || address }); - + provider.signTransactions = this.signTransactions; + provider.signMessage = this.signMessage; return provider; }; @@ -64,4 +78,92 @@ export class IFrameProviderStrategy { this.address = address; }; + + private signTransactions = async (transactions: Transaction[]) => { + if (!this.provider || !this._signTransactions) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, manager, onClose } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: `Confirm on MultiversX ${this.type}`, + subtitle: `Check your MultiversX ${this.type} to sign the transaction` + }); + try { + const signedTransactions: Transaction[] = + await this._signTransactions(transactions); + + return signedTransactions; + } catch (error) { + this.provider.cancelAction(); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } + }; + + private signMessage = async (message: Message) => { + if (!this.provider || !this._signMessage || !this.type) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, manager, onClose } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: 'Message Signing', + subtitle: `Check your MultiversX ${this.type} to sign the message` + }); + + try { + const signedMessage = await this._signMessage(message); + + return signedMessage; + } catch (error) { + this.provider.cancelAction(); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } + }; + + private getModalHandlers = async (modalElement: PendingTransactionsModal) => { + const eventBus = await modalElement.getEventBus(); + + if (!eventBus) { + throw new Error(ProviderErrorsEnum.eventBusError); + } + + const manager = PendingTransactionsStateManager.getInstance(eventBus); + + const onClose = (cancelAction = true) => { + if (!this.provider) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + if (cancelAction) { + this.provider.cancelAction(); + } + + manager.closeAndReset(); + }; + + return { eventBus, manager, onClose }; + }; } diff --git a/src/core/providers/strategies/IFrameProviderStrategy/index.ts b/src/core/providers/strategies/IFrameProviderStrategy/index.ts index 8d565bb..e805579 100644 --- a/src/core/providers/strategies/IFrameProviderStrategy/index.ts +++ b/src/core/providers/strategies/IFrameProviderStrategy/index.ts @@ -1 +1,2 @@ export * from './IFrameProviderStrategy'; +export * from './types'; diff --git a/src/core/providers/strategies/LedgerProviderStrategy/LedgerProviderStrategy.ts b/src/core/providers/strategies/LedgerProviderStrategy/LedgerProviderStrategy.ts index 53a3fbf..a264637 100644 --- a/src/core/providers/strategies/LedgerProviderStrategy/LedgerProviderStrategy.ts +++ b/src/core/providers/strategies/LedgerProviderStrategy/LedgerProviderStrategy.ts @@ -149,29 +149,27 @@ export class LedgerProviderStrategy { return; } - const { eventBus: modalEventBus } = - await createModalElement({ - name: 'ledger-connect-modal', - withEventBus: true - }); + const modalElement = await createModalElement( + 'ledger-connect-modal' + ); + const eventBus = await modalElement.getEventBus(); - if (!modalEventBus) { - throw new Error('Event bus not provided for Ledger provider'); + if (!eventBus) { + throw new Error(ProviderErrorsEnum.eventBusError); } - this.eventBus = modalEventBus; - return modalEventBus; + this.eventBus = eventBus; + return eventBus; }; private signTransactions = async (transactions: Transaction[]) => { - const { modalElement: signModalElement, eventBus } = - await createModalElement({ - name: 'sign-transactions-modal', - withEventBus: true - }); + const signModalElement = await createModalElement( + 'sign-transactions-modal' + ); + const eventBus = await signModalElement.getEventBus(); if (!eventBus) { - throw new Error('Event bus not provided for Ledger provider'); + throw new Error(ProviderErrorsEnum.eventBusError); } const manager = SignTransactionsStateManager.getInstance(eventBus); diff --git a/src/core/providers/strategies/LedgerProviderStrategy/index.ts b/src/core/providers/strategies/LedgerProviderStrategy/index.ts index e385951..5840241 100644 --- a/src/core/providers/strategies/LedgerProviderStrategy/index.ts +++ b/src/core/providers/strategies/LedgerProviderStrategy/index.ts @@ -1 +1,2 @@ export * from './LedgerProviderStrategy'; +export * from './types'; diff --git a/src/core/providers/strategies/WalletConnectProviderStrategy/WalletConnectProviderStrategy.ts b/src/core/providers/strategies/WalletConnectProviderStrategy/WalletConnectProviderStrategy.ts index 5fb4072..f6f29ad 100644 --- a/src/core/providers/strategies/WalletConnectProviderStrategy/WalletConnectProviderStrategy.ts +++ b/src/core/providers/strategies/WalletConnectProviderStrategy/WalletConnectProviderStrategy.ts @@ -1,9 +1,15 @@ +import { Message, Transaction } from '@multiversx/sdk-core/out'; import { IProviderAccount, SessionEventTypes, - SessionTypes + SessionTypes, + OptionalOperation } from '@multiversx/sdk-wallet-connect-provider/out'; import { safeWindow } from 'constants/window.constants'; +import { + PendingTransactionsStateManager, + PendingTransactionsEventsEnum +} from 'core/managers'; import { WalletConnectStateManager } from 'core/managers/WalletConnectStateManager/WalletConnectStateManager'; import { getIsLoggedIn } from 'core/methods/account/getIsLoggedIn'; import { getAccountProvider } from 'core/providers/helpers/accountProvider'; @@ -11,7 +17,11 @@ import { IEventBus, IProvider } from 'core/providers/types/providerFactory.types'; -import { defineCustomElements, WalletConnectModal } from 'lib/sdkDappCoreUi'; +import { + defineCustomElements, + PendingTransactionsModal, + WalletConnectModal +} from 'lib/sdkDappCoreUi'; import { logoutAction } from 'store/actions'; import { chainIdSelector, @@ -49,6 +59,10 @@ export class WalletConnectProviderStrategy { token?: string; }) => Promise) | null = null; + private _signTransactions: + | ((transactions: Transaction[]) => Promise) + | null = null; + private _signMessage: ((message: Message) => Promise) | null = null; constructor(config?: WalletConnectConfig) { this.config = config; @@ -71,6 +85,13 @@ export class WalletConnectProviderStrategy { // Bind in order to break reference this._login = walletConnectProvider.login.bind(walletConnectProvider); + this._signTransactions = walletConnectProvider.signTransactions.bind( + walletConnectProvider + ); + this._signMessage = walletConnectProvider.signMessage.bind( + walletConnectProvider + ); + this.provider = walletConnectProvider; this.methods = dappMethods; } @@ -85,7 +106,7 @@ export class WalletConnectProviderStrategy { this.unsubscribeEvents = () => { if (!eventBus) { - throw new Error('Event bus is not initialized'); + throw new Error(ProviderErrorsEnum.eventBusError); } eventBus.unsubscribe(WalletConnectEventsEnum.CLOSE, onClose); @@ -112,6 +133,8 @@ export class WalletConnectProviderStrategy { const provider = this.provider as unknown as IProvider; provider.login = this.login; + provider.signTransactions = this.signTransactions; + provider.signMessage = this.signMessage; return provider; }; @@ -137,11 +160,11 @@ export class WalletConnectProviderStrategy { return; } - const { eventBus } = await createModalElement({ - name: 'wallet-connect-modal', - withEventBus: true - }); + const modalElement = await createModalElement( + 'wallet-connect-modal' + ); + const eventBus = await modalElement.getEventBus(); return eventBus; }; @@ -276,4 +299,124 @@ export class WalletConnectProviderStrategy { return await reconnect(); } }; + + private signTransactions = async (transactions: Transaction[]) => { + if (!this.provider || !this._signTransactions) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, manager, onClose } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: 'Confirm on the xPortal App', + subtitle: 'Check your phone to sign the transaction' + }); + try { + const signedTransactions: Transaction[] = + await this._signTransactions(transactions); + + return signedTransactions; + } catch (error) { + await this.sendCustomRequest({ + method: WalletConnectOptionalMethodsEnum.CANCEL_ACTION, + action: OptionalOperation.CANCEL_ACTION + }); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } + }; + + private signMessage = async (message: Message) => { + if (!this.provider || !this._signMessage) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + const modalElement = await createModalElement( + 'pending-transactions-modal' + ); + const { eventBus, manager, onClose } = + await this.getModalHandlers(modalElement); + + eventBus.subscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + + manager.updateData({ + isPending: true, + title: 'Message Signing', + subtitle: 'Check your MultiversX xPortal App to sign the message' + }); + + try { + const signedMessage = await this._signMessage(message); + + return signedMessage; + } catch (error) { + await this.sendCustomRequest({ + method: WalletConnectOptionalMethodsEnum.CANCEL_ACTION, + action: OptionalOperation.CANCEL_ACTION + }); + throw error; + } finally { + onClose(false); + eventBus.unsubscribe(PendingTransactionsEventsEnum.CLOSE, onClose); + } + }; + + private sendCustomRequest = async ({ + action, + method + }: { + action: OptionalOperation; + method: WalletConnectOptionalMethodsEnum; + }) => { + if (!this.provider) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + try { + await this.provider.sendCustomRequest?.({ + request: { + method, + params: { action } + } + }); + } catch (error) { + console.error(WalletConnectV2Error.actionError, error); + } + }; + + private getModalHandlers = async (modalElement: PendingTransactionsModal) => { + const eventBus = await modalElement.getEventBus(); + + if (!eventBus) { + throw new Error(ProviderErrorsEnum.eventBusError); + } + + const manager = PendingTransactionsStateManager.getInstance(eventBus); + + const onClose = async (cancelAction = true) => { + if (!this.provider) { + throw new Error(ProviderErrorsEnum.notInitialized); + } + + if (cancelAction) { + await this.sendCustomRequest({ + method: WalletConnectOptionalMethodsEnum.CANCEL_ACTION, + action: OptionalOperation.CANCEL_ACTION + }); + } + + manager.closeAndReset(); + }; + + return { eventBus, manager, onClose }; + }; } diff --git a/src/core/providers/strategies/WalletConnectProviderStrategy/index.ts b/src/core/providers/strategies/WalletConnectProviderStrategy/index.ts index 8cd6870..ea53f38 100644 --- a/src/core/providers/strategies/WalletConnectProviderStrategy/index.ts +++ b/src/core/providers/strategies/WalletConnectProviderStrategy/index.ts @@ -1 +1,2 @@ export * from './WalletConnectProviderStrategy'; +export * from './types'; diff --git a/src/core/providers/strategies/WalletConnectProviderStrategy/types/walletConnect.types.ts b/src/core/providers/strategies/WalletConnectProviderStrategy/types/walletConnect.types.ts index e9c37a6..b2222fe 100644 --- a/src/core/providers/strategies/WalletConnectProviderStrategy/types/walletConnect.types.ts +++ b/src/core/providers/strategies/WalletConnectProviderStrategy/types/walletConnect.types.ts @@ -9,7 +9,8 @@ export enum WalletConnectV2Error { userRejected = 'User rejected connection proposal', userRejectedExisting = 'User rejected existing connection proposal', errorLogout = 'Unable to remove existing pairing', - invalidChainID = 'Invalid chainID' + invalidChainID = 'Invalid chainID', + actionError = 'Unable to send event' } // types here need to be synced with the types in sdk-dapp-core-ui diff --git a/src/lib/sdkDappCoreUi.ts b/src/lib/sdkDappCoreUi.ts index 7c86e35..8cc3be8 100644 --- a/src/lib/sdkDappCoreUi.ts +++ b/src/lib/sdkDappCoreUi.ts @@ -2,6 +2,7 @@ import type { CustomElementsDefineOptions } from '@multiversx/sdk-dapp-core-ui/l export type { LedgerConnectModal } from '@multiversx/sdk-dapp-core-ui/dist/components/ledger-connect-modal'; export type { SignTransactionsModal } from '@multiversx/sdk-dapp-core-ui/dist/components/sign-transactions-modal'; export type { WalletConnectModal } from '@multiversx/sdk-dapp-core-ui/dist/components/wallet-connect-modal'; +export type { PendingTransactionsModal } from '@multiversx/sdk-dapp-core-ui/dist/components/pending-transactions-modal'; export async function defineCustomElements( win?: Window, diff --git a/src/types/manager.types.ts b/src/types/manager.types.ts new file mode 100644 index 0000000..5f6cedf --- /dev/null +++ b/src/types/manager.types.ts @@ -0,0 +1,3 @@ +export interface IEventBus { + publish(event: string, data: T): void; +} diff --git a/src/types/provider.types.ts b/src/types/provider.types.ts index 79c7ee7..5e69921 100644 --- a/src/types/provider.types.ts +++ b/src/types/provider.types.ts @@ -1,4 +1,5 @@ export enum ProviderErrorsEnum { notInitialized = 'Provider is not initialized.', - invalidType = 'Invalid type.' + invalidType = 'Invalid type.', + eventBusError = 'eventBus is not initialized' } diff --git a/src/utils/createModalElement.ts b/src/utils/createModalElement.ts index ae8bc7d..1175bd4 100644 --- a/src/utils/createModalElement.ts +++ b/src/utils/createModalElement.ts @@ -2,32 +2,16 @@ import { IEventBus } from '@multiversx/sdk-dapp-core-ui/loader'; import { safeWindow } from 'constants/index'; import { defineCustomElements } from 'lib/sdkDappCoreUi'; -type CreateModalElementType = { - name: string; - withEventBus?: boolean; -}; - export const createModalElement = async < T extends HTMLElement & { getEventBus: () => Promise } ->({ - name, - withEventBus = false -}: CreateModalElementType) => { - let eventBus: IEventBus | undefined; - +>( + name: string +) => { await defineCustomElements(safeWindow); const modalElement = document.createElement(name) as T; document.body.appendChild(modalElement); await customElements.whenDefined(name); - if (withEventBus) { - eventBus = await modalElement.getEventBus(); - - if (!eventBus) { - throw new Error(`Event bus not provided for ${name}.`); - } - } - - return { modalElement, eventBus }; + return modalElement; }; diff --git a/yarn.lock b/yarn.lock index a81f001..8066a7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1031,12 +1031,13 @@ buffer "6.0.3" platform "1.3.6" -"@multiversx/sdk-metamask-provider@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@multiversx/sdk-metamask-provider/-/sdk-metamask-provider-0.0.7.tgz#d53e15493a94d44490c47ea8e9a3eafa9b63591b" - integrity sha512-eqA1z/QIflauv5lqetKw2J5E7UooSTcHbZsxwkquWFnO6j1hj35/odS4P8AcbCOVssenZ+THkLOR7kxx5l7e5g== +"@multiversx/sdk-metamask-provider@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@multiversx/sdk-metamask-provider/-/sdk-metamask-provider-1.0.0.tgz#ba3f373d498688209c4dff81732e3c5cc2d219fa" + integrity sha512-5AsaeAQlhgH/nH0fxGlUOzKcjxAlsqZ8JTwKev3mZWKrbugOOukp8rM3a3oUCRO4yTDI8z3VAYb8ELvnNfYM3Q== dependencies: "@metamask/providers" "16.0.0" + protobufjs "7.4.0" "@multiversx/sdk-native-auth-client@^1.0.8": version "1.0.9" @@ -1084,10 +1085,10 @@ tweetnacl "1.0.3" uuid "8.3.2" -"@multiversx/sdk-web-wallet-cross-window-provider@>= 2.0.1": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@multiversx/sdk-web-wallet-cross-window-provider/-/sdk-web-wallet-cross-window-provider-2.0.2.tgz#ec9176c44b561d906d6613486bfc6e089ee8f3a1" - integrity sha512-HULqqmS09OX3cymIuC+DzCzsSdkqwAa9ZXZK7xQDwPiWiUOulrA1Tj8BeqmdxfqyOfeeBWW9oNpueCEqm4jigg== +"@multiversx/sdk-web-wallet-cross-window-provider@>= 2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@multiversx/sdk-web-wallet-cross-window-provider/-/sdk-web-wallet-cross-window-provider-2.0.4.tgz#bb605e42bd68f1806c16a9152cc2f2be4e2b304b" + integrity sha512-Oh5OvNsfGUYFc6fCl7pxaX+yqnRvn0d5rVhwEHVHhNMZVv0Q3cLaZak/wLftJNaQfAPVOw0gbD573fHCUa5bUw== dependencies: qs "6.11.2" @@ -6071,6 +6072,24 @@ protobufjs@7.3.0: "@types/node" ">=13.7.0" long "^5.0.0" +protobufjs@7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" + integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"