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
JavaScript
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
};