UNPKG

zklogin-plus

Version:

A powerful zkLogin plugin for Sui blockchain - inspired by @mysten/enoki

448 lines 17.4 kB
import { SuiClient } from '@onelabs/sui/client'; import { Transaction } from '@onelabs/sui/transactions'; import { Ed25519Keypair } from '@onelabs/sui/keypairs/ed25519'; import { MIST_PER_SUI } from '@onelabs/sui/utils'; import { NETWORK_URLS, DEFAULT_CONFIG, generateEphemeralKeyPair, decodeJwtToken, generateUserSalt, generateZkLoginAddress, getExtendedPublicKey, requestZkProof, buildZkLoginSignature, getCurrentEpoch, calculateMaxEpoch, getAddressBalance, requestFaucet, validateConfig, ZkLoginError, } from '../utils'; import { BrowserStorage, BrowserSessionStorage, STORAGE_KEYS } from '../storage'; import { createOAuthProvider, parseOAuthCallback, validateOAuthCallback } from '../providers'; /** * 事件发射器 */ class EventEmitter { constructor() { this.listeners = {}; } on(event, listener) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(listener); } off(event, listener) { if (!this.listeners[event]) return; const index = this.listeners[event].indexOf(listener); if (index > -1) { this.listeners[event].splice(index, 1); } } emit(event, ...args) { if (!this.listeners[event]) return; this.listeners[event].forEach(listener => { try { listener(...args); } catch (error) { console.error(`Error in event listener for ${String(event)}:`, error); } }); } } /** * ZkLogin客户端 */ export class ZkLoginClient extends EventEmitter { constructor(config) { super(); validateConfig(config); this.config = { network: 'devnet', proverEndpoint: DEFAULT_CONFIG.proverEndpoint, faucetEndpoint: DEFAULT_CONFIG.faucetEndpoint, storagePrefix: DEFAULT_CONFIG.storagePrefix, getSaltUrl: DEFAULT_CONFIG.getSaltUrl, debug: false, ...config, }; // 初始化Sui客户端 const networkUrl = typeof this.config.network === 'string' && this.config.network in NETWORK_URLS ? NETWORK_URLS[this.config.network] : this.config.network; this.suiClient = new SuiClient({ url: networkUrl }); // 初始化存储 this.storage = new BrowserStorage(this.config.storagePrefix); this.sessionStorage = new BrowserSessionStorage(this.config.storagePrefix); // 初始化状态 this.state = { currentStep: 0, isReady: false, }; // 从存储中恢复状态 this.restoreState(); } /** * 获取当前状态 */ getState() { return { ...this.state }; } /** * 获取配置 */ getConfig() { return { ...this.config }; } /** * 获取Sui客户端 */ getSuiClient() { return this.suiClient; } /** * 步骤1: 生成临时密钥对 */ async generateEphemeralKeyPair() { try { this.debug('Generating ephemeral key pair...'); // 获取当前epoch const currentEpoch = await getCurrentEpoch(this.suiClient); const maxEpoch = calculateMaxEpoch(currentEpoch); // 生成密钥对 const keyPairState = generateEphemeralKeyPair(maxEpoch); // 保存到session storage this.sessionStorage.set(STORAGE_KEYS.EPHEMERAL_KEYPAIR, keyPairState.keyPair.getSecretKey()); this.sessionStorage.set(STORAGE_KEYS.MAX_EPOCH, maxEpoch.toString()); this.sessionStorage.set(STORAGE_KEYS.RANDOMNESS, keyPairState.randomness); // 更新状态 this.state.ephemeralKeyPair = keyPairState; this.state.currentStep = Math.max(this.state.currentStep, 1); this.emit('keypair:generated', keyPairState); this.emit('step:changed', this.state.currentStep); return keyPairState; } catch (error) { const errorMsg = `Failed to generate ephemeral key pair: ${error}`; this.handleError(errorMsg, 1); throw new ZkLoginError(errorMsg, 'KEYPAIR_GENERATION_FAILED', 1); } } /** * 步骤2: 重定向到OAuth提供商 */ redirectToOAuth(provider = 'google', state) { try { if (!this.state.ephemeralKeyPair) { throw new Error('Ephemeral key pair not generated yet'); } this.debug(`Redirecting to OAuth provider: ${provider}`); const oauthProvider = createOAuthProvider(provider, this.config.clientId, this.config.redirectUri); oauthProvider.redirect(this.state.ephemeralKeyPair.nonce, state); } catch (error) { const errorMsg = `Failed to redirect to OAuth: ${error}`; this.handleError(errorMsg, 2); throw new ZkLoginError(errorMsg, 'OAUTH_REDIRECT_FAILED', 2); } } /** * 步骤3: 处理OAuth回调并解码JWT */ handleOAuthCallback(url) { try { this.debug('Handling OAuth callback...'); const params = parseOAuthCallback(url); validateOAuthCallback(params); const jwtState = decodeJwtToken(params.id_token); // 保存JWT到session storage this.sessionStorage.set(STORAGE_KEYS.JWT_TOKEN, params.id_token); // 更新状态 this.state.jwt = jwtState; this.state.currentStep = Math.max(this.state.currentStep, 3); this.emit('jwt:received', jwtState); this.emit('step:changed', this.state.currentStep); this.generateSalt(); return jwtState; } catch (error) { const errorMsg = `Failed to handle OAuth callback: ${error}`; this.handleError(errorMsg, 3); throw new ZkLoginError(errorMsg, 'OAUTH_CALLBACK_FAILED', 3); } } /** * 步骤4: 生成用户Salt */ async generateSalt() { try { this.debug('Generating user salt...'); const jwtToken = this.sessionStorage.get(STORAGE_KEYS.JWT_TOKEN); const salt = await generateUserSalt(this.config.getSaltUrl, jwtToken); const saltState = { salt, createdAt: Date.now(), }; // 保存到localStorage this.storage.set(STORAGE_KEYS.USER_SALT, JSON.stringify(saltState)); // 更新状态 this.state.userSalt = saltState; this.state.currentStep = Math.max(this.state.currentStep, 4); this.emit('salt:generated', saltState); this.emit('step:changed', this.state.currentStep); this.generateAddress(); return saltState; } catch (error) { const errorMsg = `Failed to generate salt: ${error}`; this.handleError(errorMsg, 4); throw new ZkLoginError(errorMsg, 'SALT_GENERATION_FAILED', 4); } } /** * 步骤5: 生成ZkLogin地址 */ async generateAddress() { try { if (!this.state.jwt) { throw new Error('JWT not available'); } if (!this.state.userSalt) { throw new Error('User salt not generated'); } this.debug('Generating zkLogin address...'); const addressState = generateZkLoginAddress(this.state.jwt.token, this.state.userSalt.salt); // 获取余额 addressState.balance = await getAddressBalance(this.suiClient, addressState.address); // 保存到localStorage this.storage.set(STORAGE_KEYS.ZKLOGIN_ADDRESS, JSON.stringify(addressState)); // 更新状态 this.state.zkLoginAddress = addressState; this.state.currentStep = Math.max(this.state.currentStep, 5); this.emit('address:generated', addressState); this.emit('step:changed', this.state.currentStep); this.getZkProof(); return addressState; } catch (error) { const errorMsg = `Failed to generate address: ${error}`; this.handleError(errorMsg, 5); throw new ZkLoginError(errorMsg, 'ADDRESS_GENERATION_FAILED', 5); } } /** * 步骤6: 获取ZK证明 */ async getZkProof() { try { if (!this.state.ephemeralKeyPair) { throw new Error('Ephemeral key pair not available'); } if (!this.state.jwt) { throw new Error('JWT not available'); } if (!this.state.userSalt) { throw new Error('User salt not available'); } this.debug('Requesting ZK proof...'); const extendedPublicKey = getExtendedPublicKey(this.state.ephemeralKeyPair.keyPair.getPublicKey()); const zkProof = await requestZkProof(this.state.jwt.token, extendedPublicKey, this.state.ephemeralKeyPair.maxEpoch, this.state.ephemeralKeyPair.randomness, this.state.userSalt.salt, this.config.proverEndpoint); // 保存ZK证明 this.sessionStorage.set(STORAGE_KEYS.ZK_PROOF, JSON.stringify(zkProof)); // 更新状态 this.state.zkProof = zkProof; this.state.currentStep = Math.max(this.state.currentStep, 6); this.state.isReady = true; this.emit('proof:received', zkProof); this.emit('step:changed', this.state.currentStep); this.emit('ready'); return zkProof; } catch (error) { const errorMsg = `Failed to get ZK proof: ${error}`; this.handleError(errorMsg, 6); throw new ZkLoginError(errorMsg, 'ZK_PROOF_FAILED', 6); } } /** * 步骤7: 执行交易 */ async executeTransaction(options = {}) { try { if (!this.state.isReady) { throw new Error('ZkLogin not ready. Complete all steps first.'); } this.debug('Executing transaction...'); const { ephemeralKeyPair, zkProof, zkLoginAddress } = this.state; if (!ephemeralKeyPair || !zkProof || !zkLoginAddress) { throw new Error('Missing required state for transaction execution'); } // 创建交易 const txb = new Transaction(); if (options.recipient && options.amount) { // 转账交易 const [coin] = txb.splitCoins(txb.gas, [options.amount]); txb.transferObjects([coin], options.recipient); } else if (options.transactionData) { // 自定义交易数据 // 这里可以根据需要扩展 } else { // 默认交易:转账少量SUI给固定地址 const [coin] = txb.splitCoins(txb.gas, [MIST_PER_SUI / 1n]); txb.transferObjects([coin], "0x23bf8c3d7d2d55f8b78a72e3ee2d53a849c9db976ac5e8142e3ee12be4cf81d6"); } txb.setSender(zkLoginAddress.address); // 签名交易 const { bytes, signature: userSignature } = await txb.sign({ client: this.suiClient, signer: ephemeralKeyPair.keyPair, }); // 构建zkLogin签名 const zkLoginSignature = buildZkLoginSignature(zkProof, ephemeralKeyPair.maxEpoch, userSignature, zkLoginAddress.addressSeed); // 执行交易 const executeRes = await this.suiClient.executeTransactionBlock({ transactionBlock: bytes, signature: zkLoginSignature, }); this.debug(`Transaction executed: ${executeRes.digest}`); return executeRes.digest; } catch (error) { const errorMsg = `Failed to execute transaction: ${error}`; this.handleError(errorMsg, 7); throw new ZkLoginError(errorMsg, 'TRANSACTION_EXECUTION_FAILED', 7); } } /** * 请求测试代币 */ async requestTestTokens() { try { if (!this.state.zkLoginAddress) { throw new Error('ZkLogin address not generated'); } this.debug('Requesting test tokens from faucet...'); await requestFaucet(this.state.zkLoginAddress.address, this.config.faucetEndpoint); // 更新余额 const balance = await getAddressBalance(this.suiClient, this.state.zkLoginAddress.address); this.state.zkLoginAddress.balance = balance; this.debug('Test tokens requested successfully'); } catch (error) { const errorMsg = `Failed to request test tokens: ${error}`; this.handleError(errorMsg); throw new ZkLoginError(errorMsg, 'FAUCET_REQUEST_FAILED'); } } /** * 刷新地址余额 */ async refreshBalance() { if (!this.state.zkLoginAddress) { throw new Error('ZkLogin address not generated'); } const balance = await getAddressBalance(this.suiClient, this.state.zkLoginAddress.address); this.state.zkLoginAddress.balance = balance; return balance; } /** * 重置所有状态 */ reset() { this.debug('Resetting all state...'); this.storage.clear(); this.sessionStorage.clear(); this.state = { currentStep: 0, isReady: false, }; this.debug('State reset completed'); this.generateEphemeralKeyPair(); } /** * 从存储中恢复状态 */ restoreState() { try { // 恢复ephemeral key pair const privateKey = this.sessionStorage.get(STORAGE_KEYS.EPHEMERAL_KEYPAIR); const maxEpochStr = this.sessionStorage.get(STORAGE_KEYS.MAX_EPOCH); const randomness = this.sessionStorage.get(STORAGE_KEYS.RANDOMNESS); if (privateKey && maxEpochStr && randomness) { const keyPair = Ed25519Keypair.fromSecretKey(privateKey); const maxEpoch = parseInt(maxEpochStr); this.state.ephemeralKeyPair = { keyPair, maxEpoch, randomness, nonce: '', // nonce会在需要时重新生成 }; this.state.currentStep = Math.max(this.state.currentStep, 1); } else { this.generateEphemeralKeyPair(); } // 恢复JWT const jwtToken = this.sessionStorage.get(STORAGE_KEYS.JWT_TOKEN); if (jwtToken) { try { this.state.jwt = decodeJwtToken(jwtToken); this.state.currentStep = Math.max(this.state.currentStep, 3); } catch (error) { // JWT可能已过期,清除它 this.sessionStorage.remove(STORAGE_KEYS.JWT_TOKEN); } } // 恢复用户salt const userSaltStr = this.storage.get(STORAGE_KEYS.USER_SALT); if (userSaltStr) { try { this.state.userSalt = JSON.parse(userSaltStr); this.state.currentStep = Math.max(this.state.currentStep, 4); } catch (error) { this.storage.remove(STORAGE_KEYS.USER_SALT); } } // 恢复zkLogin地址 const addressStr = this.storage.get(STORAGE_KEYS.ZKLOGIN_ADDRESS); if (addressStr) { try { this.state.zkLoginAddress = JSON.parse(addressStr); this.state.currentStep = Math.max(this.state.currentStep, 5); } catch (error) { this.storage.remove(STORAGE_KEYS.ZKLOGIN_ADDRESS); } } // 恢复ZK证明 const zkProofStr = this.sessionStorage.get(STORAGE_KEYS.ZK_PROOF); if (zkProofStr) { try { this.state.zkProof = JSON.parse(zkProofStr); this.state.currentStep = Math.max(this.state.currentStep, 6); this.state.isReady = true; } catch (error) { this.sessionStorage.remove(STORAGE_KEYS.ZK_PROOF); } } this.debug(`State restored. Current step: ${this.state.currentStep}`); } catch (error) { this.debug(`Failed to restore state: ${error}`); } } /** * 错误处理 */ handleError(message, step) { this.state.error = message; if (step !== undefined) { this.state.currentStep = step; } this.emit('error', message); this.debug(`Error: ${message}`); } /** * 调试日志 */ debug(message) { if (this.config.debug) { console.log(`[ZkLoginClient] ${message}`); } } } //# sourceMappingURL=index.js.map