import { CHAIN_NAMESPACES, type IPortalApi, type KeychainAdapter, } from '@portal-hq/utils' import type { ProgressCallback } from '@portal-hq/utils' import type { AddressesByNamespace, BackupConfigs, BackupSharePairMetadata, Shares, SigningSharePairMetadata, } from '@portal-hq/utils/types' import type PortalMpc from '../mpc' import { BackupMethods, PortalCurve, PortalSharePairStatus } from '../mpc' import type { IWalletManager, WalletManagerDependencies } from './types' export type { IWalletManager, WalletManagerDependencies } export class WalletManager implements IWalletManager { private readonly api: IPortalApi private readonly mpc: PortalMpc private readonly keychain: KeychainAdapter constructor({ api, mpc, keychain }: WalletManagerDependencies) { this.api = api this.mpc = mpc this.keychain = keychain } public async create( progress?: ProgressCallback, ): Promise { return await this.mpc.generate(progress) } public async backup( method: BackupMethods, progress?: ProgressCallback, backupConfig: BackupConfigs = {}, ): Promise { return await this.mpc.backup(method, progress, backupConfig) } public async recover( cipherText: string = '', method: BackupMethods, progress?: ProgressCallback, backupConfig: BackupConfigs = {}, ): Promise { return await this.mpc.recover(cipherText, method, progress, backupConfig) } public async doesExist(chainId?: string): Promise { const client = await this.api.getClient() if (!chainId) { const hasOneValidSigningShare = client.wallets?.some((wallet) => wallet.signingSharePairs?.some( (share) => share.status === PortalSharePairStatus.COMPLETED, ), ) return hasOneValidSigningShare } const namespace = chainId.split(':')[0] switch (namespace) { case CHAIN_NAMESPACES.STELLAR: case CHAIN_NAMESPACES.SOLANA: { const ed25519Wallet = client?.wallets?.find( (wallet) => wallet.curve === PortalCurve.ED25519, ) const hasOneValidSigningShare = ed25519Wallet?.signingSharePairs?.some( (share) => share.status === PortalSharePairStatus.COMPLETED, ) return !!hasOneValidSigningShare } default: { const secp256k1Wallet = client?.wallets?.find( (wallet) => wallet.curve === PortalCurve.SECP256K1, ) const hasOneValidSigningShare = secp256k1Wallet?.signingSharePairs?.some( (share) => share.status === PortalSharePairStatus.COMPLETED, ) return !!hasOneValidSigningShare } } } public async isOnDevice(chainId?: string): Promise { const walletExists = await this.doesExist(chainId) if (!walletExists) { return false } let shares: Shares try { shares = await this.keychain.getShares() } catch (error) { if ( error instanceof Error && error.message.includes('No dkgResult found') ) { return false } throw error } if (!chainId) { return Object.values(shares).some((share) => !!share?.id) } const namespace = chainId.split(':')[0] switch (namespace) { case CHAIN_NAMESPACES.STELLAR: case CHAIN_NAMESPACES.SOLANA: { return !!shares.ed25519?.id } default: { return !!shares.secp256k1?.id } } } public async isBackedUp(chainId?: string): Promise { const client = await this.api.getClient() if (!chainId) { return client.wallets.some((wallet) => wallet.backupSharePairs.some( (share) => share.status === PortalSharePairStatus.COMPLETED, ), ) } const namespace = chainId.split(':')[0] switch (namespace) { case CHAIN_NAMESPACES.STELLAR: case CHAIN_NAMESPACES.SOLANA: { const ed25519Wallet = client?.wallets?.find( (wallet) => wallet.curve === PortalCurve.ED25519, ) const hasOneValidBackupShare = ed25519Wallet?.backupSharePairs?.some( (share) => share.status === PortalSharePairStatus.COMPLETED, ) return !!hasOneValidBackupShare } default: { const secp256k1Wallet = client?.wallets?.find( (wallet) => wallet.curve === PortalCurve.SECP256K1, ) const hasOneValidBackupShare = secp256k1Wallet?.backupSharePairs?.some( (share) => share.status === PortalSharePairStatus.COMPLETED, ) return !!hasOneValidBackupShare } } } public async isRecoverable(chainId?: string): Promise { const methods = await this.availableRecoveryMethods(chainId) return methods?.length > 0 } public async availableRecoveryMethods( chainId?: string, ): Promise { const client = await this.api.getClient() if (!chainId) { const methods = new Set( client.wallets.flatMap((wallet) => wallet.backupSharePairs .filter((share) => share.status === PortalSharePairStatus.COMPLETED) .map((share) => share.backupMethod.toLowerCase() as BackupMethods), ), ) return Array.from(methods) } const namespace = chainId.split(':')[0] switch (namespace) { case CHAIN_NAMESPACES.STELLAR: case CHAIN_NAMESPACES.SOLANA: { const ed25519Wallet = client?.wallets?.find( (wallet) => wallet.curve === PortalCurve.ED25519, ) if (!ed25519Wallet) return [] const methods = new Set( ed25519Wallet.backupSharePairs .filter((share) => share.status === PortalSharePairStatus.COMPLETED) .map((share) => share.backupMethod.toLowerCase() as BackupMethods), ) return Array.from(methods) } default: { const secp256k1Wallet = client?.wallets?.find( (wallet) => wallet.curve === PortalCurve.SECP256K1, ) if (!secp256k1Wallet) return [] const methods = new Set( secp256k1Wallet.backupSharePairs .filter((share) => share.status === PortalSharePairStatus.COMPLETED) .map((share) => share.backupMethod.toLowerCase() as BackupMethods), ) return Array.from(methods) } } } public async getSigningSharesMetadata( chainId?: string, ): Promise { return this.api.getSigningSharesMetadata(chainId) } public async getBackupSharesMetadata( chainId?: string, ): Promise { return this.api.getBackupSharesMetadata(chainId) } public async deleteShares(): Promise { return await this.keychain.deleteShares() } /** * @deprecated The Portal SDK is now multi-wallet. * Use deleteShares() instead. */ public async deleteAddress(): Promise { return await this.keychain.deleteAddress() } /** * @deprecated The Portal SDK is now multi-wallet. * Use deleteShares() instead. */ public async deleteSigningShare(): Promise { return await this.keychain.deleteDkgResult() } }