Generate Key & Sign
This example walks through the complete flow of generating an MPC key share, deriving an EVM address from it, building a transaction, and signing it with the MPC node network — all without the private key ever leaving the nodes.
Workflow canvas​
GENERATE_KEY_SHARE (curve: SECP256k1, threshold: 2)
│ keyId, rootPublicKey
â–¼
GET_EVM_DERIVATION_PATH (addressIndex: 0)
│ derivationPath
â–¼
COMPUTE_PUBLIC_KEY (keyId, derivationPath)
│ publicKey
â–¼
COMPUTE_EVM_ADDRESS (publicKey)
│ address
â–¼
BUILD_EVM_TRANSACTION (from: address, to: recipient, value: ...)
│ unsignedTransaction, serializedHash
â–¼
SIGN_WITH_KEY_SHARE (keyId, derivationPath, messageHash: serializedHash)
│ signature
â–¼
SIGN_EVM_TRANSACTION (unsignedTransaction, signature)
│ signedTransaction
â–¼
BROADCAST_EVM_TRANSACTION
│ txHash
TypeScript example​
import { WorkspaceClient, ComponentModule } from 'caller-sdk';
const workspace = new WorkspaceClient({ apiKey: process.env.WR_API_KEY! });
// Step 1: Generate a new MPC key share
// IMPORTANT: Store keyId permanently — losing it means losing the key forever.
const key = await workspace.call(ComponentModule.GENERATE_KEY_SHARE, {
curve: 'SECP256k1',
threshold: 2,
}).promise();
const { keyId, rootPublicKey } = key;
console.log('keyId:', keyId); // e.g. "3f8a2b1c-..."
console.log('rootPublicKey:', rootPublicKey);
// Persist keyId to your database before proceeding
await db.saveKeyId(keyId);
// Step 2: Derive the BIP-44 path for account 0, address index 0
const pathResult = await workspace.call(ComponentModule.GET_EVM_DERIVATION_PATH, {
addressIndex: 0,
accountIndex: 0,
changeIndex: 0,
}).promise();
const { derivationPath } = pathResult;
// Step 3: Compute the public key at this derivation path
const pubKeyResult = await workspace.call(ComponentModule.COMPUTE_PUBLIC_KEY, {
keyId,
derivationPath,
}).promise();
const { publicKey } = pubKeyResult;
console.log('publicKey:', publicKey); // compressed hex, 33 bytes
// Step 4: Derive the EVM address from the public key
const addressResult = await workspace.call(ComponentModule.COMPUTE_EVM_ADDRESS, {
publicKey,
}).promise();
const { address } = addressResult;
console.log('EVM address:', address); // e.g. "0xAbCd..."
// Step 5: Build an EVM transaction (sending 0.01 ETH to a recipient)
const buildResult = await workspace.call(ComponentModule.BUILD_EVM_TRANSACTION, {
from: address,
to: '0xRecipientAddress...',
value: '0x2386F26FC10000', // 0.01 ETH in wei (hex)
data: null,
chainId: 1, // Ethereum mainnet
gasLimit: null, // auto-estimated
gasPrice: null, // auto-estimated
nonce: null, // auto-fetched
jsonRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
}).promise();
const { unsignedTransaction, serializedHash } = buildResult;
// Step 6: Sign the transaction hash with the MPC key share
const signResult = await workspace.call(ComponentModule.SIGN_WITH_KEY_SHARE, {
keyId,
derivationPath,
messageHash: serializedHash,
}).promise();
const { signature } = signResult;
// Step 7: Apply the signature to the unsigned transaction
const signedResult = await workspace.call(ComponentModule.SIGN_EVM_TRANSACTION, {
unsignedTransaction,
signature,
}).promise();
const { signedTransaction } = signedResult;
// Step 8: Broadcast to the network
const broadcastResult = await workspace.call(ComponentModule.BROADCAST_EVM_TRANSACTION, {
signedTransaction,
jsonRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
}).promise();
console.log('Transaction hash:', broadcastResult.txHash);
Notes​
- The
keyIdreturned byGENERATE_KEY_SHAREis the sole identifier for your distributed key shares. Persist it to durable storage (database, secrets manager) before doing anything else. COMPUTE_PUBLIC_KEYcan be called with any MPC server holding a share — it does not require the signing threshold.SIGN_WITH_KEY_SHARErequires at leastthresholdservers listed in itsserversconfig to participate in the signing ceremony.- The
serializedHashfromBUILD_EVM_TRANSACTIONis the EIP-155 transaction hash — pass it directly asmessageHashtoSIGN_WITH_KEY_SHARE.