UNPKG

@windoge98/pnp-okx

Version:

OKX multi-chain wallet adapter for Plug-n-Play

377 lines (376 loc) 14.9 kB
import { BaseMultiChainAdapter, BaseSiwxAdapter, Adapter, deriveAccountId, formatSiwsMessage, createAdapterExtension } from "@windoge98/plug-n-play"; import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; import { AnonymousIdentity } from "@dfinity/agent"; import { Ed25519KeyIdentity } from "@dfinity/identity"; import bs58 from "bs58"; const idlFactory = ({ IDL }) => { const RuntimeFeature = IDL.Variant({ "IncludeUriInSeed": IDL.Null, "DisablePrincipalToSolMapping": IDL.Null, "DisableSolToPrincipalMapping": IDL.Null }); IDL.Record({ "uri": IDL.Text, "runtime_features": IDL.Opt(IDL.Vec(RuntimeFeature)), "domain": IDL.Text, "statement": IDL.Opt(IDL.Text), "scheme": IDL.Opt(IDL.Text), "salt": IDL.Text, "session_expires_in": IDL.Opt(IDL.Nat64), "targets": IDL.Opt(IDL.Vec(IDL.Text)), "chain_id": IDL.Opt(IDL.Text), "sign_in_expires_in": IDL.Opt(IDL.Nat64) }); const Principal = IDL.Vec(IDL.Nat8); const Address = IDL.Text; const GetAddressResponse = IDL.Variant({ "Ok": Address, "Err": IDL.Text }); const GetPrincipalResponse = IDL.Variant({ "Ok": Principal, "Err": IDL.Text }); const PublicKey = IDL.Vec(IDL.Nat8); const SessionKey = PublicKey; const Timestamp = IDL.Nat64; const Delegation = IDL.Record({ "pubkey": PublicKey, "targets": IDL.Opt(IDL.Vec(IDL.Principal)), "expiration": Timestamp }); const SignedDelegation = IDL.Record({ "signature": IDL.Vec(IDL.Nat8), "delegation": Delegation }); const GetDelegationResponse = IDL.Variant({ "Ok": SignedDelegation, "Err": IDL.Text }); const SiwsSignature = IDL.Text; const Nonce = IDL.Text; const CanisterPublicKey = PublicKey; const LoginDetails = IDL.Record({ "user_canister_pubkey": CanisterPublicKey, "expiration": Timestamp }); const LoginResponse = IDL.Variant({ "Ok": LoginDetails, "Err": IDL.Text }); const SiwsMessage = IDL.Record({ "uri": IDL.Text, "issued_at": IDL.Nat64, "domain": IDL.Text, "statement": IDL.Text, "version": IDL.Nat32, "chain_id": IDL.Text, "address": Address, "nonce": IDL.Text, "expiration_time": IDL.Nat64 }); const PrepareLoginResponse = IDL.Variant({ "Ok": SiwsMessage, "Err": IDL.Text }); return IDL.Service({ "get_address": IDL.Func([Principal], [GetAddressResponse], ["query"]), "get_caller_address": IDL.Func([], [GetAddressResponse], ["query"]), "get_principal": IDL.Func([Address], [GetPrincipalResponse], ["query"]), "siws_get_delegation": IDL.Func( [Address, SessionKey, Timestamp], [GetDelegationResponse], ["query"] ), "siws_login": IDL.Func( [SiwsSignature, Address, SessionKey, Nonce], [LoginResponse], [] ), "siws_prepare_login": IDL.Func([Address], [PrepareLoginResponse], []) }); }; class OkxSolanaNetworkAdapter extends BaseSiwxAdapter { constructor(args) { super({ ...args, adapter: { ...args.adapter, id: "okxSolana", walletName: "OKX Wallet (Solana)" } }); this.solanaAddress = null; this.identity = null; this.sessionKey = null; } async connect() { const win = window; if (!win.okxwallet?.solana) { throw new Error("OKX Wallet Solana provider not available"); } try { const response = await win.okxwallet.solana.connect(); const address = response.publicKey.toBase58(); this.solanaAddress = address; const { identity, sessionKey } = await this.performSiwsLogin(address, win.okxwallet.solana); this.identity = identity; this.sessionKey = sessionKey; const principal = identity.getPrincipal(); this.setState(Adapter.Status.CONNECTED); return { owner: principal.toText(), subaccount: deriveAccountId(principal) }; } catch (error) { this.setState(Adapter.Status.ERROR); throw error; } } async performSiwsLogin(address, provider) { const actor = await this.createSiwsProviderActor(); const prepareResult = await actor.siws_prepare_login(address); if ("Err" in prepareResult) { throw new Error(`SIWS Prepare Login failed: ${prepareResult.Err}`); } const messageText = formatSiwsMessage(prepareResult.Ok); const messageBytes = new TextEncoder().encode(messageText); let signatureBytes; try { if (typeof provider.signMessage === "function") { const result = await provider.signMessage(messageBytes, "utf8"); if (result?.signature) { signatureBytes = typeof result.signature === "string" ? bs58.decode(result.signature) : new Uint8Array(result.signature); } else if (result instanceof Uint8Array) { signatureBytes = result; } else if (Array.isArray(result)) { signatureBytes = new Uint8Array(result); } else { throw new Error("Unexpected signature format from OKX wallet"); } } else if (typeof provider.sign === "function") { const result = await provider.sign(messageBytes, "utf8"); signatureBytes = typeof result === "string" ? bs58.decode(result) : new Uint8Array(result); } else if (typeof provider.request === "function") { const result = await provider.request({ method: "signMessage", params: [bs58.encode(messageBytes), "utf8"] }); signatureBytes = typeof result === "string" ? bs58.decode(result) : new Uint8Array(result); } else { throw new Error("OKX wallet provider does not support any known message signing method"); } } catch (error) { throw new Error(`Failed to sign message with OKX wallet: ${error}`); } const signature = bs58.encode(signatureBytes); const sessionIdentity = Ed25519KeyIdentity.generate(); const sessionPublicKeyDer = sessionIdentity.getPublicKey().toDer(); const loginResult = await actor.siws_login( signature, address, new Uint8Array(sessionPublicKeyDer), prepareResult.Ok.nonce ); if ("Err" in loginResult) { throw new Error(`SIWS Login failed: ${loginResult.Err}`); } const delegationResult = await actor.siws_get_delegation( address, new Uint8Array(sessionPublicKeyDer), loginResult.Ok.expiration ); if ("Err" in delegationResult) { throw new Error(`SIWS Get Delegation failed: ${delegationResult.Err}`); } const identity = this.createDelegationIdentity( delegationResult.Ok, sessionIdentity, new Uint8Array(loginResult.Ok.user_canister_pubkey).buffer ); return { identity, sessionKey: sessionIdentity }; } async createSiwsProviderActor(identity) { const id = identity ?? new AnonymousIdentity(); return this.createProviderActor(idlFactory, id); } async isConnected() { return this.identity !== null && !this.identity.getPrincipal().isAnonymous(); } async getPrincipal() { if (!this.identity) { throw new Error("Not connected"); } return this.identity.getPrincipal().toText(); } async getAccountId() { if (!this.identity) { throw new Error("Not connected"); } const principal = this.identity.getPrincipal(); return deriveAccountId(principal); } async getAddresses() { const principal = this.identity?.getPrincipal(); return { sol: { address: this.solanaAddress, network: "mainnet" }, icp: { address: principal?.toText(), subaccount: principal ? deriveAccountId(principal) : void 0 } }; } async disconnectInternal() { const win = window; if (win.okxwallet?.solana) { try { await win.okxwallet.solana.disconnect(); } catch { } } this.solanaAddress = null; await super.disconnectInternal(); } createActorInternal(canisterId, idl, options) { const requiresSigning = options?.requiresSigning ?? true; if (requiresSigning && !this.identity) { throw new Error("Cannot create signed actor: Not connected"); } const agent = this.buildHttpAgentSync({ identity: this.identity ?? void 0 }); return this.createActorWithAgent(agent, canisterId, idl); } async onStorageRestored(_sessionKey, _delegationChain) { } async onClearStoredSession() { this.solanaAddress = null; } } class OkxMultiChainAdapter extends BaseMultiChainAdapter { constructor(args) { super({ ...args, config: { ...args.config, supportedNetworks: args.config.supportedNetworks || ["solana"] } }); this.networkAdapters = /* @__PURE__ */ new Map(); } /** * Override connect to add OKX-specific error handling */ async connect() { try { const result = await super.connect(); return result; } catch (error) { if (error?.message?.includes("SIWS provider")) { const enhancedError = new Error( `OKX Wallet connection failed: Please ensure siwsProviderCanisterId is configured in your PNP config` ); throw enhancedError; } throw error; } } /** * Detect which network OKX wallet is currently connected to */ async detectNetwork() { const win = window; if (!win.okxwallet) { throw new Error("OKX Wallet is not installed. Please install OKX Wallet browser extension."); } if (!win.okxwallet.solana) { throw new Error("OKX Wallet Solana provider not available. Please ensure OKX Wallet is properly installed."); } return { network: "solana", isTestnet: false }; } /** * Get or create the appropriate network-specific adapter */ async getNetworkAdapter(network) { const cached = this.networkAdapters.get(network.network); if (cached) { return cached; } if (network.network === "solana") { const adapter = new OkxSolanaNetworkAdapter({ adapter: { ...this.adapter, chain: "SOL" }, config: { ...this.config, solanaNetwork: this.config?.solanaNetwork || WalletAdapterNetwork.Mainnet, siwsProviderCanisterId: this.config?.siwsProviderCanisterId, providerCanisterId: this.config?.siwsProviderCanisterId }, logger: this.logger }); this.networkAdapters.set(network.network, adapter); return adapter; } throw new Error(`OKX wallet adapter only supports Solana network. Current network: ${network.network}`); } /** * Switch network (OKX supports programmatic network switching for EVM chains) */ async switchNetwork(targetNetwork) { const win = window; if (!win.okxwallet) { throw new Error("OKX Wallet not available"); } if (["ethereum", "bsc", "polygon", "optimism", "arbitrum", "avalanche", "fantom"].includes(targetNetwork)) { const chainIdMap = { "ethereum": "0x1", "bsc": "0x38", "polygon": "0x89", "optimism": "0xa", "arbitrum": "0xa4b1", "avalanche": "0xa86a", "fantom": "0xfa" }; const targetChainId = chainIdMap[targetNetwork]; if (!targetChainId) { throw new Error(`Unknown chain ID for network: ${targetNetwork}`); } try { await win.okxwallet.request({ method: "wallet_switchEthereumChain", params: [{ chainId: targetChainId }] }); } catch (error) { if (error.code === 4902) { throw new Error(`Please add ${targetNetwork} network to OKX Wallet manually`); } throw error; } } else { throw new Error(`Network switching to ${targetNetwork} must be done manually in OKX Wallet`); } } } const okxLogo = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiByeD0iOCIgZmlsbD0iYmxhY2siLz4KPHBhdGggZD0iTTI0LjA0MDkgMTQuNzAxOUgyOC44OTI1QzI5LjE3NzEgMTQuNzAxOSAyOS40MDc1IDE0LjkzMjMgMjkuNDA3NSAxNS4yMTY5VjIwLjA2ODRDMjkuNDA3NSAyMC4zNTMgMjkuMTc3MSAyMC41ODM0IDI4Ljg5MjUgMjAuNTgzNEgyNC4wNDA5QzIzLjc1NjMgMjAuNTgzNCAyMy41MjU5IDIwLjM1MyAyMy41MjU5IDIwLjA2ODRWMTUuMjE2OUMyMy41MjU5IDE0LjkzMjMgMjMuNzU2MyAxNC43MDE5IDI0LjA0MDkgMTQuNzAxOVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMC4xMDcxIDE0LjcwMTlIMTQuOTU4N0MxNS4yNDMzIDE0LjcwMTkgMTUuNDczNyAxNC45MzIzIDE1LjQ3MzcgMTUuMjE2OVYyMC4wNjg0QzE1LjQ3MzcgMjAuMzUzIDE1LjI0MzMgMjAuNTgzNCAxNC45NTg3IDIwLjU4MzRIMTAuMTA3MUM5LjgyMjUgMjAuNTgzNCA5LjU5MjEgMjAuMzUzIDkuNTkyMSAyMC4wNjg0VjE1LjIxNjlDOS41OTIxIDE0LjkzMjMgOS44MjI1IDE0LjcwMTkgMTAuMTA3MSAxNC43MDE5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTI0LjA0MDkgNS41SDI4Ljg5MjVDMjkuMTc3MSA1LjUgMjkuNDA3NSA1LjczMDQgMjkuNDA3NSA2LjAxNVYxMC44NjY1QzI5LjQwNzUgMTEuMTUxMSAyOS4xNzcxIDExLjM4MTUgMjguODkyNSAxMS4zODE1SDI0LjA0MDlDMjMuNzU2MyAxMS4zODE1IDIzLjUyNTkgMTEuMTUxMSAyMy41MjU5IDEwLjg2NjVWNi4wMTVDMjMuNTI1OSA1LjczMDQgMjMuNzU2MyA1LjUgMjQuMDQwOSA1LjVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAuMTA3MSA1LjVIMTQuOTU4N0MxNS4yNDMzIDUuNSAxNS40NzM3IDUuNzMwNCAxNS40NzM3IDYuMDE1VjEwLjg2NjVDMTUuNDczNyAxMS4xNTExIDE1LjI0MzMgMTEuMzgxNSAxNC45NTg3IDExLjM4MTVIMTAuMTA3MUM5LjgyMjUgMTEuMzgxNSA5LjU5MjEgMTEuMTUxMSA5LjU5MjEgMTAuODY2NVY2LjAxNUM5LjU5MjEgNS43MzA0IDkuODIyNSA1LjUgMTAuMTA3MSA1LjVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjQuMDQwOSAyMy45MDM3SDI4Ljg5MjVDMjkuMTc3MSAyMy45MDM3IDI5LjQwNzUgMjQuMTM0MSAyOS40MDc1IDI0LjQxODdWMjkuMjcwM0MyOS40MDc1IDI5LjU1NDkgMjkuMTc3MSAyOS43ODUzIDI4Ljg5MjUgMjkuNzg1M0gyNC4wNDA5QzIzLjc1NjMgMjkuNzg1MyAyMy41MjU5IDI5LjU1NDkgMjMuNTI1OSAyOS4yNzAzVjI0LjQxODdDMjMuNTI1OSAyNC4xMzQxIDIzLjc1NjMgMjMuOTAzNyAyNC4wNDA5IDIzLjkwMzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAuMTA3MSAyMy45MDM3SDE0Ljk1ODdDMTUuMjQzMyAyMy45MDM3IDE1LjQ3MzcgMjQuMTM0MSAxNS40NzM3IDI0LjQxODdWMjkuMjcwM0MxNS40NzM3IDI5LjU1NDkgMTUuMjQzMyAyOS43ODUzIDE0Ljk1ODcgMjkuNzg1M0gxMC4xMDcxQzkuODIyNSAyOS43ODUzIDkuNTkyMSAyOS41NTQ5IDkuNTkyMSAyOS4yNzAzVjI0LjQxODdDOS41OTIxIDI0LjEzNDEgOS44MjI1IDIzLjkwMzcgMTAuMTA3MSAyMy45MDM3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE3LjA3NCAxNC43MDE5SDIxLjkyNTVDMjIuMjEwMSAxNC43MDE5IDIyLjQ0MDUgMTQuOTMyMyAyMi40NDA1IDE1LjIxNjlWMjAuMDY4NEMyMi40NDA1IDIwLjM1MyAyMi4yMTAxIDIwLjU4MzQgMjEuOTI1NSAyMC41ODM0SDE3LjA3NEMxNi43ODk0IDIwLjU4MzQgMTYuNTU5IDIwLjM1MyAxNi41NTkgMjAuMDY4NFYxNS4yMTY5QzE2LjU1OSAxNC45MzIzIDE2Ljc4OTQgMTQuNzAxOSAxNy4wNzQgMTQuNzAxOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg=="; const OkxExtension = createAdapterExtension({ okx: { id: "okx", enabled: false, walletName: "OKX Wallet", logo: okxLogo, website: "https://www.okx.com/web3", chain: "SOL", adapter: OkxMultiChainAdapter, config: { enabled: false, supportedNetworks: ["solana"], solanaNetwork: WalletAdapterNetwork.Mainnet, // Provider canister ID should be set by the user siwsProviderCanisterId: "" } } }); export { OkxExtension, OkxMultiChainAdapter };