UNPKG

@windoge98/pnp-solflare

Version:

Solflare Wallet adapter for PNP (Plug N Play)

308 lines (307 loc) 12.6 kB
import { BaseSiwxAdapter, Adapter, deriveAccountId, formatSiwsMessage, createAdapterExtension } from "@windoge98/plug-n-play"; import { formatSiwsMessage as formatSiwsMessage2 } from "@windoge98/plug-n-play"; import { AnonymousIdentity } from "@dfinity/agent"; import { SolflareWalletAdapter } from "@solana/wallet-adapter-solflare"; import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; import { Connection } from "@solana/web3.js"; 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 SolflareAdapter extends BaseSiwxAdapter { constructor(args) { super(args); this.id = "solflare"; this.solanaAdapter = null; this.solanaAddress = null; this.connectingPromise = null; this.handleSolanaConnect = (_publicKey) => { this.logger.debug("Solflare connected to Solana"); }; this.handleSolanaDisconnect = () => { this.logger.debug("Solflare disconnected from Solana"); this.setState(Adapter.Status.DISCONNECTED); }; this.handleSolanaError = (error) => { this.logger.error("Solflare error:", error); this.setState(Adapter.Status.ERROR); }; this.solanaConnection = this.initializeConnection(); this.initializeSolanaAdapter(); this.setState(Adapter.Status.READY); } initializeConnection() { const network = this.config.solanaNetwork || WalletAdapterNetwork.Mainnet; const endpoint = this.config.rpcEndpoint || (network === WalletAdapterNetwork.Mainnet ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com"); return new Connection(endpoint); } initializeSolanaAdapter() { const network = this.config.solanaNetwork || WalletAdapterNetwork.Mainnet; this.solanaAdapter = new SolflareWalletAdapter({ network }); this.setupWalletListeners(); } setupWalletListeners() { if (!this.solanaAdapter) return; this.solanaAdapter.on("connect", this.handleSolanaConnect); this.solanaAdapter.on("disconnect", this.handleSolanaDisconnect); this.solanaAdapter.on("error", this.handleSolanaError); } removeWalletListeners() { if (!this.solanaAdapter) return; this.solanaAdapter.off("connect", this.handleSolanaConnect); this.solanaAdapter.off("disconnect", this.handleSolanaDisconnect); this.solanaAdapter.off("error", this.handleSolanaError); } async connect() { if (this.connectingPromise) { return this.connectingPromise; } this.connectingPromise = this.performConnect(); try { const result = await this.connectingPromise; return result; } finally { this.connectingPromise = null; } } async performConnect() { this.setState(Adapter.Status.CONNECTING); try { if (!this.solanaAdapter) { throw new Error("Solflare adapter not initialized"); } await this.solanaAdapter.connect(); if (!this.solanaAdapter.publicKey) { throw new Error("Failed to get public key from Solflare"); } const address = this.solanaAdapter.publicKey.toBase58(); this.solanaAddress = address; const { identity, sessionKey } = await this.performSiwsLogin(address, this.solanaAdapter); this.identity = identity; this.sessionKey = sessionKey; await this.storage.set(`${this.id}-solana-address`, address); const principal = identity.getPrincipal(); this.setState(Adapter.Status.CONNECTED); return { owner: principal.toText(), subaccount: deriveAccountId(principal) }; } catch (error) { this.setState(Adapter.Status.ERROR); this.logger.error("Failed to connect Solflare:", error); throw error; } } async performSiwsLogin(address, adapter) { 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); if (!adapter.signMessage) { throw new Error("Solflare wallet does not support message signing"); } const signatureBytes = await adapter.signMessage(messageBytes); 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() && this.solanaAdapter?.connected === true; } 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: this.config.solanaNetwork === WalletAdapterNetwork.Devnet ? "devnet" : "mainnet" }, icp: { address: principal?.toText(), subaccount: principal ? deriveAccountId(principal) : void 0 } }; } async disconnectInternal() { if (this.solanaAdapter) { try { await this.solanaAdapter.disconnect(); } catch (error) { this.logger.warn("Error disconnecting Solflare:", error); } } this.solanaAddress = null; await this.storage.remove(`${this.id}-solana-address`); 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) { const storedSolanaAddress = await this.storage.get(`${this.id}-solana-address`); if (storedSolanaAddress && typeof storedSolanaAddress === "string") { this.solanaAddress = storedSolanaAddress; } } async onClearStoredSession() { this.solanaAddress = null; await this.storage.remove(`${this.id}-solana-address`); } destroy() { this.removeWalletListeners(); if (this.solanaAdapter) { this.solanaAdapter.disconnect().catch(() => { }); this.solanaAdapter = null; } } } const solflareLogo = "data:image/svg+xml,%3csvg%20width='290'%20height='290'%20viewBox='0%200%20290%20290'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20clip-path='url(%23clip0_146_299)'%3e%3cpath%20d='M63.2951%201H226.705C261.11%201%20289%2028.8905%20289%2063.2951V226.705C289%20261.11%20261.11%20289%20226.705%20289H63.2951C28.8905%20289%201%20261.11%201%20226.705V63.2951C1%2028.8905%2028.8905%201%2063.2951%201Z'%20fill='%23FFEF46'%20stroke='%23EEDA0F'%20stroke-width='2'/%3e%3cpath%20d='M140.548%20153.231L154.832%20139.432L181.462%20148.147C198.893%20153.958%20207.609%20164.61%20207.609%20179.62C207.609%20190.999%20203.251%20198.504%20194.536%20208.188L191.873%20211.093L192.841%20204.314C196.714%20179.62%20189.452%20168.968%20165.484%20161.22L140.548%20153.231ZM104.717%2068.739L177.347%2092.9488L161.61%20107.959L123.843%2095.3698C110.77%2091.012%20106.412%2083.9911%20104.717%2069.2232V68.739ZM100.359%20191.725L116.822%20175.988L147.811%20186.157C164.031%20191.483%20169.599%20198.504%20167.905%20216.177L100.359%20191.725ZM79.539%20121.516C79.539%20116.917%2081.9599%20112.559%2086.0756%20108.927C90.4334%20115.222%2097.9384%20120.79%20109.801%20124.664L135.464%20133.137L121.18%20146.937L96.0016%20138.705C84.3809%20134.832%2079.539%20129.021%2079.539%20121.516ZM155.558%20248.618C208.819%20213.272%20237.387%20189.304%20237.387%20159.768C237.387%20140.158%20225.766%20129.263%20200.104%20120.79L180.736%20114.253L233.756%2063.4128L223.103%2052.0342L207.367%2065.8337L133.043%2041.3818C110.043%2048.8869%2080.9916%2070.9178%2080.9916%2092.9487C80.9916%2095.3697%2081.2337%2097.7907%2081.96%20100.454C62.8342%20111.348%2055.0871%20121.516%2055.0871%20134.105C55.0871%20145.968%2061.3816%20157.831%2081.4758%20164.368L97.4542%20169.694L42.2559%20222.713L52.9082%20234.092L70.0972%20218.356L155.558%20248.618Z'%20fill='%2302050A'/%3e%3c/g%3e%3cdefs%3e%3cclipPath%20id='clip0_146_299'%3e%3crect%20width='290'%20height='290'%20fill='white'/%3e%3c/clipPath%3e%3c/defs%3e%3c/svg%3e"; const SolflareExtension = createAdapterExtension({ solflare: { id: "solflare", enabled: false, walletName: "Solflare", logo: solflareLogo, website: "https://solflare.com", chain: "SOL", adapter: SolflareAdapter, config: { enabled: false, solanaNetwork: WalletAdapterNetwork.Mainnet, // Provider canister ID should be set by the user siwsProviderCanisterId: "" } } }); export { SolflareAdapter, SolflareExtension, formatSiwsMessage2 as formatSiwsMessage };