sui-svelte-wallet-kit
Version:
Svelte 5 wallet kit for Sui: connect wallets, manage accounts, SuiNS, balance, sign transactions/messages
351 lines (350 loc) • 12.8 kB
JavaScript
/**
* SignerResolver
* Resolves public keys from signer definitions
*/
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 { ZkLoginPublicIdentifier } from '@mysten/sui/zklogin';
import { fromBase64, toBase64 } from '@mysten/sui/utils';
import { MultisigError, MultisigErrorCode } from './MultisigTypes.js';
/**
* Parse base64 public key string to PublicKey object
* Supports ed25519, secp256k1, secp256r1, and zklogin key types
*/
export function parsePublicKey(publicKeyBase64, keyType) {
try {
const bytes = fromBase64(publicKeyBase64);
// Handle zklogin type explicitly
if (keyType === 'zklogin') {
return new ZkLoginPublicIdentifier(bytes);
}
// Handle passkey type explicitly (Passkey scheme flag, not Secp256r1)
if (keyType === 'passkey') {
return new PasskeyPublicKey(bytes);
}
// Try to detect zkLogin by byte length (zkLogin identifiers are typically > 48 bytes)
if (bytes.length > 48) {
try {
return new ZkLoginPublicIdentifier(bytes);
}
catch {
// If zkLogin fails, try other types
}
}
switch (keyType) {
case 'ed25519':
return new Ed25519PublicKey(bytes);
case 'secp256k1':
return new Secp256k1PublicKey(bytes);
case 'secp256r1':
return new Secp256r1PublicKey(bytes);
default:
throw new Error(`Unknown key type: ${keyType}`);
}
}
catch (error) {
throw new MultisigError(MultisigErrorCode.PUBLIC_KEY_PARSE_ERROR, `Failed to parse public key: ${error.message}`, error);
}
}
/**
* Generate a unique ID for a signer
*/
export function generateSignerId(signer, index) {
switch (signer.type) {
case 'passkey':
return `passkey-${index}`;
case 'zklogin':
return `zklogin-${index}`;
case 'wallet':
return `wallet-${signer.address.slice(0, 10)}-${index}`;
case 'publicKey':
return `pubkey-${signer.publicKey.slice(0, 8)}-${index}`;
default:
return `signer-${index}`;
}
}
/**
* SignerResolver class
* Handles resolution of signer definitions to actual public keys
*/
export class SignerResolver {
/**
* Check if a signer can be resolved with current context
*/
canResolve(signer, context) {
switch (signer.type) {
case 'passkey':
return !!context.passkeyPublicKey;
case 'zklogin':
return !!context.zkLoginPublicIdentifier;
case 'wallet':
return context.connectedWallets.has(signer.address);
case 'publicKey':
// Public key signers can always be resolved (they have the key inline)
return true;
default:
return false;
}
}
/**
* Resolve a signer definition to a ResolvedSigner
*/
resolve(signer, index, context) {
const id = generateSignerId(signer, index);
const weight = signer.weight ?? 1;
const name = signer.name;
try {
switch (signer.type) {
case 'passkey':
return this.resolvePasskeySigner(id, weight, name, context, signer.address);
case 'zklogin':
return this.resolveZkLoginSigner(id, weight, name, context, signer.address);
case 'wallet':
return this.resolveWalletSigner(id, signer.address, weight, name, context);
case 'publicKey':
return this.resolvePublicKeySigner(id, signer.publicKey, signer.keyType, weight, name, signer.address);
default:
return {
id,
type: 'ed25519',
weight,
name,
resolved: false,
error: `Unknown signer type: ${signer.type}`
};
}
}
catch (error) {
return {
id,
type: 'ed25519',
weight,
name,
resolved: false,
error: error.message
};
}
}
/**
* Resolve passkey signer
* Uses saved address from signer config if available, falls back to context
*/
resolvePasskeySigner(id, weight, name, context, savedAddress) {
const address = savedAddress || context.passkeyAddress;
if (!context.passkeyPublicKey) {
// If we have a saved address, mark as resolved (signing will work when passkey connects)
if (address) {
return {
id,
type: 'passkey',
weight,
name: name ?? 'Passkey',
address,
resolved: true // Resolved because we have address for multisig derivation
};
}
return {
id,
type: 'passkey',
weight,
name: name ?? 'Passkey',
resolved: false,
error: 'Passkey wallet not connected'
};
}
const pkBase64 = toBase64(context.passkeyPublicKey.toRawBytes());
return {
id,
type: 'passkey',
weight,
name: name ?? 'Passkey',
publicKey: context.passkeyPublicKey,
publicKeyBase64: pkBase64,
address: address || context.passkeyAddress,
resolved: true
};
}
/**
* Resolve zkLogin signer
* Uses saved address from signer config if available, falls back to context.
* zkLogin signers are considered "resolved" if we have an address,
* because the actual signing will be handled by the zkLogin wallet.
*/
resolveZkLoginSigner(id, weight, name, context, savedAddress) {
const address = savedAddress || context.zkLoginAddress;
// If we have a saved address, mark as resolved (for multisig address derivation)
// Signing will work when zkLogin wallet connects
if (address) {
// If we have full zkLogin info from context, include it
if (context.zkLoginAddress === address &&
context.zkLoginPublicIdentifier &&
context.zkLoginAddressSeed &&
context.zkLoginIssuer) {
const pkBase64 = toBase64(context.zkLoginPublicIdentifier.toRawBytes());
return {
id,
type: 'zklogin',
weight,
name: name ?? 'zkLogin',
publicKey: context.zkLoginPublicIdentifier,
publicKeyBase64: pkBase64,
addressSeed: context.zkLoginAddressSeed,
issuer: context.zkLoginIssuer,
address,
resolved: true
};
}
// Mark as resolved with just address
return {
id,
type: 'zklogin',
weight,
name: name ?? 'zkLogin',
address,
resolved: true
};
}
return {
id,
type: 'zklogin',
weight,
name: name ?? 'zkLogin',
resolved: false,
error: 'zkLogin wallet not connected and no saved address'
};
}
/**
* Resolve wallet signer by address
* Note: Wallet signers are considered "resolved" if the wallet is connected,
* even without public key - the signing will work via wallet adapter.
*/
resolveWalletSigner(id, address, weight, name, context) {
const wallet = context.connectedWallets.get(address);
if (!wallet) {
return {
id,
type: 'ed25519', // Default, will be updated when resolved
weight,
name: name ?? `Wallet ${address.slice(0, 8)}...`,
address,
resolved: false,
error: 'Wallet not connected'
};
}
// If we have public key, use it to determine type
if (wallet.publicKey) {
const type = this.inferTypeFromPublicKey(wallet.publicKey);
const pkBase64 = toBase64(wallet.publicKey.toRawBytes());
return {
id,
type,
weight,
name: name ?? `Wallet ${address.slice(0, 8)}...`,
publicKey: wallet.publicKey,
publicKeyBase64: pkBase64,
address,
resolved: true
};
}
// Wallet is connected but no public key - still mark as resolved
// Signing will work via wallet adapter
return {
id,
type: 'ed25519', // Default type
weight,
name: name ?? `Wallet ${address.slice(0, 8)}...`,
address,
resolved: true
};
}
/**
* Resolve public key signer (inline key)
* Note: Public key signers are always considered "resolved" because they have
* the public key inline - we have all info needed to derive multisig address.
* Wallet connection is only checked when signing.
*/
resolvePublicKeySigner(id, publicKeyBase64, keyType, weight, name, signerAddress) {
try {
const publicKey = parsePublicKey(publicKeyBase64, keyType);
// Always derive address from public key.
// (Using a persisted address here can drift and cause signing/execution mismatches.)
const derivedAddress = publicKey.toSuiAddress();
if (signerAddress && signerAddress !== derivedAddress) {
try {
console.warn(`[SignerResolver] Signer '${id}' persisted address mismatch. Using derived address from public key.`, { persisted: signerAddress, derived: derivedAddress });
}
catch { }
}
const address = derivedAddress;
// Map keyType to ResolvedSignerType (zklogin stays as zklogin)
const resolvedType = keyType === 'zklogin' ? 'zklogin' : keyType;
return {
id,
type: resolvedType,
weight,
name: name ?? `PublicKey ${publicKeyBase64.slice(0, 8)}...`,
publicKey,
publicKeyBase64,
address,
resolved: true
};
}
catch (error) {
// Even if parsing fails, try to mark as resolved if we have address
// This allows the multisig to work even with parse errors
console.warn(`[SignerResolver] Failed to parse public key: ${error.message}`);
const resolvedType = keyType === 'zklogin' ? 'zklogin' : keyType;
return {
id,
type: resolvedType,
weight,
name: name ?? `PublicKey ${publicKeyBase64.slice(0, 8)}...`,
publicKeyBase64,
address: signerAddress,
resolved: false,
error: error.message
};
}
}
/**
* Infer signer type from public key
*/
inferTypeFromPublicKey(publicKey) {
const flag = publicKey.flag();
// Sui public key flags
// 0x00 = ED25519
// 0x01 = Secp256k1
// 0x02 = Secp256r1
// 0x03 = MultiSig (Not currently supported as a signer type)
// 0x05 = ZkLogin
// 0x06 = Passkey
switch (flag) {
case 0x00:
return 'ed25519';
case 0x01:
return 'secp256k1';
case 0x02:
return 'secp256r1';
case 0x05:
return 'zklogin';
case 0x06:
return 'passkey';
default:
return 'ed25519'; // Default fallback
}
}
/**
* Resolve all signers from a list
*/
resolveAll(signers, context) {
return signers.map((signer, index) => this.resolve(signer, index, context));
}
}
/**
* Create a SignerResolver instance
*/
export function createSignerResolver() {
return new SignerResolver();
}