diff --git a/packages/wallet-adapter-core-new/src/WalletCoreNew.ts b/packages/wallet-adapter-core-new/src/WalletCoreNew.ts new file mode 100644 index 00000000..476eeca6 --- /dev/null +++ b/packages/wallet-adapter-core-new/src/WalletCoreNew.ts @@ -0,0 +1,1020 @@ +import EventEmitter from "eventemitter3"; +import { + AptosStandardWallet, + AvailableWallets, + WalletStandardCore, +} from "./AIP62StandardWallets"; +import { GA4 } from "./ga"; + +import { ChainIdToAnsSupportedNetworkMap, WalletReadyState } from "./constants"; +import { getSDKWallets } from "./AIP62StandardWallets/sdkWallets"; +import { WALLET_ADAPTER_CORE_VERSION } from "./version"; +import { + fetchDevnetChainId, + generalizedErrorMessage, + getAptosConfig, + handlePublishPackageTransaction, + isAptosNetwork, + isRedirectable, + removeLocalStorage, + setLocalStorage, +} from "./utils"; +import { + AccountAddress, + AccountAuthenticator, + AnyPublicKey, + AnyPublicKeyVariant, + AnyRawTransaction, + Aptos, + generateRawTransaction, + generateTransactionPayload, + InputEntryFunctionData, + InputGenerateTransactionOptions, + InputSubmitTransactionData, + MultiEd25519PublicKey, + MultiEd25519Signature, + Network, + NetworkToChainId, + PendingTransactionResponse, + SimpleTransaction, +} from "@aptos-labs/ts-sdk"; +import { + WalletChangeNetworkError, + WalletAccountChangeError, + WalletAccountError, + WalletConnectionError, + WalletGetNetworkError, + WalletNetworkChangeError, + WalletNotConnectedError, + WalletNotReadyError, + WalletNotSelectedError, + WalletSignAndSubmitMessageError, + WalletSignMessageError, + WalletSignTransactionError, + WalletSignMessageAndVerifyError, + WalletDisconnectionError, + WalletSubmitTransactionError, +} from "./error"; +import { + AptosWallet, + getAptosWallets, + isWalletWithRequiredFeatureSet, + UserResponseStatus, + AptosSignAndSubmitTransactionOutput, + UserResponse, + AptosSignTransactionOutputV1_1, + AptosSignTransactionInputV1_1, + AptosSignTransactionMethod, + AptosSignTransactionMethodV1_1, + NetworkInfo, + AccountInfo, + AptosSignMessageInput, + AptosSignMessageOutput, + AptosChangeNetworkOutput, +} from "@aptos-labs/wallet-standard"; +export type { NetworkInfo, AccountInfo } from "@aptos-labs/wallet-standard"; +import { InputTransactionData } from "./LegacyWalletPlugins/types"; +import { AptosConnectWalletConfig } from "@aptos-connect/wallet-adapter-plugin"; +import { aptosStandardSupportedWalletList } from "./AIP62StandardWallets/registry"; + +// An adapter wallet types is a wallet that is compatible with the wallet standard and the wallet adapter properties +export type AdapterWallet = AptosWallet & { + readyState?: WalletReadyState; +}; + +// An adapter not detected wallet types is a wallet that is compatible with the wallet standard but not detected +export type AdapterNotDetectedWallet = Omit< + AdapterWallet, + "features" | "version" | "chains" | "accounts" +> & { + readyState: WalletReadyState.NotDetected; +}; + +export interface DappConfig { + network: Network; + aptosApiKeys?: Partial>; + aptosConnectDappId?: string; + aptosConnect?: Omit; + mizuwallet?: { + manifestURL: string; + appId?: string; + }; +} + +export declare interface WalletCoreEvents { + connect(account: AccountInfo | null): void; + disconnect(): void; + standardWalletsAdded(wallets: AdapterWallet): void; + standardNotDetectedWalletAdded(wallets: AdapterNotDetectedWallet): void; + networkChange(network: NetworkInfo | null): void; + accountChange(account: AccountInfo | null): void; +} + +export type AdapterAccountInfo = Omit & { + // ansName is a read-only property on the standard AccountInfo type + ansName?: string; +}; + +export class WalletCoreNew extends EventEmitter { + // Local private variable to hold the wallet that is currently connected + private _wallet: AdapterWallet | null = null; + + // Local private variable to hold SDK wallets in the adapter + private readonly _sdkWallets: AdapterWallet[] = []; + + // Local array that holds all the wallets that are AIP-62 standard compatible + private _standard_wallets: AdapterWallet[] = []; + + // Local array that holds all the wallets that are AIP-62 standard compatible but are not installed on the user machine + private _standard_not_detected_wallets: AdapterNotDetectedWallet[] = []; + + // Local private variable to hold the network that is currently connected + private _network: NetworkInfo | null = null; + + // Local private variable to hold the wallet connected state + private _connected: boolean = false; + + // Local private variable to hold the connecting state + private _connecting: boolean = false; + + // Local private variable to hold the account that is currently connected + private _account: AdapterAccountInfo | null = null; + + // JSON configuration for AptosConnect + private _dappConfig: DappConfig | undefined; + + // Private array that holds all the Wallets a dapp decided to opt-in to + private _optInWallets: ReadonlyArray = []; + + // Local flag to disable the adapter telemetry tool + private _disableTelemetry: boolean = false; + + // Google Analytics 4 module + private readonly ga4: GA4 | null = null; + + constructor( + optInWallets?: ReadonlyArray, + dappConfig?: DappConfig, + disableTelemetry?: boolean + ) { + super(); + this._optInWallets = optInWallets || []; + this._dappConfig = dappConfig; + this._disableTelemetry = disableTelemetry || false; + this._sdkWallets = getSDKWallets(this._dappConfig); + + // If disableTelemetry set to false (by default), start GA4 + if (!this._disableTelemetry) { + this.ga4 = new GA4(); + } + // Strategy to detect AIP-62 standard compatible extension wallets + this.fetchExtensionAIP62AptosWallets(); + // Strategy to detect AIP-62 standard compatible SDK wallets. + // We separate the extension and sdk detection process so we dont refetch sdk wallets everytime a new + // extension wallet is detected + this.fetchSDKAIP62AptosWallets(); + + this.appendNotDetectedStandardSupportedWallets(); + } + + private fetchExtensionAIP62AptosWallets(): void { + let { aptosWallets, on } = getAptosWallets(); + this.setExtensionAIP62Wallets(aptosWallets); + + if (typeof window === "undefined") return; + // Adds an event listener for new wallets that get registered after the dapp has been loaded, + // receiving an unsubscribe function, which it can later use to remove the listener + const that = this; + const removeRegisterListener = on("register", function () { + let { aptosWallets } = getAptosWallets(); + that.setExtensionAIP62Wallets(aptosWallets); + }); + + const removeUnregisterListener = on("unregister", function () { + let { aptosWallets } = getAptosWallets(); + that.setExtensionAIP62Wallets(aptosWallets); + }); + } + + /** + * Set AIP-62 extension wallets + * + * @param extensionwWallets + */ + private setExtensionAIP62Wallets( + extensionwWallets: readonly AptosWallet[] + ): void { + // Twallet SDK fires a register event so the adapter assumes it is an extension wallet + // so filter out t wallet, remove it when twallet fixes it + const wallets = extensionwWallets.filter( + (wallet) => wallet.name !== "Dev T wallet" && wallet.name !== "T wallet" + ); + + wallets.map((wallet: AptosStandardWallet) => { + if (this.excludeWallet(wallet)) { + return; + } + + // Remove optional duplications in the _all_wallets array + this._standard_wallets = this._standard_wallets.filter( + (item) => item.name !== wallet.name + ); + + const isValid = isWalletWithRequiredFeatureSet(wallet); + if (isValid) { + // check if we already have this wallet as a not detected wallet + const index = this._standard_not_detected_wallets.findIndex( + (notDetctedWallet) => notDetctedWallet.name == wallet.name + ); + // if we do, remove it from the not detected wallets array as it is now become detected + if (index !== -1) { + this._standard_not_detected_wallets.splice(index, 1); + } + + wallet.readyState = WalletReadyState.Installed; + this._standard_wallets.push(wallet); + this.emit("standardWalletsAdded", wallet); + } + }); + } + + /** + * Set AIP-62 SDK wallets + */ + private fetchSDKAIP62AptosWallets(): void { + this._sdkWallets.map((wallet: AptosStandardWallet) => { + if (this.excludeWallet(wallet)) { + return; + } + const isValid = isWalletWithRequiredFeatureSet(wallet); + + if (isValid) { + wallet.readyState = WalletReadyState.Installed; + this._standard_wallets.push(wallet); + } + }); + } + + // Since we can't discover AIP-62 wallets that are not installed on the user machine, + // we hold a AIP-62 wallets registry to show on the wallet selector modal for the users. + // Append wallets from wallet standard support registry to the `all_wallets` array + // when wallet is not installed on the user machine + private appendNotDetectedStandardSupportedWallets(): void { + // Loop over the registry map + aptosStandardSupportedWalletList.map((supportedWallet) => { + // Check if we already have this wallet as a AIP-62 wallet standard + const existingStandardWallet = this._standard_wallets.find( + (wallet) => wallet.name == supportedWallet.name + ); + if (existingStandardWallet) { + return; + } + // If AIP-62 wallet detected but it is excluded by the dapp, dont add it to the wallets array + if ( + existingStandardWallet && + this.excludeWallet(existingStandardWallet) + ) { + return; + } + + // If AIP-62 wallet does not exist, append it to the wallet selector modal + // as an undetected wallet + if (!existingStandardWallet) { + this._standard_not_detected_wallets.push(supportedWallet); + this.emit("standardNotDetectedWalletAdded", supportedWallet); + } + }); + } + + /** + * A function that excludes an AIP-62 compatible wallet the dapp doesnt want to include + * + * @param walletName + * @returns + */ + excludeWallet(wallet: AptosStandardWallet): boolean { + // If _optInWallets is not empty, and does not include the provided wallet, + // return true to exclude the wallet, otherwise return false + if ( + this._optInWallets.length > 0 && + !this._optInWallets.includes(wallet.name as AvailableWallets) + ) { + return true; + } + return false; + } + + private recordEvent(eventName: string, additionalInfo?: object): void { + this.ga4?.gtag("event", `wallet_adapter_${eventName}`, { + wallet: this._wallet?.name, + network: this._network?.name, + network_url: this._network?.url, + adapter_core_version: WALLET_ADAPTER_CORE_VERSION, + send_to: process.env.GAID, + ...additionalInfo, + }); + } + + /** + * Helper function to ensure wallet exists + * + * @param wallet A wallet + */ + private ensureWalletExists( + wallet: AdapterWallet | null + ): asserts wallet is AdapterWallet { + if (!wallet) { + throw new WalletNotConnectedError().name; + } + if (!(wallet.readyState === WalletReadyState.Installed)) + throw new WalletNotReadyError("Wallet is not set").name; + } + + /** + * Helper function to ensure account exists + * + * @param account An account + */ + private ensureAccountExists( + account: AccountInfo | null + ): asserts account is AccountInfo { + if (!account) { + throw new WalletAccountError("Account is not set").name; + } + } + + /** + * Queries and sets ANS name for the current connected wallet account + */ + private async setAnsName(): Promise { + if (this._network?.chainId && this._account) { + if (this._account.ansName) return; + // ANS supports only MAINNET or TESTNET + if ( + !ChainIdToAnsSupportedNetworkMap[this._network.chainId] || + !isAptosNetwork(this._network) + ) { + this._account.ansName = undefined; + return; + } + + const aptosConfig = getAptosConfig(this._network, this._dappConfig); + const aptos = new Aptos(aptosConfig); + const name = await aptos.ans.getPrimaryName({ + address: this._account.address.toString(), + }); + + this._account.ansName = name; + } + } + + /** + * Function to cleat wallet adapter data. + * + * - Removes current connected wallet state + * - Removes current connected account state + * - Removes current connected network state + * - Removes autoconnect local storage value + */ + private clearData(): void { + this._connected = false; + this.setWallet(null); + this.setAccount(null); + this.setNetwork(null); + removeLocalStorage(); + } + + /** + * Sets the connected wallet + * + * @param wallet A wallet + */ + setWallet(wallet: AptosWallet | null): void { + this._wallet = wallet; + } + + /** + * Sets the connected account + * + * @param account An account + */ + setAccount(account: AccountInfo | null): void { + this._account = account; + } + + /** + * Sets the connected network + * + * @param network A network + */ + setNetwork(network: NetworkInfo | null): void { + this._network = network; + } + + /** + * Helper function to detect whether a wallet is connected + * + * @returns boolean + */ + isConnected(): boolean { + return this._connected; + } + + /** + * Getter to fetch all detected wallets + */ + get wallets(): ReadonlyArray { + return this._standard_wallets; + } + + get notDetectedWallets(): ReadonlyArray { + return this._standard_not_detected_wallets; + } + + /** + * Getter for the current connected wallet + * + * @return wallet info + * @throws WalletNotSelectedError + */ + get wallet(): AptosWallet | null { + try { + if (!this._wallet) return null; + return this._wallet; + } catch (error: any) { + throw new WalletNotSelectedError(error).message; + } + } + + /** + * Getter for the current connected account + * + * @return account info + * @throws WalletAccountError + */ + get account(): AccountInfo | null { + try { + return this._account; + } catch (error: any) { + throw new WalletAccountError(error).message; + } + } + + /** + * Getter for the current wallet network + * + * @return network info + * @throws WalletGetNetworkError + */ + get network(): NetworkInfo | null { + try { + return this._network; + } catch (error: any) { + throw new WalletGetNetworkError(error).message; + } + } + + /** + * Helper function to run some checks before we connect with a wallet. + * + * @param walletName. The wallet name we want to connect with. + */ + async connect(walletName: string): Promise { + // Checks the wallet exists in the detected wallets array + const allDetectedWallets = this._standard_wallets; + + const selectedWallet = allDetectedWallets.find( + (wallet: AdapterWallet) => wallet.name === walletName + ); + + if (!selectedWallet) return; + + // Check if wallet is already connected + if (this._connected) { + // if the selected wallet is already connected, we don't need to connect again + if (this._wallet?.name === walletName) + throw new WalletConnectionError( + `${walletName} wallet is already connected` + ).message; + } + + // Check if we are in a redirectable view (i.e on mobile AND not in an in-app browser) + // Ignore if wallet is installed (iOS extension) + if ( + isRedirectable() && + selectedWallet.readyState !== WalletReadyState.Installed + ) { + // use wallet deep link + if (selectedWallet.features["aptos:openInMobileApp"]?.openInMobileApp) { + selectedWallet.features["aptos:openInMobileApp"]?.openInMobileApp(); + return; + } + + return; + } + + // Check wallet state is Installed + if (selectedWallet.readyState !== WalletReadyState.Installed) { + return; + } + + // Now we can connect to the wallet + await this.connectWallet(selectedWallet); + } + + /** + * Connects a wallet to the dapp. + * On connect success, we set the current account and the network, and keeping the selected wallet + * name in LocalStorage to support autoConnect function. + * + * @param selectedWallet. The wallet we want to connect. + * @emit emits "connect" event + * @throws WalletConnectionError + */ + async connectWallet(selectedWallet: AdapterWallet): Promise { + try { + this._connecting = true; + this.setWallet(selectedWallet); + const response = await selectedWallet.features["aptos:connect"].connect(); + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + const account = response.args; + this.setAccount(account); + const network = await selectedWallet.features["aptos:network"]?.network(); + this.setNetwork(network); + await this.setAnsName(); + setLocalStorage(selectedWallet.name); + this._connected = true; + this.recordEvent("wallet_connect"); + this.emit("connect", account); + } catch (error: any) { + this.clearData(); + const errMsg = generalizedErrorMessage(error); + throw new WalletConnectionError(errMsg).message; + } finally { + this._connecting = false; + } + } + + /** + * Disconnect the current connected wallet. On success, we clear the + * current account, current network and LocalStorage data. + * + * @emit emits "disconnect" event + * @throws WalletDisconnectionError + */ + async disconnect(): Promise { + try { + this.ensureWalletExists(this._wallet); + await this._wallet.features["aptos:disconnect"].disconnect(); + this.clearData(); + this.recordEvent("wallet_disconnect"); + this.emit("disconnect"); + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletDisconnectionError(errMsg).message; + } + } + + /** + * Signs and submits a transaction to chain + * + * @param transactionInput InputTransactionData + * @returns PendingTransactionResponse + */ + async signAndSubmitTransaction( + transactionInput: InputTransactionData + ): Promise { + try { + if ("function" in transactionInput.data) { + if ( + transactionInput.data.function === + "0x1::account::rotate_authentication_key_call" + ) { + throw new WalletSignAndSubmitMessageError("SCAM SITE DETECTED") + .message; + } + + if ( + transactionInput.data.function === "0x1::code::publish_package_txn" + ) { + ({ + metadataBytes: transactionInput.data.functionArguments[0], + byteCode: transactionInput.data.functionArguments[1], + } = handlePublishPackageTransaction(transactionInput)); + } + } + this.ensureWalletExists(this._wallet); + this.ensureAccountExists(this._account); + this.recordEvent("sign_and_submit_transaction"); + + if (this._wallet.features["aptos:signAndSubmitTransaction"]) { + // check for backward compatibility. before version 1.1.0 the standard expected + // AnyRawTransaction input so the adapter built the transaction before sending it to the wallet + if ( + this._wallet.features["aptos:signAndSubmitTransaction"]?.version !== + "1.1.0" + ) { + const aptosConfig = getAptosConfig(this._network, this._dappConfig); + + const aptos = new Aptos(aptosConfig); + const transaction = await aptos.transaction.build.simple({ + sender: this._account.address.toString(), + data: transactionInput.data, + options: transactionInput.options, + }); + + type AptosSignAndSubmitTransactionV1Method = ( + transaction: AnyRawTransaction + ) => Promise>; + + const signAndSubmitTransactionMethod = this._wallet.features[ + "aptos:signAndSubmitTransaction" + ] + .signAndSubmitTransaction as unknown as AptosSignAndSubmitTransactionV1Method; + + const response = (await signAndSubmitTransactionMethod( + transaction + )) as UserResponse; + + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + + return response.args; + } + + const response = await this._wallet.features[ + "aptos:signAndSubmitTransaction" + ].signAndSubmitTransaction({ + payload: transactionInput.data, + gasUnitPrice: transactionInput.options?.gasUnitPrice, + maxGasAmount: transactionInput.options?.maxGasAmount, + }); + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + return response.args; + } + + // If wallet does not support signAndSubmitTransaction + // the adapter will sign and submit it for the dapp. + const aptosConfig = getAptosConfig(this._network, this._dappConfig); + + const aptos = new Aptos(aptosConfig); + const transaction = await aptos.transaction.build.simple({ + sender: this._account.address, + data: transactionInput.data, + options: transactionInput.options, + }); + + const signTransactionResponse = await this.signTransaction(transaction); + const response = await this.submitTransaction({ + transaction, + senderAuthenticator: + "authenticator" in signTransactionResponse + ? signTransactionResponse.authenticator + : signTransactionResponse, + }); + return { hash: response.hash }; + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletSignAndSubmitMessageError(errMsg).message; + } + } + + /** + * Signs a transaction + * + * To support both existing wallet adapter V1 and V2, we support 2 input types + * + * @param transactionOrPayload AnyRawTransaction - V2 input | Types.TransactionPayload - V1 input + * @param options optional. V1 input + * + * @returns AccountAuthenticator + */ + async signTransaction( + transactionOrPayload: AnyRawTransaction | InputTransactionData, + asFeePayer?: boolean, + options?: InputGenerateTransactionOptions & { + expirationSecondsFromNow?: number; + expirationTimestamp?: number; + } + ): Promise { + /** + * All standard compatible wallets should support AnyRawTransaction for signTransaction version 1.0.0 + * For standard signTransaction version 1.1.0, the standard expects a transaction input + * + * So, if the input is AnyRawTransaction, we can directly call the wallet's signTransaction method + * + * + * If the input is InputTransactionData, we need to + * 1. check if the wallet supports signTransaction version 1.1.0 - if so, we convert the input to the standard expected input + * 2. if it does not support signTransaction version 1.1.0, we convert it to a rawTransaction input and call the wallet's signTransaction method + */ + + try { + this.ensureWalletExists(this._wallet); + this.ensureAccountExists(this._account); + this.recordEvent("sign_transaction"); + + // dapp sends a generated transaction (i.e AnyRawTransaction), which is supported by the wallet at signMessage version 1.0.0 + if ("rawTransaction" in transactionOrPayload) { + const response = (await this._wallet?.features[ + "aptos:signTransaction" + ].signTransaction( + transactionOrPayload, + asFeePayer + )) as UserResponse; + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + return response.args; + } // dapp sends a transaction data input (i.e InputTransactionData), which is supported by the wallet at signMessage version 1.1.0 + else if ( + this._wallet.features["aptos:signTransaction"]?.version === "1.1" + ) { + // convert input to standard expected input + const signTransactionV1_1StandardInput: AptosSignTransactionInputV1_1 = + { + payload: transactionOrPayload.data, + expirationTimestamp: options?.expirationTimestamp, + expirationSecondsFromNow: options?.expirationSecondsFromNow, + gasUnitPrice: options?.gasUnitPrice, + maxGasAmount: options?.maxGasAmount, + sequenceNumber: options?.accountSequenceNumber, + sender: transactionOrPayload.sender + ? { address: AccountAddress.from(transactionOrPayload.sender) } + : undefined, + }; + + const walletSignTransactionMethod = this._wallet?.features[ + "aptos:signTransaction" + ].signTransaction as AptosSignTransactionMethod & + AptosSignTransactionMethodV1_1; + + const response = (await walletSignTransactionMethod( + signTransactionV1_1StandardInput + )) as UserResponse; + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + return response.args; + } else { + // dapp input is InputTransactionData but the wallet does not support it, so we convert it to a rawTransaction + const aptosConfig = getAptosConfig(this._network, this._dappConfig); + const payload = await generateTransactionPayload({ + ...(transactionOrPayload.data as InputEntryFunctionData), + aptosConfig, + }); + const rawTransaction = await generateRawTransaction({ + aptosConfig, + payload, + sender: this._account.address, + options: options, + }); + const response = (await this._wallet?.features[ + "aptos:signTransaction" + ].signTransaction( + new SimpleTransaction(rawTransaction), + asFeePayer + )) as UserResponse; + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + return response.args; + } + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletSignTransactionError(errMsg).message; + } + } + + /** + * Sign message (doesnt submit to chain). + * + * @param message + * @return response from the wallet's signMessage function + * @throws WalletSignMessageError + */ + async signMessage( + message: AptosSignMessageInput + ): Promise { + try { + this.ensureWalletExists(this._wallet); + this.recordEvent("sign_message"); + + const response = + await this._wallet?.features["aptos:signMessage"]?.signMessage(message); + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + return response.args; + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletSignMessageError(errMsg).message; + } + } + + /** + * Submits transaction to chain + * + * @param transaction + * @returns PendingTransactionResponse + */ + async submitTransaction( + transaction: InputSubmitTransactionData + ): Promise { + // The standard does not support submitTransaction, so we use the adapter to submit the transaction + try { + this.ensureWalletExists(this._wallet); + + const { additionalSignersAuthenticators } = transaction; + const transactionType = + additionalSignersAuthenticators !== undefined + ? "multi-agent" + : "simple"; + this.recordEvent("submit_transaction", { + transaction_type: transactionType, + }); + + const aptosConfig = getAptosConfig(this._network, this._dappConfig); + const aptos = new Aptos(aptosConfig); + if (additionalSignersAuthenticators !== undefined) { + const multiAgentTxn = { + ...transaction, + additionalSignersAuthenticators, + }; + return aptos.transaction.submit.multiAgent(multiAgentTxn); + } else { + return aptos.transaction.submit.simple(transaction); + } + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletSubmitTransactionError(errMsg).message; + } + } + + /** + Event for when account has changed on the wallet + @return the new account info + @throws WalletAccountChangeError + */ + async onAccountChange(): Promise { + try { + this.ensureWalletExists(this._wallet); + await this._wallet.features["aptos:onAccountChange"]?.onAccountChange( + async (data: AccountInfo) => { + this.setAccount(data); + await this.setAnsName(); + this.recordEvent("account_change"); + this.emit("accountChange", this._account); + } + ); + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletAccountChangeError(errMsg).message; + } + } + + /** + Event for when network has changed on the wallet + @return the new network info + @throws WalletNetworkChangeError + */ + async onNetworkChange(): Promise { + try { + this.ensureWalletExists(this._wallet); + await this._wallet.features["aptos:onNetworkChange"]?.onNetworkChange( + async (data: NetworkInfo) => { + this.setNetwork(data); + await this.setAnsName(); + this.emit("networkChange", this._network); + } + ); + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletNetworkChangeError(errMsg).message; + } + } + + /** + * Sends a change network request to the wallet to change the connected network + * + * @param network + * @returns AptosChangeNetworkOutput + */ + async changeNetwork(network: Network): Promise { + try { + this.ensureWalletExists(this._wallet); + this.recordEvent("change_network_request", { + from: this._network?.name, + to: network, + }); + const chainId = + network === Network.DEVNET + ? await fetchDevnetChainId() + : NetworkToChainId[network]; + + const networkInfo: NetworkInfo = { + name: network, + chainId, + }; + + if (this._wallet.features["aptos:changeNetwork"]) { + const response = + await this._wallet.features["aptos:changeNetwork"].changeNetwork( + networkInfo + ); + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("User has rejected the request") + .message; + } + return response.args; + } + + throw new WalletChangeNetworkError( + `${this._wallet.name} does not support changing network request` + ).message; + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletChangeNetworkError(errMsg).message; + } + } + + /** + * Signs a message and verifies the signer + * @param message SignMessagePayload + * @returns boolean + */ + async signMessageAndVerify(message: AptosSignMessageInput): Promise { + try { + this.ensureWalletExists(this._wallet); + this.ensureAccountExists(this._account); + this.recordEvent("sign_message_and_verify"); + + try { + // sign the message + const response = (await this._wallet.features[ + "aptos:signMessage" + ].signMessage(message)) as UserResponse; + + if (response.status === UserResponseStatus.REJECTED) { + throw new WalletConnectionError("Failed to sign a message").message; + } + + // For Keyless wallet accounts we skip verification for now. + // TODO: Remove when client-side verification is done in SDK. + if ( + this._account.publicKey instanceof AnyPublicKey && + this._account.publicKey.variant === AnyPublicKeyVariant.Keyless + ) { + return true; + } + + let verified = false; + // if is a multi sig wallet with a MultiEd25519Signature type + if (response.args.signature instanceof MultiEd25519Signature) { + if (!(this._account.publicKey instanceof MultiEd25519PublicKey)) { + throw new WalletSignMessageAndVerifyError( + "Public key and Signature type mismatch" + ).message; + } + const { fullMessage, signature } = response.args; + const bitmap = signature.bitmap; + if (bitmap) { + const minKeysRequired = this._account.publicKey.threshold; + if (signature.signatures.length < minKeysRequired) { + verified = false; + } else { + verified = this._account.publicKey.verifySignature({ + message: new TextEncoder().encode(fullMessage), + signature, + }); + } + } + } else { + verified = this._account.publicKey.verifySignature({ + message: new TextEncoder().encode(response.args.fullMessage), + signature: response.args.signature, + }); + } + return verified; + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletSignMessageAndVerifyError(errMsg).message; + } + } catch (error: any) { + const errMsg = generalizedErrorMessage(error); + throw new WalletSignMessageAndVerifyError(errMsg).message; + } + } +} diff --git a/packages/wallet-adapter-react-new/src/WalletProviderNew.tsx b/packages/wallet-adapter-react-new/src/WalletProviderNew.tsx new file mode 100644 index 00000000..04aed89a --- /dev/null +++ b/packages/wallet-adapter-react-new/src/WalletProviderNew.tsx @@ -0,0 +1,314 @@ +import { + AvailableWallets, + DappConfig, + AccountInfo, + AdapterWallet, + NetworkInfo, + WalletCoreNew, + InputTransactionData, + AptosSignAndSubmitTransactionOutput, + AnyRawTransaction, + InputGenerateTransactionOptions, + AccountAuthenticator, + AptosSignTransactionOutputV1_1, + AptosSignMessageInput, + AptosSignMessageOutput, + AdapterNotDetectedWallet, +} from "@aptos-labs/wallet-adapter-core-new"; +import { ReactNode, FC, useState, useEffect, useCallback } from "react"; +import { WalletContextNew } from "./useWalletNew"; + +export interface AptosWalletProviderPropsNew { + children: ReactNode; + optInWallets?: ReadonlyArray; + autoConnect?: boolean; + dappConfig?: DappConfig; + disableTelemetry?: boolean; + onError?: (error: any) => void; +} + +const initialState: { + account: AccountInfo | null; + network: NetworkInfo | null; + connected: boolean; + wallet: AdapterWallet | null; +} = { + connected: false, + account: null, + network: null, + wallet: null, +}; + +export const AptosWalletAdapterProviderNew: FC = ({ + children, + optInWallets, + autoConnect = false, + dappConfig, + disableTelemetry = false, + onError, +}: AptosWalletProviderPropsNew) => { + const [{ account, network, connected, wallet }, setState] = + useState(initialState); + + const [isLoading, setIsLoading] = useState(true); + const [walletCore, setWalletCore] = useState(); + + const [wallets, setWallets] = useState>([]); + const [notDetectedWallets, setNotDetectedWallets] = useState< + ReadonlyArray + >([]); + // Initialize WalletCore on first load + useEffect(() => { + const walletCore = new WalletCoreNew( + optInWallets, + dappConfig, + disableTelemetry + ); + setWalletCore(walletCore); + }, []); + + // Update initial Wallets state once WalletCore has been initialized + useEffect(() => { + setWallets(walletCore?.wallets ?? []); + setNotDetectedWallets(walletCore?.notDetectedWallets ?? []); + }, [walletCore]); + + useEffect(() => { + if (autoConnect) { + if (localStorage.getItem("AptosWalletName") && !connected) { + connect(localStorage.getItem("AptosWalletName") as string); + } else { + // if we dont use autoconnect set the connect is loading to false + setIsLoading(false); + } + } + }, [autoConnect, wallets]); + + const connect = async (walletName: string): Promise => { + try { + setIsLoading(true); + await walletCore?.connect(walletName); + } catch (error: any) { + if (onError) onError(error); + return Promise.reject(error); + } finally { + setIsLoading(false); + } + }; + + const disconnect = async (): Promise => { + try { + await walletCore?.disconnect(); + } catch (error) { + if (onError) onError(error); + return Promise.reject(error); + } + }; + + const signAndSubmitTransaction = async ( + transaction: InputTransactionData + ): Promise => { + try { + if (!walletCore) { + throw new Error("WalletCore is not initialized"); + } + return await walletCore.signAndSubmitTransaction(transaction); + } catch (error: any) { + if (onError) onError(error); + return Promise.reject(error); + } + }; + + const signTransaction = async ( + transactionOrPayload: AnyRawTransaction | InputTransactionData, + asFeePayer?: boolean, + options?: InputGenerateTransactionOptions & { + expirationSecondsFromNow?: number; + expirationTimestamp?: number; + } + ): Promise => { + if (!walletCore) { + throw new Error("WalletCore is not initialized"); + } + try { + return await walletCore.signTransaction( + transactionOrPayload, + asFeePayer, + options + ); + } catch (error: any) { + if (onError) onError(error); + return Promise.reject(error); + } + }; + + const signMessage = async ( + message: AptosSignMessageInput + ): Promise => { + if (!walletCore) { + throw new Error("WalletCore is not initialized"); + } + try { + return await walletCore?.signMessage(message); + } catch (error: any) { + if (onError) onError(error); + return Promise.reject(error); + } + }; + + const signMessageAndVerify = async ( + message: AptosSignMessageInput + ): Promise => { + if (!walletCore) { + throw new Error("WalletCore is not initialized"); + } + try { + return await walletCore?.signMessageAndVerify(message); + } catch (error: any) { + if (onError) onError(error); + return Promise.reject(error); + } + }; + + // Handle the adapter's connect event + const handleConnect = (): void => { + setState((state) => { + return { + ...state, + connected: true, + account: walletCore?.account || null, + network: walletCore?.network || null, + wallet: walletCore?.wallet || null, + }; + }); + }; + + // Handle the adapter's account change event + const handleAccountChange = useCallback((): void => { + if (!connected) return; + if (!walletCore?.wallet) return; + setState((state) => { + return { + ...state, + account: walletCore?.account || null, + }; + }); + }, [connected]); + + // Handle the adapter's network event + const handleNetworkChange = useCallback((): void => { + if (!connected) return; + if (!walletCore?.wallet) return; + setState((state) => { + return { + ...state, + network: walletCore?.network || null, + }; + }); + }, [connected]); + + useEffect(() => { + if (connected) { + walletCore?.onAccountChange(); + walletCore?.onNetworkChange(); + } + }, [connected]); + + // Handle the adapter's disconnect event + const handleDisconnect = (): void => { + if (!connected) return; + setState((state) => { + return { + ...state, + connected: false, + account: walletCore?.account || null, + network: walletCore?.network || null, + wallet: null, + }; + }); + }; + + const handleStandardWalletsAdded = (standardWallet: AdapterWallet): void => { + // Manage current wallet state by removing optional duplications + // as new wallets are coming + const existingWalletIndex = wallets.findIndex( + (wallet) => wallet.name == standardWallet.name + ); + if (existingWalletIndex !== -1) { + // If wallet exists, replace it with the new wallet + setWallets((wallets) => [ + ...wallets.slice(0, existingWalletIndex), + standardWallet, + ...wallets.slice(existingWalletIndex + 1), + ]); + } else { + // If wallet doesn't exist, add it to the array + setWallets((wallets) => [...wallets, standardWallet]); + } + }; + + const handleStandardNotDetectedWalletsAdded = ( + notDetectedWallet: AdapterNotDetectedWallet + ): void => { + // Manage current wallet state by removing optional duplications + // as new wallets are coming + const existingWalletIndex = wallets.findIndex( + (wallet) => wallet.name == notDetectedWallet.name + ); + if (existingWalletIndex !== -1) { + // If wallet exists, replace it with the new wallet + setNotDetectedWallets((wallets) => [ + ...wallets.slice(0, existingWalletIndex), + notDetectedWallet, + ...wallets.slice(existingWalletIndex + 1), + ]); + } else { + // If wallet doesn't exist, add it to the array + setNotDetectedWallets((wallets) => [...wallets, notDetectedWallet]); + } + }; + + useEffect(() => { + walletCore?.on("connect", handleConnect); + walletCore?.on("accountChange", handleAccountChange); + walletCore?.on("networkChange", handleNetworkChange); + walletCore?.on("disconnect", handleDisconnect); + walletCore?.on("standardWalletsAdded", handleStandardWalletsAdded); + walletCore?.on( + "standardNotDetectedWalletAdded", + handleStandardNotDetectedWalletsAdded + ); + return () => { + walletCore?.off("connect", handleConnect); + walletCore?.off("accountChange", handleAccountChange); + walletCore?.off("networkChange", handleNetworkChange); + walletCore?.off("disconnect", handleDisconnect); + walletCore?.off("standardWalletsAdded", handleStandardWalletsAdded); + walletCore?.off( + "standardNotDetectedWalletAdded", + handleStandardNotDetectedWalletsAdded + ); + }; + }, [wallets, account]); + return ( + + {children} + + ); +}; diff --git a/packages/wallet-adapter-react-new/src/useWalletNew.tsx b/packages/wallet-adapter-react-new/src/useWalletNew.tsx new file mode 100644 index 00000000..3c4d306c --- /dev/null +++ b/packages/wallet-adapter-react-new/src/useWalletNew.tsx @@ -0,0 +1,57 @@ +import { useContext } from "react"; +import { + AccountAuthenticator, + AccountInfo, + AdapterWallet, + AnyRawTransaction, + AptosSignAndSubmitTransactionOutput, + AptosSignTransactionOutputV1_1, + InputGenerateTransactionOptions, + InputTransactionData, + NetworkInfo, + AptosSignMessageInput, + AptosSignMessageOutput, + AdapterNotDetectedWallet, +} from "@aptos-labs/wallet-adapter-core-new"; +import { createContext } from "react"; + +export interface WalletContextStateNew { + connected: boolean; + isLoading: boolean; + account: AccountInfo | null; + network: NetworkInfo | null; + connect(walletName: string): void; + signAndSubmitTransaction( + transaction: InputTransactionData + ): Promise; + signTransaction( + transaction: AnyRawTransaction | InputTransactionData, + asFeePayer?: boolean, + options?: InputGenerateTransactionOptions & { + expirationSecondsFromNow?: number; + expirationTimestamp?: number; + } + ): Promise; + signMessage(message: AptosSignMessageInput): Promise; + signMessageAndVerify(message: AptosSignMessageInput): Promise; + disconnect(): void; + wallet: AdapterWallet | null; + wallets: ReadonlyArray; + notDetectedWallets: ReadonlyArray; +} + +const DEFAULT_CONTEXT = { + connected: false, +}; + +export const WalletContextNew = createContext( + DEFAULT_CONTEXT as WalletContextStateNew +); + +export function useWalletNew(): WalletContextStateNew { + const context = useContext(WalletContextNew); + if (!context) { + throw new Error("useWallet must be used within a WalletContextState"); + } + return context; +}