UNPKG

sui-svelte-wallet-kit

Version:

Svelte 5 wallet kit for Sui: connect wallets, manage accounts, SuiNS, balance, sign transactions/messages

433 lines (432 loc) 15.6 kB
import { registerEnokiWallets } from '@mysten/enoki'; import { getWallets } from '@wallet-standard/core'; import { PasskeyService } from '../../PasskeyService/index.js'; import { PasskeyWalletAdapter } from '../../adapters/PasskeyWalletAdapter.js'; import { MultisigWalletAdapter } from '../../adapters/MultisigWalletAdapter.js'; import { multisigStore, initializeMultisig } from '../../MultisigService/index.js'; import { Ed25519PublicKey } from '@mysten/sui/keypairs/ed25519'; import { Secp256k1PublicKey } from '@mysten/sui/keypairs/secp256k1'; import { Secp256r1PublicKey } from '@mysten/sui/keypairs/secp256r1'; import { PasskeyPublicKey } from '@mysten/sui/keypairs/passkey'; import { fromBase64, toBase64 } from '@mysten/sui/utils'; import { getPreferredRedirectUrlForOAuth, getConfiguredNetwork, isBrowser } from './core.js'; // NOTE: // Svelte 5 does not allow exporting `$state` from a module if it is reassigned. // This file therefore keeps any module-level state as plain variables (not `$state`), // and UI/component state should live in `SuiModule.svelte`. // Validation helpers export const isSuspiciousEnokiConfig = (apiKey, googleId) => { try { const suspiciousApiKey = typeof apiKey === 'string' && apiKey.length < 16; const suspiciousGoogleId = typeof googleId === 'string' && !googleId.endsWith('.apps.googleusercontent.com'); return !!(suspiciousApiKey || suspiciousGoogleId); } catch { return false; } }; export const isMultisigConfig = (config) => { return config !== null && 'mode' in config; }; export const isLegacyMultisigConfig = (config) => { return config !== null && 'signers' in config && !('mode' in config); }; // Enoki registration callback export const registerEnokiWalletsCallback = (client, network, zkLoginGoogle) => { try { const googleProviderOptions = { clientId: zkLoginGoogle.googleClientId }; try { const ru = getPreferredRedirectUrlForOAuth(zkLoginGoogle); if (ru) googleProviderOptions.redirectUrl = ru; } catch { } registerEnokiWallets({ client, network: network, apiKey: zkLoginGoogle.apiKey, providers: { google: googleProviderOptions } }); } catch (err) { try { console.error('[SuiModule] Failed to register Enoki wallets:', err); } catch { } } }; // Enoki API key probe export const probeEnokiApiKey = async (apiKey, network) => { if (!isBrowser) return undefined; if (!apiKey) return undefined; try { const res = await fetch('https://api.enoki.mystenlabs.com/v1/app', { method: 'GET', headers: { Authorization: `Bearer ${apiKey}` } }); if (res.status === 401 || res.status === 403) { try { console.error('[SuiModule] Enoki API key invalid or unauthorized'); } catch { } return false; } if (!res.ok) { try { console.warn('[SuiModule] Enoki /v1/app returned non-OK status:', res.status); } catch { } return undefined; } return true; } catch (err) { try { console.warn('[SuiModule] Enoki API key probe failed:', err); } catch { } return undefined; } }; // Passkey registration export const registerPasskeyWallet = (passkeyConfig, zkLoginGoogle, onDiscoveryRefresh) => { if (!isBrowser) return null; if (!PasskeyService.isSupported()) { console.warn('[SuiModule] WebAuthn is not supported in this browser. Passkey wallet disabled.'); return null; } if (!passkeyConfig.rpId || !passkeyConfig.rpName) { console.error('[SuiModule] Passkey config requires rpId and rpName.'); return null; } const cfg = getConfiguredNetwork(zkLoginGoogle) || 'mainnet'; const adapter = new PasskeyWalletAdapter({ rpId: passkeyConfig.rpId, rpName: passkeyConfig.rpName, authenticatorAttachment: passkeyConfig.authenticatorAttachment, timeout: passkeyConfig.timeout, network: cfg }); try { const wallets = getWallets(); wallets.register(adapter); } catch (err) { console.error('[SuiModule] Failed to register Passkey wallet:', err); return null; } setTimeout(() => { onDiscoveryRefresh(); }, 100); return adapter; }; // Multisig: build resolver context export const buildResolverContext = (passkeyAdapter, wallet, account, walletAdapter) => { const ctx = { connectedWallets: new Map() }; if (passkeyAdapter && wallet?.name === 'Passkey') { const passkeyAccount = passkeyAdapter.accounts?.[0]; if (passkeyAccount) { ctx.passkeyAddress = passkeyAccount.address; try { const pkBytes = passkeyAccount.publicKey; if (pkBytes) { ctx.passkeyPublicKey = new PasskeyPublicKey(pkBytes); ctx.connectedWallets.set(passkeyAccount.address, { address: passkeyAccount.address, publicKey: ctx.passkeyPublicKey }); } } catch { } } } if (wallet?.name === 'Sign in with Google' && account) { ctx.zkLoginAddress = account.address; ctx.connectedWallets.set(account.address, { address: account.address }); } if (account && walletAdapter && wallet?.name !== 'Passkey' && wallet?.name !== 'Sign in with Google') { try { const addr = account.address; let pk = undefined; const accountPk = account.publicKey; if (accountPk) { if (typeof accountPk.toRawBytes === 'function') { pk = accountPk; } else if (accountPk instanceof Uint8Array) { try { pk = new Ed25519PublicKey(accountPk); } catch { try { pk = new Secp256k1PublicKey(accountPk); } catch { } } } } ctx.connectedWallets.set(addr, { address: addr, publicKey: pk }); } catch { } } return ctx; }; // Multisig: get config key for legacy config export const getMultisigConfigKey = (cfg) => { try { const signers = Array.isArray(cfg?.signers) ? cfg.signers : []; const normalized = signers.map((s) => { let pk = ''; try { if (typeof s.publicKey === 'string') pk = s.publicKey; else if (s.publicKey instanceof Uint8Array) pk = toBase64(s.publicKey); } catch { } return { id: s.id, type: s.type, weight: s.weight, publicKey: pk, addressSeed: s.addressSeed || '', issuer: s.issuer || '' }; }); return JSON.stringify({ threshold: cfg.threshold, name: cfg.name || '', signers: normalized }); } catch { return `${cfg?.threshold ?? ''}:${cfg?.name ?? ''}`; } }; // Multisig: register legacy adapter export const registerLegacyMultisigAdapter = (legacyConfig, zkLoginGoogle, onDiscoveryRefresh, onAccountUpdate, onStatusUpdate, onWalletAdapterUpdate, onConnectionDataUpdate, existingAdapter) => { if (!legacyConfig.signers || legacyConfig.signers.length === 0) { console.error('[SuiModule] Multisig config requires at least one signer.'); return null; } if (typeof legacyConfig.threshold !== 'number' || legacyConfig.threshold < 1) { console.error('[SuiModule] Multisig config requires a valid threshold.'); return null; } const signers = legacyConfig.signers.map((s) => { const signer = { id: s.id, type: s.type, weight: s.weight, name: s.name, addressSeed: s.addressSeed, issuer: s.issuer }; if (s.publicKey) { let pkBytes; if (typeof s.publicKey === 'string') { pkBytes = fromBase64(s.publicKey); } else if (s.publicKey instanceof Uint8Array) { pkBytes = s.publicKey; } else { signer.publicKey = s.publicKey; return signer; } switch (s.type) { case 'ed25519': signer.publicKey = new Ed25519PublicKey(pkBytes); break; case 'secp256k1': signer.publicKey = new Secp256k1PublicKey(pkBytes); break; case 'secp256r1': case 'passkey': signer.publicKey = new Secp256r1PublicKey(pkBytes); break; default: signer.publicKey = pkBytes; } } return signer; }); const cfg = getConfiguredNetwork(zkLoginGoogle) || 'mainnet'; const adapterConfig = { threshold: legacyConfig.threshold, signers, name: legacyConfig.name, network: cfg, onSignatureRequest: legacyConfig.onSignatureRequest ? async (signer, txBytes) => { const moduleSigner = legacyConfig.signers?.find((s) => s.id === signer.id) ?? { id: signer.id, type: signer.type, weight: signer.weight, name: signer.name }; return await legacyConfig.onSignatureRequest(moduleSigner, txBytes); } : undefined, onAddressChange: legacyConfig.onAddressChange }; if (existingAdapter) { try { existingAdapter.updateConfig(adapterConfig); const nextAccount = existingAdapter.accounts?.[0] || null; if (nextAccount && typeof existingAdapter.isConnected === 'function' && existingAdapter.isConnected()) { onAccountUpdate(nextAccount); onStatusUpdate('connected'); onWalletAdapterUpdate(existingAdapter); onConnectionDataUpdate({ selectedAccountAddress: nextAccount.address, selectedChain: Array.isArray(nextAccount?.chains) ? nextAccount.chains[0] : undefined }); } } catch (err) { console.error('[SuiModule] Failed to update Multisig wallet config:', err); } setTimeout(() => { onDiscoveryRefresh(); }, 0); return existingAdapter; } const adapter = new MultisigWalletAdapter(adapterConfig); try { const wallets = getWallets(); wallets.register(adapter); } catch (err) { console.error('[SuiModule] Failed to register Multisig wallet:', err); return null; } setTimeout(() => { onDiscoveryRefresh(); }, 100); return adapter; }; // Multisig: initialize new config export const initializeNewMultisigConfig = (multisigConfig, zkLoginGoogle, buildResolverContextFn, signTransactionFn, getCurrentWalletPublicKeyFn) => { const network = multisigConfig.network || getConfiguredNetwork(zkLoginGoogle) || 'testnet'; initializeMultisig(multisigConfig, { getResolverContext: buildResolverContextFn, signTransaction: signTransactionFn, getCurrentWalletPublicKey: getCurrentWalletPublicKeyFn }, network); }; // Helper hooks export const isPasskeyWallet = (wallet, passkeyAdapter) => { return wallet?.name === 'Passkey' && !!passkeyAdapter; }; export const usePasskeyAccount = (wallet, passkeyAdapter) => { return { get isPasskey() { return isPasskeyWallet(wallet, passkeyAdapter); }, get address() { if (!passkeyAdapter || wallet?.name !== 'Passkey') return null; return passkeyAdapter.accounts?.[0]?.address ?? null; }, get publicKey() { if (!passkeyAdapter || wallet?.name !== 'Passkey') return null; return passkeyAdapter.accounts?.[0]?.publicKey ?? null; }, get credentialId() { if (!passkeyAdapter || wallet?.name !== 'Passkey') return null; const acc = passkeyAdapter.accounts?.[0]; return acc?.credentialId ?? null; }, async getCredential() { if (!passkeyAdapter || wallet?.name !== 'Passkey') return null; const feat = passkeyAdapter.features?.['passkey:getCredential']; if (feat && typeof feat.getCredential === 'function') { return await feat.getCredential(); } return null; } }; }; export const isMultisigWallet = (wallet, multisigAdapter) => { return wallet?.name === 'Multisig' && !!multisigAdapter; }; export const useMultisigAccount = (wallet, multisigAdapter) => { return { get isMultisig() { return isMultisigWallet(wallet, multisigAdapter); }, get address() { if (!multisigAdapter || wallet?.name !== 'Multisig') return null; return multisigAdapter.accounts?.[0]?.address ?? null; }, get threshold() { if (!multisigAdapter || wallet?.name !== 'Multisig') return null; return multisigAdapter.accounts?.[0]?.threshold ?? null; }, get totalWeight() { if (!multisigAdapter || wallet?.name !== 'Multisig') return null; return multisigAdapter.accounts?.[0]?.totalWeight ?? null; }, getConfig() { if (!multisigAdapter || wallet?.name !== 'Multisig') return null; return multisigAdapter.getConfig(); }, async getAvailableSigners() { if (!multisigAdapter || wallet?.name !== 'Multisig') return []; return await multisigAdapter.getAvailableSigners(); } }; }; export const isZkLoginWallet = (walletAdapter) => { return !!walletAdapter?.features?.['enoki:getSession']; }; export const getZkLoginInfo = async (walletAdapter) => { if (!isZkLoginWallet(walletAdapter)) return null; try { const sessionFeat = walletAdapter?.features?.['enoki:getSession']; const metaFeat = walletAdapter?.features?.['enoki:getMetadata']; let session = null; let metadata = null; try { if (sessionFeat && typeof sessionFeat.getSession === 'function') { session = await sessionFeat.getSession(); } } catch { } try { if (metaFeat && typeof metaFeat.getMetadata === 'function') { metadata = await metaFeat.getMetadata(); } } catch { } return { session, metadata }; } catch { return null; } };