UNPKG

sui-svelte-wallet-kit

Version:

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

311 lines (310 loc) 11 kB
/** * MultisigWalletAdapter * Implements SuiWalletAdapter interface for multisig accounts */ import { MultisigService, MultisigError, MultisigErrorCode } from '../MultisigService/index.js'; import { toBase64 } from '@mysten/sui/utils'; // Multisig wallet icon (users emoji as data URL) const MULTISIG_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNiAyMXYtMmE0IDQgMCAwIDAtNC00SDZhNCA0IDAgMCAwLTQgNHYyIi8+PGNpcmNsZSBjeD0iOSIgY3k9IjciIHI9IjQiLz48cGF0aCBkPSJNMjIgMjF2LTJhNCA0IDAgMCAwLTMtMy44NyIvPjxwYXRoIGQ9Ik0xNiAzLjEzYTQgNCAwIDAgMSAwIDcuNzUiLz48L3N2Zz4='; /** * MultisigWalletAdapter class */ export class MultisigWalletAdapter { name = 'Multisig'; version = '1.0.0'; icon; chains = ['sui:mainnet', 'sui:testnet', 'sui:devnet']; config; service; _accounts = []; _connected = false; signerProviders = new Map(); constructor(config) { this.config = config; this.service = new MultisigService(config); this.icon = config.icon || MULTISIG_ICON; // Register signer providers if (config.signerProviders) { for (const provider of config.signerProviders) { this.signerProviders.set(provider.signerId, provider); } } } get accounts() { return this._accounts; } get features() { return { 'standard:connect': { connect: () => this.connect() }, 'standard:disconnect': { disconnect: () => this.disconnect() }, 'sui:signAndExecuteTransaction': { signAndExecuteTransaction: (params) => this.signAndExecuteTransaction(params) }, 'sui:signPersonalMessage': { signPersonalMessage: (params) => this.signPersonalMessage(params) }, 'multisig:getConfig': { getConfig: () => this.getConfig() }, 'multisig:getAvailableSigners': { getAvailableSigners: () => this.getAvailableSigners() } }; } /** * Connect to multisig wallet */ async connect() { const address = this.service.getAddress(); const multiSigPk = this.service.getMultiSigPublicKey(); this.setupAccount(address, multiSigPk.toRawBytes()); this._connected = true; } /** * Disconnect from multisig wallet */ async disconnect() { this._accounts = []; this._connected = false; } /** * Sign and execute a transaction */ async signAndExecuteTransaction(params) { if (!this._connected) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'Not connected to multisig wallet'); } const { SuiClient, getFullnodeUrl } = await import('@mysten/sui/client'); // Get network from chain const network = params.chain.replace('sui:', ''); const client = new SuiClient({ url: getFullnodeUrl(network) }); const transaction = params.transaction; const senderAddress = this.service.getAddress(); // Ensure sender is set if (typeof transaction.setSender === 'function') { transaction.setSender(senderAddress); } // Build the transaction const txBytes = await transaction.build({ client }); // Collect signatures const signatures = await this.collectSignatures(txBytes); // Combine signatures const combinedSignature = this.service.combineSignatures(signatures); // Execute the transaction const result = await client.executeTransactionBlock({ transactionBlock: txBytes, signature: combinedSignature, options: { showEffects: true, showEvents: true } }); return result; } /** * Sign a personal message */ async signPersonalMessage(params) { if (!this._connected) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'Not connected to multisig wallet'); } // Collect signatures for message const signatures = await this.collectMessageSignatures(params.message); // Combine signatures const combinedSignature = this.service.combineSignatures(signatures); return { signature: combinedSignature, messageBytes: toBase64(params.message) }; } /** * Collect signatures from signers for transaction */ async collectSignatures(txBytes) { const signatures = []; let collectedWeight = 0; const threshold = this.config.threshold; const signers = this.service.getSigners(); for (const signer of signers) { if (collectedWeight >= threshold) { break; // Threshold met } try { let signature = null; // Try signer provider first const provider = this.signerProviders.get(signer.id); if (provider) { const isAvailable = await provider.isAvailable(); if (isAvailable) { const result = await provider.signTransaction(txBytes); signature = result.signature; } } // Fall back to callback if (!signature && this.config.onSignatureRequest) { const result = await this.config.onSignatureRequest(signer, txBytes); if (result) { signature = result.signature; } } if (signature) { signatures.push(signature); collectedWeight += signer.weight; } } catch (error) { // Skip this signer on error, continue to next console.warn(`[MultisigWalletAdapter] Signer ${signer.id} failed:`, error); } } if (collectedWeight < threshold) { throw new MultisigError(MultisigErrorCode.INSUFFICIENT_SIGNATURES, `Collected weight (${collectedWeight}) is less than threshold (${threshold})`); } return signatures; } /** * Collect signatures from signers for personal message */ async collectMessageSignatures(message) { const signatures = []; let collectedWeight = 0; const threshold = this.config.threshold; const signers = this.service.getSigners(); for (const signer of signers) { if (collectedWeight >= threshold) { break; } try { const provider = this.signerProviders.get(signer.id); if (provider) { const isAvailable = await provider.isAvailable(); if (isAvailable) { const result = await provider.signPersonalMessage(message); signatures.push(result.signature); collectedWeight += signer.weight; } } } catch (error) { console.warn(`[MultisigWalletAdapter] Signer ${signer.id} failed:`, error); } } if (collectedWeight < threshold) { throw new MultisigError(MultisigErrorCode.INSUFFICIENT_SIGNATURES, `Collected weight (${collectedWeight}) is less than threshold (${threshold})`); } return signatures; } /** * Get current multisig configuration */ getConfig() { return this.service.getConfig(); } /** * Get available signers with their status */ async getAvailableSigners() { const signers = this.service.getSigners(); const statuses = []; for (const signer of signers) { let available = false; const provider = this.signerProviders.get(signer.id); if (provider) { try { available = await provider.isAvailable(); } catch { available = false; } } statuses.push({ id: signer.id, name: signer.name, type: signer.type, weight: signer.weight, available, signed: false }); } return statuses; } /** * Check if connected */ isConnected() { return this._connected; } /** * Get MultisigService instance */ getService() { return this.service; } /** * Update multisig configuration */ updateConfig(newConfig) { const oldAddress = this.service.getAddress(); this.config = newConfig; this.service = new MultisigService(newConfig); // Update signer providers this.signerProviders.clear(); if (newConfig.signerProviders) { for (const provider of newConfig.signerProviders) { this.signerProviders.set(provider.signerId, provider); } } const newAddress = this.service.getAddress(); // Update account if connected if (this._connected) { const multiSigPk = this.service.getMultiSigPublicKey(); this.setupAccount(newAddress, multiSigPk.toRawBytes()); } // Notify address change if (oldAddress !== newAddress && this.config.onAddressChange) { this.config.onAddressChange(oldAddress, newAddress); } } /** * Add a signer provider */ addSignerProvider(provider) { this.signerProviders.set(provider.signerId, provider); } /** * Remove a signer provider */ removeSignerProvider(signerId) { this.signerProviders.delete(signerId); } /** * Set up account from address */ setupAccount(address, publicKey) { const network = this.config.network || 'mainnet'; const account = { address, publicKey, chains: [`sui:${network}`], features: [ 'sui:signAndExecuteTransaction', 'sui:signPersonalMessage', 'multisig:getConfig', 'multisig:getAvailableSigners' ], threshold: this.config.threshold, totalWeight: this.service.getTotalWeight() }; this._accounts = [account]; } } /** * Create a MultisigWalletAdapter instance */ export function createMultisigWalletAdapter(config) { return new MultisigWalletAdapter(config); }