UNPKG

solana-mpc-tss-lib

Version:

A comprehensive TypeScript library for Solana Multi-Party Computation (MPC) and Threshold Signature Schemes (TSS) - Compatible with ZenGo-X/solana-tss

699 lines (688 loc) 23 kB
var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/mpc/MPCKeypair.ts var MPCKeypair = class { constructor(mpcSigner) { this.publicKey = mpcSigner.publicKey; this.mpcSigner = mpcSigner; this.secretKey = new Uint8Array(32); } sign(message) { return __async(this, null, function* () { return this.mpcSigner.sign(message); }); } signTransaction(tx) { return __async(this, null, function* () { const msg = tx.serializeMessage(); const sig = yield this.sign(msg); tx.addSignature(this.publicKey, Buffer.from(sig)); return tx; }); } signAllTransactions(txs) { return __async(this, null, function* () { return Promise.all(txs.map((tx) => this.signTransaction(tx))); }); } }; // src/mpc/ed25519.ts import { PublicKey } from "@solana/web3.js"; import * as nacl from "tweetnacl"; function createMPCSigner() { return __async(this, null, function* () { try { const wasmPath = ["..", "pkg", "ed25519_tss_wasm"].join("/"); const wasmModule = yield Function("p", "return import(p)")(wasmPath); if (typeof wasmModule.default === "function") { yield wasmModule.default(); } const kp = wasmModule.Keypair.generate(); const publicKeyBytes = Uint8Array.from(kp.public_key()); const secretKeyBytes = Uint8Array.from(kp.secret_key()); const publicKey = new PublicKey(publicKeyBytes); return { publicKey, sign: (message) => __async(null, null, function* () { const signature = wasmModule.sign(Uint8Array.from(message), Uint8Array.from(secretKeyBytes)); return Uint8Array.from(signature); }) }; } catch (error) { console.warn("WASM module not found, falling back to tweetnacl"); const keypair = nacl.sign.keyPair(); const publicKey = new PublicKey(keypair.publicKey); return { publicKey, sign: (message) => __async(null, null, function* () { const signature = nacl.sign.detached(message, keypair.secretKey); return signature; }) }; } }); } function createMPCSignerFromSecretKey(secretKeyBytes) { return __async(this, null, function* () { try { const wasmPath = ["..", "pkg", "ed25519_tss_wasm"].join("/"); const wasmModule = yield Function("p", "return import(p)")(wasmPath); if (typeof wasmModule.default === "function") { yield wasmModule.default(); } const seed32 = secretKeyBytes.length === 32 ? secretKeyBytes : secretKeyBytes.slice(0, 32); const naclKeypair = nacl.sign.keyPair.fromSeed(seed32); const derivedPublicKey = new PublicKey(naclKeypair.publicKey); return { publicKey: derivedPublicKey, sign: (message) => __async(null, null, function* () { const signature = wasmModule.sign(Uint8Array.from(message), Uint8Array.from(seed32)); return Uint8Array.from(signature); }) }; } catch (error) { const fullSecret = secretKeyBytes.length === 64 ? secretKeyBytes : nacl.sign.keyPair.fromSeed(secretKeyBytes.length === 32 ? secretKeyBytes : secretKeyBytes.slice(0, 32)).secretKey; const keypair = nacl.sign.keyPair.fromSecretKey(fullSecret); const publicKey = new PublicKey(keypair.publicKey); return { publicKey, sign: (message) => __async(null, null, function* () { const signature = nacl.sign.detached(message, keypair.secretKey); return signature; }) }; } }); } // src/solana/tx.ts import { SystemProgram, Transaction } from "@solana/web3.js"; function createTransferTx(connection, from, to, lamports) { return __async(this, null, function* () { const { blockhash } = yield connection.getLatestBlockhash(); const tx = new Transaction({ recentBlockhash: blockhash, feePayer: from }).add( SystemProgram.transfer({ fromPubkey: from, toPubkey: to, lamports }) ); return tx; }); } // src/tss/wallet.ts import { Connection as Connection2, PublicKey as PublicKey3, Keypair, clusterApiUrl, LAMPORTS_PER_SOL } from "@solana/web3.js"; var TSSWallet = class { constructor(network = "devnet") { this.network = network; this.connection = new Connection2(clusterApiUrl(network), "confirmed"); } /** * Generate a new TSS keypair * Equivalent to: solana-tss generate */ generateKeypair() { return __async(this, null, function* () { try { const mpcSigner = yield createMPCSigner(); return { publicKey: mpcSigner.publicKey, secretKey: new Uint8Array(32) // MPC manages the actual secret }; } catch (error) { const keypair = Keypair.generate(); return { publicKey: keypair.publicKey, secretKey: keypair.secretKey }; } }); } /** * Check the balance of an address * Equivalent to: solana-tss balance <address> */ getBalance(publicKey) { return __async(this, null, function* () { const balance = yield this.connection.getBalance(publicKey); return balance / LAMPORTS_PER_SOL; }); } /** * Request an airdrop from the faucet (devnet/testnet only) * Equivalent to: solana-tss airdrop <address> <amount> */ requestAirdrop(publicKey, amount) { return __async(this, null, function* () { if (this.network === "mainnet-beta") { throw new Error("Airdrop not available on mainnet"); } const lamports = amount * LAMPORTS_PER_SOL; const signature = yield this.connection.requestAirdrop(publicKey, lamports); yield this.connection.confirmTransaction(signature); return signature; }); } /** * Aggregate multiple public keys into a single multisig address * Equivalent to: solana-tss aggregate-keys <key1> <key2> ... <keyN> */ aggregateKeys(participantKeys, threshold) { const combinedKey = this.combinePublicKeys(participantKeys); return { aggregatedPublicKey: combinedKey, participantKeys, threshold: threshold || participantKeys.length // n-of-n by default }; } /** * Get recent blockhash for transaction signing * Equivalent to: solana-tss recent-block-hash */ getRecentBlockhash() { return __async(this, null, function* () { const { blockhash } = yield this.connection.getLatestBlockhash(); return blockhash; }); } /** * Switch to a different Solana network */ switchNetwork(network) { this.network = network; this.connection = new Connection2(clusterApiUrl(network), "confirmed"); } /** * Get current network */ getCurrentNetwork() { return this.network; } /** * Get connection instance */ getConnection() { return this.connection; } /** * Private helper to combine public keys (simplified implementation) * In production, this would use proper TSS key aggregation schemes */ combinePublicKeys(keys) { if (keys.length === 0) { throw new Error("Cannot aggregate empty key list"); } if (keys.length === 1) { return keys[0]; } let combined = new Uint8Array(32); for (const key of keys) { const keyBytes = key.toBytes(); for (let i = 0; i < 32; i++) { combined[i] ^= keyBytes[i]; } } return new PublicKey3(combined); } /** * Validate a public key string */ static validatePublicKey(keyString) { try { return new PublicKey3(keyString); } catch (error) { throw new Error(`Invalid public key format: ${keyString}`); } } /** * Format balance for display */ static formatBalance(lamports) { const sol = lamports / LAMPORTS_PER_SOL; return `${sol.toFixed(9)} SOL`; } }; // src/tss/signing.ts import { PublicKey as PublicKey4 } from "@solana/web3.js"; import * as nacl2 from "tweetnacl"; var TSSSigningService = class { constructor(connection) { this.connection = connection; } /** * Send a transaction using a single private key (non-TSS) * Equivalent to: solana-tss send-single */ sendSingle(fromSecretKey, to, amount, memo) { return __async(this, null, function* () { const mpcSigner = yield createMPCSigner(); const tx = yield createTransferTx( this.connection, mpcSigner.publicKey, to, amount ); if (memo) { tx.add({ keys: [], programId: new PublicKey4("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), data: Buffer.from(memo, "utf8") }); } const signature = yield mpcSigner.sign(tx.serializeMessage()); tx.addSignature(mpcSigner.publicKey, Buffer.from(signature)); const txId = yield this.connection.sendTransaction(tx, []); yield this.connection.confirmTransaction(txId); return txId; }); } /** * Step 1 of aggregate signing: Generate nonce and commitment * Equivalent to: solana-tss agg-send-step-one */ aggregateSignStepOne(participantSecretKey, transactionDetails) { return __async(this, null, function* () { const secretNonce = nacl2.randomBytes(32); const publicNonce = nacl2.hash(secretNonce).slice(0, 32); const participantKey = this.derivePublicKey(participantSecretKey); return { secretNonce, publicNonce, participantKey }; }); } /** * Step 2 of aggregate signing: Create partial signature * Equivalent to: solana-tss agg-send-step-two */ aggregateSignStepTwo(stepOneData, participantSecretKey, transactionDetails, allPublicNonces) { return __async(this, null, function* () { const tx = yield this.createTransactionFromDetails(transactionDetails); const messageToSign = tx.serializeMessage(); const aggregatedNonce = this.aggregateNonces(allPublicNonces); const partialSignature = this.createPartialSignature( messageToSign, participantSecretKey, stepOneData.secretNonce, aggregatedNonce ); return { partialSignature, publicNonce: stepOneData.publicNonce, participantKey: stepOneData.participantKey }; }); } /** * Aggregate all partial signatures and broadcast transaction * Equivalent to: solana-tss aggregate-signatures-and-broadcast */ aggregateSignaturesAndBroadcast(partialSignatures, transactionDetails, aggregateWallet) { return __async(this, null, function* () { if (partialSignatures.length < aggregateWallet.threshold) { throw new Error(`Insufficient signatures: ${partialSignatures.length}/${aggregateWallet.threshold}`); } const tx = yield this.createTransactionFromDetails(transactionDetails); const completeSignature = this.aggregatePartialSignatures( partialSignatures, aggregateWallet ); tx.addSignature(aggregateWallet.aggregatedPublicKey, Buffer.from(completeSignature.signature)); const txId = yield this.connection.sendTransaction(tx, []); yield this.connection.confirmTransaction(txId); return txId; }); } /** * Create a transaction from transaction details */ createTransactionFromDetails(details) { return __async(this, null, function* () { const tx = yield createTransferTx( this.connection, details.from, details.to, details.amount ); tx.recentBlockhash = details.recentBlockhash; if (details.memo) { tx.add({ keys: [], programId: new PublicKey4("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"), data: Buffer.from(details.memo, "utf8") }); } return tx; }); } /** * Derive public key from secret key */ derivePublicKey(secretKey) { let fullSecretKey = secretKey; if (secretKey.length === 32) { const keypair2 = nacl2.sign.keyPair.fromSeed(secretKey); fullSecretKey = keypair2.secretKey; } const keypair = nacl2.sign.keyPair.fromSecretKey(fullSecretKey); return new PublicKey4(keypair.publicKey); } /** * Aggregate nonces for TSS signing */ aggregateNonces(nonces) { let aggregated = new Uint8Array(32); for (const nonce of nonces) { for (let i = 0; i < 32; i++) { aggregated[i] ^= nonce[i]; } } return aggregated; } /** * Create a partial signature for TSS */ createPartialSignature(message, secretKey, secretNonce, aggregatedNonce) { let seedKey = secretKey; if (secretKey.length === 64) { seedKey = secretKey.slice(0, 32); } else if (secretKey.length !== 32) { seedKey = new Uint8Array(32); seedKey.set(secretKey.slice(0, Math.min(32, secretKey.length))); } const keypair = nacl2.sign.keyPair.fromSeed(seedKey); const signature = nacl2.sign.detached(message, keypair.secretKey); return signature; } /** * Aggregate partial signatures into a complete signature */ aggregatePartialSignatures(partialSignatures, aggregateWallet) { let aggregatedSig = new Uint8Array(64); for (const partial of partialSignatures) { for (let i = 0; i < 64; i++) { aggregatedSig[i] ^= partial.partialSignature[i]; } } return { signature: aggregatedSig, publicKey: aggregateWallet.aggregatedPublicKey, transaction: new Uint8Array() // Would contain serialized transaction }; } /** * Verify a partial signature */ verifyPartialSignature(signature, message) { try { return nacl2.sign.detached.verify( message, signature.signature, signature.signer.toBytes() ); } catch (error) { return false; } } }; // src/tss/cli.ts var TSSCli = class { constructor(network = "devnet") { this.wallet = new TSSWallet(network); this.signingService = new TSSSigningService(this.wallet.getConnection()); } /** * Generate a pair of keys * solana-tss generate */ generate() { return __async(this, null, function* () { const keypair = yield this.wallet.generateKeypair(); return { publicKey: keypair.publicKey.toString(), secretKey: Buffer.from(keypair.secretKey).toString("hex") }; }); } /** * Check the balance of an address * solana-tss balance <address> */ balance(address) { return __async(this, null, function* () { const publicKey = TSSWallet.validatePublicKey(address); return yield this.wallet.getBalance(publicKey); }); } /** * Request an airdrop from a faucet * solana-tss airdrop <address> <amount> */ airdrop(address, amount) { return __async(this, null, function* () { const publicKey = TSSWallet.validatePublicKey(address); return yield this.wallet.requestAirdrop(publicKey, amount); }); } /** * Send a transaction using a single private key * solana-tss send-single <from_secret> <to> <amount> [memo] */ sendSingle(fromSecretHex, to, amount, memo) { return __async(this, null, function* () { const fromSecret = new Uint8Array(Buffer.from(fromSecretHex, "hex")); const toPublicKey = TSSWallet.validatePublicKey(to); return yield this.signingService.sendSingle( fromSecret, toPublicKey, amount, memo ); }); } /** * Aggregate a list of addresses into a single address * solana-tss aggregate-keys <key1> <key2> ... <keyN> */ aggregateKeys(keyStrings, threshold) { const keys = keyStrings.map((keyStr) => TSSWallet.validatePublicKey(keyStr)); const aggregateWallet = this.wallet.aggregateKeys(keys, threshold); return { aggregatedPublicKey: aggregateWallet.aggregatedPublicKey.toString(), participantKeys: aggregateWallet.participantKeys.map((k) => k.toString()), threshold: aggregateWallet.threshold }; } /** * Start aggregate signing * solana-tss agg-send-step-one <participant_secret> <to> <amount> <network> [memo] [recent_block_hash] */ aggregateSignStepOne(participantSecretHex, to, amount, memo, recentBlockhash) { return __async(this, null, function* () { const participantSecret = new Uint8Array(Buffer.from(participantSecretHex, "hex")); const toPublicKey = TSSWallet.validatePublicKey(to); const fromPublicKey = TSSWallet.validatePublicKey(to); const blockHash = recentBlockhash || (yield this.wallet.getRecentBlockhash()); const transactionDetails = { amount, to: toPublicKey, from: fromPublicKey, network: this.wallet.getCurrentNetwork(), memo, recentBlockhash: blockHash }; const stepOneData = yield this.signingService.aggregateSignStepOne( participantSecret, transactionDetails ); return { secretNonce: Buffer.from(stepOneData.secretNonce).toString("hex"), publicNonce: Buffer.from(stepOneData.publicNonce).toString("hex"), participantKey: stepOneData.participantKey.toString() }; }); } /** * Print the hash of a recent block * solana-tss recent-block-hash */ recentBlockHash() { return __async(this, null, function* () { return yield this.wallet.getRecentBlockhash(); }); } /** * Step 2 of aggregate signing * solana-tss agg-send-step-two <step_one_data> <participant_secret> <to> <amount> <network> <all_public_nonces> [memo] [recent_block_hash] */ aggregateSignStepTwo(stepOneDataJson, participantSecretHex, to, amount, allPublicNoncesHex, memo, recentBlockhash) { return __async(this, null, function* () { const stepOneData = { secretNonce: new Uint8Array(Buffer.from(JSON.parse(stepOneDataJson).secretNonce, "hex")), publicNonce: new Uint8Array(Buffer.from(JSON.parse(stepOneDataJson).publicNonce, "hex")), participantKey: TSSWallet.validatePublicKey(JSON.parse(stepOneDataJson).participantKey) }; const participantSecret = new Uint8Array(Buffer.from(participantSecretHex, "hex")); const toPublicKey = TSSWallet.validatePublicKey(to); const fromPublicKey = stepOneData.participantKey; const blockHash = recentBlockhash || (yield this.wallet.getRecentBlockhash()); const transactionDetails = { amount, to: toPublicKey, from: fromPublicKey, network: this.wallet.getCurrentNetwork(), memo, recentBlockhash: blockHash }; const allPublicNonces = allPublicNoncesHex.map( (hex) => new Uint8Array(Buffer.from(hex, "hex")) ); const stepTwoData = yield this.signingService.aggregateSignStepTwo( stepOneData, participantSecret, transactionDetails, allPublicNonces ); return { partialSignature: Buffer.from(stepTwoData.partialSignature).toString("hex"), publicNonce: Buffer.from(stepTwoData.publicNonce).toString("hex"), participantKey: stepTwoData.participantKey.toString() }; }); } /** * Aggregate all the partial signatures together and send transaction * solana-tss aggregate-signatures-and-broadcast <partial_signatures> <transaction_details> <aggregate_wallet> */ aggregateSignaturesAndBroadcast(partialSignaturesJson, transactionDetailsJson, aggregateWalletJson) { return __async(this, null, function* () { const partialSignatures = JSON.parse(partialSignaturesJson).map((sig) => ({ partialSignature: new Uint8Array(Buffer.from(sig.partialSignature, "hex")), publicNonce: new Uint8Array(Buffer.from(sig.publicNonce, "hex")), participantKey: TSSWallet.validatePublicKey(sig.participantKey) })); const txDetailsRaw = JSON.parse(transactionDetailsJson); const transactionDetails = { amount: txDetailsRaw.amount, to: TSSWallet.validatePublicKey(txDetailsRaw.to), from: TSSWallet.validatePublicKey(txDetailsRaw.from), network: txDetailsRaw.network, memo: txDetailsRaw.memo, recentBlockhash: txDetailsRaw.recentBlockhash }; const aggregateWalletRaw = JSON.parse(aggregateWalletJson); const aggregateWallet = { aggregatedPublicKey: TSSWallet.validatePublicKey(aggregateWalletRaw.aggregatedPublicKey), participantKeys: aggregateWalletRaw.participantKeys.map((k) => TSSWallet.validatePublicKey(k)), threshold: aggregateWalletRaw.threshold }; return yield this.signingService.aggregateSignaturesAndBroadcast( partialSignatures, transactionDetails, aggregateWallet ); }); } /** * Switch to a different network */ switchNetwork(network) { this.wallet.switchNetwork(network); this.signingService = new TSSSigningService(this.wallet.getConnection()); } /** * Get current network */ getCurrentNetwork() { return this.wallet.getCurrentNetwork(); } /** * Format balance for display */ static formatBalance(balance) { return `${balance.toFixed(9)} SOL`; } /** * Helper to print help information */ static printHelp() { return ` Solana TSS Library v1.0.0 A TypeScript library for managing Solana TSS wallets USAGE: Available methods in TSSCli class: METHODS: generate() Generate a pair of keys balance(address) Check the balance of an address airdrop(address, amount) Request an airdrop from a faucet sendSingle(fromSecret, to, amount, memo?) Send a transaction using a single private key aggregateKeys(keys, threshold?) Aggregate a list of addresses into a single address aggregateSignStepOne(participantSecret, to, amount, memo?, recentBlockhash?) Start aggregate signing recentBlockHash() Get the hash of a recent block aggregateSignStepTwo(stepOneData, participantSecret, to, amount, allPublicNonces, memo?, recentBlockhash?) Step 2 of aggregate signing aggregateSignaturesAndBroadcast(partialSignatures, transactionDetails, aggregateWallet) Aggregate signatures and broadcast transaction NETWORKS: mainnet, devnet, testnet (default: devnet) `; } }; // src/index.ts import { PublicKey as PublicKey5, Connection as Connection4, clusterApiUrl as clusterApiUrl2 } from "@solana/web3.js"; export { Connection4 as Connection, MPCKeypair, PublicKey5 as PublicKey, TSSCli, TSSSigningService, TSSWallet, clusterApiUrl2 as clusterApiUrl, createMPCSigner, createMPCSignerFromSecretKey, createTransferTx };