|
| 1 | +/** @noprettier -- trying to keep it as close to the original code as possible */ |
| 2 | +/* eslint-disable -- trying to keep it as close to the original code as possible */ |
| 3 | + |
| 4 | +/** |
| 5 | + * Portions Copyright (c) 2023 tequdev |
| 6 | + * Based on: https://github.com/tequdev/xrpl-wallet-standard/blob/1f404d9507c30810d8e663a7bc7160d51b0421f1/packages/adapter/metamask/src/index.ts |
| 7 | + * |
| 8 | + * Original code (ISC license) — permission & disclaimer (required to be preserved): |
| 9 | + * |
| 10 | + * Permission to use, copy, modify, and/or distribute this software for any purpose |
| 11 | + * with or without fee is hereby granted, provided that the above copyright notice |
| 12 | + * and this permission notice appear in all copies. |
| 13 | + * |
| 14 | + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH |
| 15 | + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND |
| 16 | + * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, |
| 17 | + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM |
| 18 | + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR |
| 19 | + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR |
| 20 | + * PERFORMANCE OF THIS SOFTWARE. |
| 21 | + * |
| 22 | + * --------------------------------------------------------------------------- |
| 23 | + * Modifications Copyright (c) 2025 Axelarnetwork |
| 24 | + * Modified locally to accept an injected provider and work with EIP-6963 discovery. |
| 25 | + * Modifications are licensed under this repository's MIT license (see LICENSE). |
| 26 | + * |
| 27 | + * Notes: |
| 28 | + * - The original portions above remain under ISC and its permission/disclaimer must |
| 29 | + * be preserved in any copies of those portions. |
| 30 | + * - If you build/distribute a bundled/minified artifact, ensure the ISC notice |
| 31 | + * remains included in the distributed artifact (or otherwise shipped with it). |
| 32 | + */ |
| 33 | + |
| 34 | +import type { BaseProvider } from '@metamask/providers' |
| 35 | +import { type XRPLBaseWallet, XRPLWalletAccount } from '@xrpl-wallet-adapter/base' |
| 36 | +import { |
| 37 | + type StandardConnectFeature, |
| 38 | + type StandardConnectMethod, |
| 39 | + type StandardEventsFeature, |
| 40 | + type StandardEventsListeners, |
| 41 | + type StandardEventsNames, |
| 42 | + type StandardEventsOnMethod, |
| 43 | + type XRPLIdentifierString, |
| 44 | + type XRPLSignAndSubmitTransactionFeature, |
| 45 | + type XRPLSignAndSubmitTransactionMethod, |
| 46 | + type XRPLSignTransactionFeature, |
| 47 | + type XRPLSignTransactionMethod, |
| 48 | + type XRPLStandardIdentifier, |
| 49 | + XRPL_DEVNET, |
| 50 | + XRPL_MAINNET, |
| 51 | + XRPL_TESTNET, |
| 52 | +} from '@xrpl-wallet-standard/core' |
| 53 | +import type { BaseTransaction, SubmitResponse } from 'xrpl' |
| 54 | + |
| 55 | +export const SNAP_ORIGIN = 'npm:xrpl-snap' |
| 56 | + |
| 57 | +interface MetamaskAccount { |
| 58 | + account: string |
| 59 | + publicKey: string |
| 60 | +} |
| 61 | + |
| 62 | +interface NetworkInfo { |
| 63 | + chainId: number |
| 64 | + name: string |
| 65 | + nodeUrl: string |
| 66 | + explorerUrl: string |
| 67 | +} |
| 68 | + |
| 69 | +type SnapInfo = { |
| 70 | + [SNAP_ORIGIN]: { |
| 71 | + blocked: boolean |
| 72 | + enabled: boolean |
| 73 | + version: string |
| 74 | + initialPermissions: Record<string, any> |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +export class MetaMaskWallet implements XRPLBaseWallet { |
| 79 | + #name = 'MetaMask' |
| 80 | + #icon = |
| 81 | + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIGlkPSJMYXllcl8xIiB4PSIwIiB5PSIwIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMTguNiAzMTguNiI+CiAgPHN0eWxlPgogICAgLnN0MSwuc3Q2e2ZpbGw6I2U0NzYxYjtzdHJva2U6I2U0NzYxYjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmR9LnN0NntmaWxsOiNmNjg1MWI7c3Ryb2tlOiNmNjg1MWJ9CiAgPC9zdHlsZT4KICA8cGF0aCBmaWxsPSIjZTI3NjFiIiBzdHJva2U9IiNlMjc2MWIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTI3NC4xIDM1LjUtOTkuNSA3My45TDE5MyA2NS44eiIvPgogIDxwYXRoIGQ9Im00NC40IDM1LjUgOTguNyA3NC42LTE3LjUtNDQuM3ptMTkzLjkgMTcxLjMtMjYuNSA0MC42IDU2LjcgMTUuNiAxNi4zLTU1LjN6bS0yMDQuNC45TDUwLjEgMjYzbDU2LjctMTUuNi0yNi41LTQwLjZ6IiBjbGFzcz0ic3QxIi8+CiAgPHBhdGggZD0ibTEwMy42IDEzOC4yLTE1LjggMjMuOSA1Ni4zIDIuNS0yLTYwLjV6bTExMS4zIDAtMzktMzQuOC0xLjMgNjEuMiA1Ni4yLTIuNXpNMTA2LjggMjQ3LjRsMzMuOC0xNi41LTI5LjItMjIuOHptNzEuMS0xNi41IDMzLjkgMTYuNS00LjctMzkuM3oiIGNsYXNzPSJzdDEiLz4KICA8cGF0aCBmaWxsPSIjZDdjMWIzIiBzdHJva2U9IiNkN2MxYjMiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTIxMS44IDI0Ny40LTMzLjktMTYuNSAyLjcgMjIuMS0uMyA5LjN6bS0xMDUgMCAzMS41IDE0LjktLjItOS4zIDIuNS0yMi4xeiIvPgogIDxwYXRoIGZpbGw9IiMyMzM0NDciIHN0cm9rZT0iIzIzMzQ0NyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJtMTM4LjggMTkzLjUtMjguMi04LjMgMTkuOS05LjF6bTQwLjkgMCA4LjMtMTcuNCAyMCA5LjF6Ii8+CiAgPHBhdGggZmlsbD0iI2NkNjExNiIgc3Ryb2tlPSIjY2Q2MTE2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Im0xMDYuOCAyNDcuNCA0LjgtNDAuNi0zMS4zLjl6TTIwNyAyMDYuOGw0LjggNDAuNiAyNi41LTM5Ljd6bTIzLjgtNDQuNy01Ni4yIDIuNSA1LjIgMjguOSA4LjMtMTcuNCAyMCA5LjF6bS0xMjAuMiAyMy4xIDIwLTkuMSA4LjIgMTcuNCA1LjMtMjguOS01Ni4zLTIuNXoiLz4KICA8cGF0aCBmaWxsPSIjZTQ3NTFmIiBzdHJva2U9IiNlNDc1MWYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTg3LjggMTYyLjEgMjMuNiA0Ni0uOC0yMi45em0xMjAuMyAyMy4xLTEgMjIuOSAyMy43LTQ2em0tNjQtMjAuNi01LjMgMjguOSA2LjYgMzQuMSAxLjUtNDQuOXptMzAuNSAwLTIuNyAxOCAxLjIgNDUgNi43LTM0LjF6Ii8+CiAgPHBhdGggZD0ibTE3OS44IDE5My41LTYuNyAzNC4xIDQuOCAzLjMgMjkuMi0yMi44IDEtMjIuOXptLTY5LjItOC4zLjggMjIuOSAyOS4yIDIyLjggNC44LTMuMy02LjYtMzQuMXoiIGNsYXNzPSJzdDYiLz4KICA8cGF0aCBmaWxsPSIjYzBhZDllIiBzdHJva2U9IiNjMGFkOWUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0ibTE4MC4zIDI2Mi4zLjMtOS4zLTIuNS0yLjJoLTM3LjdsLTIuMyAyLjIuMiA5LjMtMzEuNS0xNC45IDExIDkgMjIuMyAxNS41aDM4LjNsMjIuNC0xNS41IDExLTl6Ii8+CiAgPHBhdGggZmlsbD0iIzE2MTYxNiIgc3Ryb2tlPSIjMTYxNjE2IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGQ9Im0xNzcuOSAyMzAuOS00LjgtMy4zaC0yNy43bC00LjggMy4zLTIuNSAyMi4xIDIuMy0yLjJoMzcuN2wyLjUgMi4yeiIvPgogIDxwYXRoIGZpbGw9IiM3NjNkMTYiIHN0cm9rZT0iIzc2M2QxNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJtMjc4LjMgMTE0LjIgOC41LTQwLjgtMTIuNy0zNy45LTk2LjIgNzEuNCAzNyAzMS4zIDUyLjMgMTUuMyAxMS42LTEzLjUtNS0zLjYgOC03LjMtNi4yLTQuOCA4LTYuMXpNMzEuOCA3My40bDguNSA0MC44LTUuNCA0IDggNi4xLTYuMSA0LjggOCA3LjMtNSAzLjYgMTEuNSAxMy41IDUyLjMtMTUuMyAzNy0zMS4zLTk2LjItNzEuNHoiLz4KICA8cGF0aCBkPSJtMjY3LjIgMTUzLjUtNTIuMy0xNS4zIDE1LjkgMjMuOS0yMy43IDQ2IDMxLjItLjRoNDYuNXptLTE2My42LTE1LjMtNTIuMyAxNS4zLTE3LjQgNTQuMmg0Ni40bDMxLjEuNC0yMy42LTQ2em03MSAyNi40IDMuMy01Ny43IDE1LjItNDEuMWgtNjcuNWwxNSA0MS4xIDMuNSA1Ny43IDEuMiAxOC4yLjEgNDQuOGgyNy43bC4yLTQ0Ljh6IiBjbGFzcz0ic3Q2Ii8+Cjwvc3ZnPg==' as const |
| 82 | + |
| 83 | + #accounts: XRPLWalletAccount[] = [] |
| 84 | + |
| 85 | + readonly #listeners: { [E in StandardEventsNames]?: StandardEventsListeners[E][] } = {} |
| 86 | + |
| 87 | + #discoveredProvider?: BaseProvider |
| 88 | + |
| 89 | + constructor(provider?: BaseProvider) { |
| 90 | + this.#discoveredProvider = provider |
| 91 | + } |
| 92 | + |
| 93 | + get version() { |
| 94 | + return '1.0.0' as const |
| 95 | + } |
| 96 | + |
| 97 | + get name() { |
| 98 | + return this.#name |
| 99 | + } |
| 100 | + |
| 101 | + get icon() { |
| 102 | + return this.#icon |
| 103 | + } |
| 104 | + |
| 105 | + get chains() { |
| 106 | + return [XRPL_MAINNET, XRPL_TESTNET, XRPL_DEVNET] |
| 107 | + } |
| 108 | + |
| 109 | + get features(): StandardConnectFeature & |
| 110 | + StandardEventsFeature & |
| 111 | + XRPLSignTransactionFeature & |
| 112 | + XRPLSignAndSubmitTransactionFeature { |
| 113 | + return { |
| 114 | + 'standard:connect': { |
| 115 | + version: '1.0.0', |
| 116 | + connect: this.#connect, |
| 117 | + }, |
| 118 | + 'standard:events': { |
| 119 | + version: '1.0.0', |
| 120 | + on: this.#on, |
| 121 | + }, |
| 122 | + 'xrpl:signTransaction': { |
| 123 | + version: '1.0.0', |
| 124 | + signTransaction: this.#signTransaction, |
| 125 | + }, |
| 126 | + 'xrpl:signAndSubmitTransaction': { |
| 127 | + version: '1.0.0', |
| 128 | + signAndSubmitTransaction: this.#signAndSubmitTransaction, |
| 129 | + }, |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + get accounts() { |
| 134 | + return this.#accounts |
| 135 | + } |
| 136 | + |
| 137 | + get #provider(): BaseProvider { |
| 138 | + return this.#discoveredProvider ?? (window as any).ethereum |
| 139 | + } |
| 140 | + |
| 141 | + #invokeSnapRequest = async (request: { method: string; params?: Record<string, any> }) => { |
| 142 | + return await this.#provider.request({ |
| 143 | + method: 'wallet_invokeSnap', |
| 144 | + params: { |
| 145 | + snapId: SNAP_ORIGIN, |
| 146 | + request, |
| 147 | + }, |
| 148 | + }) |
| 149 | + } |
| 150 | + |
| 151 | + #changeNetworkHandler = async (network: XRPLStandardIdentifier) => { |
| 152 | + const currentNetworkInfo = (await this.#invokeSnapRequest({ |
| 153 | + method: 'xrpl_getActiveNetwork', |
| 154 | + })) as NetworkInfo |
| 155 | + const currentNetworkId = currentNetworkInfo.chainId |
| 156 | + const txNetworkId = Number(network.split(':')[1]) |
| 157 | + |
| 158 | + if (txNetworkId === currentNetworkId) return |
| 159 | + |
| 160 | + const networkList = (await this.#invokeSnapRequest({ |
| 161 | + method: 'xrpl_getStoredNetworks', |
| 162 | + })) as NetworkInfo[] |
| 163 | + const networkInfo = networkList.find((n) => n.chainId === txNetworkId) |
| 164 | + if (!networkInfo) throw new Error('Network not found') |
| 165 | + |
| 166 | + await this.#invokeSnapRequest({ |
| 167 | + method: 'xrpl_changeNetwork', |
| 168 | + params: { |
| 169 | + chainId: networkInfo.chainId, |
| 170 | + }, |
| 171 | + }) |
| 172 | + } |
| 173 | + |
| 174 | + #signTransactionHandler = async (tx_json: BaseTransaction) => { |
| 175 | + return (await this.#invokeSnapRequest({ |
| 176 | + method: 'xrpl_sign', |
| 177 | + params: tx_json, |
| 178 | + })) as { tx_blob: string; hash: string } |
| 179 | + } |
| 180 | + |
| 181 | + #signAndSubmitTransactionHandler = async (tx_json: BaseTransaction) => { |
| 182 | + return (await this.#invokeSnapRequest({ |
| 183 | + method: 'xrpl_signAndSubmit', |
| 184 | + params: tx_json, |
| 185 | + })) as SubmitResponse |
| 186 | + } |
| 187 | + |
| 188 | + #connect: StandardConnectMethod = async ({ silent = false } = {}) => { |
| 189 | + if (silent) { |
| 190 | + const snaps = (await this.#provider.request({ |
| 191 | + method: 'wallet_getSnaps', |
| 192 | + })) as SnapInfo |
| 193 | + if (!snaps[SNAP_ORIGIN]) { |
| 194 | + this.#accounts = [] |
| 195 | + this.#emit('change', { accounts: this.accounts }) |
| 196 | + return { |
| 197 | + accounts: this.#accounts, |
| 198 | + } |
| 199 | + } |
| 200 | + } else { |
| 201 | + await this.#provider.request({ |
| 202 | + method: 'wallet_requestSnaps', |
| 203 | + params: { |
| 204 | + [SNAP_ORIGIN]: {}, |
| 205 | + }, |
| 206 | + }) |
| 207 | + } |
| 208 | + const snapAccount = (await this.#invokeSnapRequest({ |
| 209 | + method: 'xrpl_getAccount', |
| 210 | + })) as MetamaskAccount |
| 211 | + |
| 212 | + this.#accounts = [new XRPLWalletAccount(snapAccount.account)] |
| 213 | + this.#emit('change', { accounts: this.accounts }) |
| 214 | + return { |
| 215 | + accounts: this.#accounts, |
| 216 | + } |
| 217 | + } |
| 218 | + |
| 219 | + #signTransaction: XRPLSignTransactionMethod = async ({ tx_json, network }) => { |
| 220 | + const networkId = this.#convertNetworkToId(network) |
| 221 | + await this.#changeNetworkHandler(networkId) |
| 222 | + const { tx_blob } = await this.#signTransactionHandler(tx_json) |
| 223 | + return { |
| 224 | + signed_tx_blob: tx_blob, |
| 225 | + } |
| 226 | + } |
| 227 | + |
| 228 | + #signAndSubmitTransaction: XRPLSignAndSubmitTransactionMethod = async ({ tx_json, network }) => { |
| 229 | + const networkId = this.#convertNetworkToId(network) |
| 230 | + await this.#changeNetworkHandler(networkId) |
| 231 | + const txResponse = await this.#signAndSubmitTransactionHandler(tx_json) |
| 232 | + return { |
| 233 | + tx_json: txResponse.result.tx_json as any, |
| 234 | + tx_hash: txResponse.result.tx_json.hash!, |
| 235 | + } |
| 236 | + } |
| 237 | + |
| 238 | + #on: StandardEventsOnMethod = (event, listener) => { |
| 239 | + if (this.#listeners[event]) this.#listeners[event]?.push(listener) |
| 240 | + else this.#listeners[event] = [listener] |
| 241 | + return (): void => this.#off(event, listener) |
| 242 | + } |
| 243 | + |
| 244 | + #emit<E extends StandardEventsNames>(event: E, ...args: Parameters<StandardEventsListeners[E]>): void { |
| 245 | + this.#listeners[event]?.forEach((listener) => listener.apply(null, args)) |
| 246 | + } |
| 247 | + |
| 248 | + #off<E extends StandardEventsNames>(event: E, listener: StandardEventsListeners[E]): void { |
| 249 | + this.#listeners[event] = this.#listeners[event]?.filter((existingListener) => listener !== existingListener) |
| 250 | + } |
| 251 | + |
| 252 | + #convertNetworkToId = (network: XRPLIdentifierString): XRPLStandardIdentifier => { |
| 253 | + const networkId = network.split(':')[1] |
| 254 | + switch (networkId) { |
| 255 | + case 'mainnet': |
| 256 | + return 'xrpl:0' |
| 257 | + case 'testnet': |
| 258 | + return 'xrpl:1' |
| 259 | + case 'devnet': |
| 260 | + return 'xrpl:2' |
| 261 | + case 'xahau-mainnet': |
| 262 | + return 'xrpl:21337' |
| 263 | + case 'xahau-testnet': |
| 264 | + return 'xrpl:21338' |
| 265 | + default: |
| 266 | + return network as XRPLStandardIdentifier |
| 267 | + } |
| 268 | + } |
| 269 | +} |
0 commit comments