UNPKG

sui-svelte-wallet-kit

Version:

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

281 lines (280 loc) 10.1 kB
/** * MultisigService * Handles multisig wallet operations including address derivation and signature collection */ import { MultiSigPublicKey } from '@mysten/sui/multisig'; import { toZkLoginPublicIdentifier } from '@mysten/sui/zklogin'; import { MultisigError, MultisigErrorCode } from './types.js'; /** * MultisigService class for multisig wallet operations */ export class MultisigService { config; multiSigPublicKey = null; constructor(config) { this.validateConfig(config); this.config = config; this.multiSigPublicKey = this.createMultiSigPublicKey(); } /** * Validate multisig configuration */ validateConfig(config) { if (!config.signers || config.signers.length === 0) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'MultisigConfig requires at least one signer'); } if (typeof config.threshold !== 'number' || config.threshold < 1) { throw new MultisigError(MultisigErrorCode.INVALID_THRESHOLD, 'Threshold must be a positive number'); } // Calculate total weight const totalWeight = config.signers.reduce((sum, s) => sum + (s.weight || 1), 0); if (config.threshold > totalWeight) { throw new MultisigError(MultisigErrorCode.INVALID_THRESHOLD, `Threshold (${config.threshold}) cannot exceed total signer weight (${totalWeight})`); } // Validate each signer for (const signer of config.signers) { this.validateSigner(signer); } // Check for duplicate signer IDs const ids = config.signers.map((s) => s.id); const uniqueIds = new Set(ids); if (ids.length !== uniqueIds.size) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'Duplicate signer IDs found'); } } /** * Validate individual signer configuration */ validateSigner(signer) { if (!signer.id) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, 'Signer must have an id'); } if (typeof signer.weight !== 'number' || signer.weight < 1) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer ${signer.id}: weight must be a positive number`); } // Validate based on signer type switch (signer.type) { case 'ed25519': case 'secp256k1': case 'secp256r1': case 'passkey': if (!signer.publicKey) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer ${signer.id}: ${signer.type} signer requires publicKey`); } break; case 'zklogin': if (!signer.addressSeed || !signer.issuer) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer ${signer.id}: zklogin signer requires addressSeed and issuer`); } break; default: throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer ${signer.id}: unknown signer type ${signer.type}`); } } /** * Create MultiSigPublicKey from config */ createMultiSigPublicKey() { const publicKeys = []; for (const signer of this.config.signers) { const pk = this.getPublicKeyForSigner(signer); publicKeys.push({ publicKey: pk, weight: signer.weight }); } return MultiSigPublicKey.fromPublicKeys({ threshold: this.config.threshold, publicKeys }); } /** * Get PublicKey object for a signer */ getPublicKeyForSigner(signer) { switch (signer.type) { case 'ed25519': case 'secp256k1': case 'secp256r1': case 'passkey': if (!signer.publicKey) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer ${signer.id}: missing publicKey`); } return signer.publicKey; case 'zklogin': if (!signer.addressSeed || !signer.issuer) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer ${signer.id}: missing addressSeed or issuer`); } // addressSeed needs to be BigInt for toZkLoginPublicIdentifier return toZkLoginPublicIdentifier(BigInt(signer.addressSeed), signer.issuer); default: throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Unknown signer type: ${signer.type}`); } } /** * Get the multisig address */ getAddress() { if (!this.multiSigPublicKey) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'MultiSigPublicKey not initialized'); } return this.multiSigPublicKey.toSuiAddress(); } /** * Get the MultiSigPublicKey instance */ getMultiSigPublicKey() { if (!this.multiSigPublicKey) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'MultiSigPublicKey not initialized'); } return this.multiSigPublicKey; } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Get account info */ getAccountInfo() { const totalWeight = this.config.signers.reduce((sum, s) => sum + s.weight, 0); return { address: this.getAddress(), config: this.getConfig(), totalWeight }; } /** * Get total weight of all signers */ getTotalWeight() { return this.config.signers.reduce((sum, s) => sum + s.weight, 0); } /** * Get signer by ID */ getSigner(id) { return this.config.signers.find((s) => s.id === id); } /** * Get all signers */ getSigners() { return [...this.config.signers]; } /** * Combine partial signatures into a multisig signature */ combineSignatures(signatures) { if (!this.multiSigPublicKey) { throw new MultisigError(MultisigErrorCode.INVALID_CONFIG, 'MultiSigPublicKey not initialized'); } if (signatures.length === 0) { throw new MultisigError(MultisigErrorCode.INSUFFICIENT_SIGNATURES, 'No signatures provided'); } return this.multiSigPublicKey.combinePartialSignatures(signatures); } /** * Verify if collected signatures meet threshold */ verifyThreshold(signerIds) { const collectedWeight = signerIds.reduce((sum, id) => { const signer = this.getSigner(id); return sum + (signer?.weight || 0); }, 0); return collectedWeight >= this.config.threshold; } /** * Get initial signer statuses */ getInitialSignerStatuses() { return this.config.signers.map((signer) => ({ id: signer.id, name: signer.name, type: signer.type, weight: signer.weight, available: true, signed: false })); } /** * Update configuration (creates new MultiSigPublicKey) * Returns the new address */ updateConfig(newConfig) { this.validateConfig(newConfig); this.config = newConfig; this.multiSigPublicKey = this.createMultiSigPublicKey(); return this.getAddress(); } /** * Add a signer to the configuration * Returns the new address */ addSigner(signer) { this.validateSigner(signer); // Check for duplicate ID if (this.config.signers.some((s) => s.id === signer.id)) { throw new MultisigError(MultisigErrorCode.INVALID_SIGNER, `Signer with id ${signer.id} already exists`); } const newConfig = { ...this.config, signers: [...this.config.signers, signer] }; return this.updateConfig(newConfig); } /** * Remove a signer from the configuration * Returns the new address */ removeSigner(signerId) { const signerIndex = this.config.signers.findIndex((s) => s.id === signerId); if (signerIndex === -1) { throw new MultisigError(MultisigErrorCode.SIGNER_NOT_FOUND, `Signer with id ${signerId} not found`); } const newSigners = this.config.signers.filter((s) => s.id !== signerId); // Validate that remaining signers can still meet threshold const remainingWeight = newSigners.reduce((sum, s) => sum + s.weight, 0); if (remainingWeight < this.config.threshold) { throw new MultisigError(MultisigErrorCode.INVALID_THRESHOLD, `Cannot remove signer: remaining weight (${remainingWeight}) would be less than threshold (${this.config.threshold})`); } const newConfig = { ...this.config, signers: newSigners }; return this.updateConfig(newConfig); } /** * Update threshold * Returns the new address (address changes when threshold changes) */ updateThreshold(newThreshold) { const totalWeight = this.getTotalWeight(); if (newThreshold < 1) { throw new MultisigError(MultisigErrorCode.INVALID_THRESHOLD, 'Threshold must be at least 1'); } if (newThreshold > totalWeight) { throw new MultisigError(MultisigErrorCode.INVALID_THRESHOLD, `Threshold (${newThreshold}) cannot exceed total weight (${totalWeight})`); } const newConfig = { ...this.config, threshold: newThreshold }; return this.updateConfig(newConfig); } } /** * Create a MultisigService instance */ export function createMultisigService(config) { return new MultisigService(config); } /** * Derive multisig address from config without creating full service */ export function deriveMultisigAddress(config) { const service = new MultisigService(config); return service.getAddress(); }