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
JavaScript
/**
* 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);
}