diff --git a/src/components/SolanaWallets/OKXAdapter.tsx b/src/components/SolanaWallets/OKXAdapter.tsx new file mode 100644 index 000000000..7e2bab1f5 --- /dev/null +++ b/src/components/SolanaWallets/OKXAdapter.tsx @@ -0,0 +1,296 @@ +import type { EventEmitter, SendTransactionOptions, WalletName } from '@solana/wallet-adapter-base' +import { + BaseMessageSignerWalletAdapter, + isVersionedTransaction, + scopePollingDetectionStrategy, + WalletAccountError, + WalletConnectionError, + WalletDisconnectedError, + WalletDisconnectionError, + WalletError, + WalletNotConnectedError, + WalletNotReadyError, + WalletPublicKeyError, + WalletReadyState, + WalletSendTransactionError, + WalletSignMessageError, + WalletSignTransactionError +} from '@solana/wallet-adapter-base' +import type { + Connection, + SendOptions, + Transaction, + TransactionSignature, + TransactionVersion, + VersionedTransaction +} from '@solana/web3.js' +import { PublicKey } from '@solana/web3.js' + +interface OKXWalletEvents { + connect(...args: unknown[]): unknown + disconnect(...args: unknown[]): unknown + accountChanged(newPublicKey: PublicKey): unknown +} + +interface OKXWallet extends EventEmitter { + isOKX?: boolean + publicKey?: { toBytes(): Uint8Array } + isConnected: boolean + signTransaction(transaction: T): Promise + signAllTransactions(transactions: T[]): Promise + signAndSendTransaction( + transaction: T, + options?: SendOptions + ): Promise<{ signature: TransactionSignature }> + signMessage(message: Uint8Array): Promise<{ signature: Uint8Array }> + connect(): Promise + disconnect(): Promise +} + +interface OKXWindow extends Window { + okxwallet?: { + isOkxWallet: boolean + solana?: OKXWallet + } +} + +declare const window: OKXWindow + +export const OKXWalletName = 'OKX Wallet' as WalletName<'OKX Wallet'> + +export class OKXWalletAdapter extends BaseMessageSignerWalletAdapter { + name = OKXWalletName + url = 'https://www.okx.com/web3' + icon = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJDSURBVHgB7Zq9jtpAEMfHlhEgQLiioXEkoAGECwoKxMcTRHmC5E3IoyRPkPAEkI7unJYmTgEFTYwA8a3NTKScLnCHN6c9r1e3P2llWQy7M/s1Gv1twCP0ej37dDq9x+Zut1t3t9vZjDEHIiSRSPg4ZpDL5fxkMvn1cDh8m0wmfugfO53OoFQq/crn8wxfY9EymQyrVCqMfHvScZx1p9ls3pFxXBy/bKlUipGPrVbLuQqAfsCliq3zl0H84zwtjQrOw4Mt1W63P5LvBm2d+Xz+YzqdgkqUy+WgWCy+Mc/nc282m4FqLBYL+3g8fjDxenq72WxANZbLJeA13zDX67UDioL5ybXwafMYu64Ltn3bdDweQ5R97fd7GyhBQMipx4POeEDHIu2LfDdBIGGz+hJ9CQ1ABjoA2egAZPM6AgiCAEQhsi/C4jHyPA/6/f5NG3Ks2+3CYDC4aTccDrn6ojG54MnEvG00GoVmWLIRNZ7wTCwDHYBsdACy0QHIhiuRETxlICWpMMhGZHmqS8qH6JLyGegAZKMDkI0uKf8X4SWlaZo+Pp1bRrwlJU8ZKLIvUjKh0WiQ3sRUbNVq9c5Ebew7KEo2m/1p4jJ4qAmDaqDQBzj5XyiAT4VCQezJigAU+IDU+z8vJFnGWeC+bKQV/5VZ71FV6L7PA3gg3tXrdQ+DgLhC+75Wq3no69P3MC0NFQpx2lL04Ql9gHK1bRDjsSBIvScBnDTk1WrlGIZBorIDEYJj+rhdgnQ67VmWRe0zlplXl81vcyEt0rSoYDUAAAAASUVORK5CYII=' + supportedTransactionVersions: ReadonlySet = new Set(['legacy', 0]) + + private _connecting: boolean + private _wallet: OKXWallet | null + private _publicKey: PublicKey | null + private _readyState: WalletReadyState = + typeof window === 'undefined' || typeof document === 'undefined' + ? WalletReadyState.Unsupported + : WalletReadyState.NotDetected + + constructor() { + super() + this._connecting = false + this._wallet = null + this._publicKey = null + + if (this._readyState !== WalletReadyState.Unsupported) { + scopePollingDetectionStrategy(() => { + if (window.okxwallet && window.okxwallet.isOkxWallet && window.okxwallet.solana) { + this._readyState = WalletReadyState.Installed + this.emit('readyStateChange', this._readyState) + return true + } + return false + }) + } + } + + get publicKey() { + return this._publicKey + } + + get connecting() { + return this._connecting + } + + get readyState() { + return this._readyState + } + + async autoConnect(): Promise { + // Skip autoconnect in the Loadable state + // We can't redirect to a universal link without user input + if (this.readyState === WalletReadyState.Installed) { + await this.connect() + } + } + + async connect(): Promise { + try { + if (this.connected || this.connecting) return + + if (this.readyState !== WalletReadyState.Installed) throw new WalletNotReadyError() + + this._connecting = true + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const wallet = window.okxwallet!.solana! + + if (!wallet.isConnected) { + try { + await wallet.connect() + } catch (error: any) { + throw new WalletConnectionError(error?.message, error) + } + } + + if (!wallet.publicKey) throw new WalletAccountError() + + let publicKey: PublicKey + try { + publicKey = new PublicKey(wallet.publicKey.toBytes()) + } catch (error: any) { + throw new WalletPublicKeyError(error?.message, error) + } + + wallet.on('disconnect', this._disconnected) + wallet.on('accountChanged', this._accountChanged) + + this._wallet = wallet + this._publicKey = publicKey + + this.emit('connect', publicKey) + } catch (error: any) { + this.emit('error', error) + throw error + } finally { + this._connecting = false + } + } + + async disconnect(): Promise { + const wallet = this._wallet + if (wallet) { + try { + wallet.off('disconnect', this._disconnected) + wallet.off('accountChanged', this._accountChanged) + } catch { + // emptry + } + + this._wallet = null + this._publicKey = null + + try { + await wallet.disconnect() + } catch (error: any) { + this.emit('error', new WalletDisconnectionError(error?.message, error)) + } + } + + this.emit('disconnect') + } + + async sendTransaction( + transaction: T, + connection: Connection, + options: SendTransactionOptions = {} + ): Promise { + try { + const wallet = this._wallet + if (!wallet) throw new WalletNotConnectedError() + + try { + const { signers, ...sendOptions } = options + + if (isVersionedTransaction(transaction)) { + signers?.length && transaction.sign(signers) + } else { + transaction = (await this.prepareTransaction(transaction, connection, sendOptions)) as T + signers?.length && (transaction as Transaction).partialSign(...signers) + } + + sendOptions.preflightCommitment = sendOptions.preflightCommitment || connection.commitment + + const { signature } = await wallet.signAndSendTransaction(transaction, sendOptions) + return signature + } catch (error: any) { + if (error instanceof WalletError) throw error + throw new WalletSendTransactionError(error?.message, error) + } + } catch (error: any) { + this.emit('error', error) + throw error + } + } + + async signTransaction(transaction: T): Promise { + try { + const wallet = this._wallet + if (!wallet) throw new WalletNotConnectedError() + + try { + return (await wallet.signTransaction(transaction)) || transaction + } catch (error: any) { + throw new WalletSignTransactionError(error?.message, error) + } + } catch (error: any) { + this.emit('error', error) + throw error + } + } + + async signAllTransactions(transactions: T[]): Promise { + try { + const wallet = this._wallet + if (!wallet) throw new WalletNotConnectedError() + + try { + return (await wallet.signAllTransactions(transactions)) || transactions + } catch (error: any) { + throw new WalletSignTransactionError(error?.message, error) + } + } catch (error: any) { + this.emit('error', error) + throw error + } + } + + async signMessage(message: Uint8Array): Promise { + try { + const wallet = this._wallet + if (!wallet) throw new WalletNotConnectedError() + + try { + const { signature } = await wallet.signMessage(message) + return signature + } catch (error: any) { + throw new WalletSignMessageError(error?.message, error) + } + } catch (error: any) { + this.emit('error', error) + throw error + } + } + + private _disconnected = () => { + const wallet = this._wallet + if (wallet) { + try { + wallet.off('disconnect', this._disconnected) + wallet.off('accountChanged', this._accountChanged) + } catch { + // emptry + } + + this._wallet = null + this._publicKey = null + + this.emit('error', new WalletDisconnectedError()) + this.emit('disconnect') + } + } + + private _accountChanged = (newPublicKey: PublicKey) => { + const publicKey = this._publicKey + if (!publicKey) return + + try { + newPublicKey = new PublicKey(newPublicKey.toBytes()) + } catch (error: any) { + this.emit('error', new WalletPublicKeyError(error?.message, error)) + return + } + + if (publicKey.equals(newPublicKey)) return + + this._publicKey = newPublicKey + this.emit('connect', newPublicKey) + } +} diff --git a/src/components/SolanaWallets/SolanaWallets.tsx b/src/components/SolanaWallets/SolanaWallets.tsx index 0f454a1bb..986e7a3a6 100644 --- a/src/components/SolanaWallets/SolanaWallets.tsx +++ b/src/components/SolanaWallets/SolanaWallets.tsx @@ -30,6 +30,7 @@ import { } from '@solana/wallet-adapter-wallets' import SquadsEmbeddedWalletAdapter, { detectEmbeddedInSquadsIframe } from './SquadsMultisig' import { clusterApiUrl } from '@solana/web3.js' +import { OKXWalletAdapter } from './OKXAdapter' import useAppSettings from '@/application/common/useAppSettings' import useConnection from '@/application/connection/useConnection' @@ -47,6 +48,7 @@ export function SolanaWalletProviders({ children }: { children?: ReactNode }) { const wallets = useMemo( () => [ new PhantomWalletAdapter(), + new OKXWalletAdapter(), new TrustWalletAdapter(), new SolflareWalletAdapter(), new SolletWalletAdapter(),