Skip to main content

Create an MPC Sign

tip

Before diving into this tutorial, please ensure you already have an asset wallet. You can also refer to Create a Wallet to create your own wallets.

An MPC Sign task is a type of transaction. More information can be found in Transaction Task.

Using this guide, you can create an MPC Sign task. The steps to do so are as follows:

  1. Create an unsigned transaction
  2. Serialize the transaction to generate the transaction hash
  3. Call the API to create an MPC Sign task
  4. Approve and sign the transaction
  5. Obtain a transaction signature
  6. Create a signed transaction
  7. Broadcast the transaction
caution
  1. Safeheron's MPC protocol now supports the Secp256k1 and Ed25519 signature algorithm. We will add support for BLS, Schnorr, and other signature algorithms in the future.
  2. There is a security risk associated with MPC Sign tasks, as improper operations may result in the loss of assets. We suggest that you sign transaction via the Web3 Sign Task if possible.

Initiate an MPC Sign Task

Using Sepolia Token USDT as an example, you can transfer USDT from Wallet A to a new address by calling transfer of the USDT contract.The Sepolia Testnet uses the Secp256k1 signature algorithm, so even if Safeheron does not support USDT, you can still sign this token's transaction using MPC Sign. Example data is as follows:

ItemData
Account Key of Wallet Aaccount4b8d2c00520646c8862b68420aa1bc55
EVM Address of Wallet A0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022
Contract Address of UDST0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0
New Receiving Address0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5
Method Calledtransfer
Amount Transferred10000

Create an unsigned transaction

// import block
import { providers, utils, Contract, UnsignedTransaction, BigNumber } from 'ethers';
import { ERC20_ABI } from './abi';

const sendAddress = "0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022";
const contractAddress = "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0";
const contractMethod = "transfer";
const recipientAddress = "0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5";

const provider = new providers.InfuraProvider('sepolia');
const ERC20 = new Contract(contractAddress, ERC20_ABI, provider);

// Encode ERC20 tx data
const amount = utils.parseUnits("10000", 18);
const data = ERC20.interface.encodeFunctionData(contractMethod,
[recipientAddress, amount]);

// Get chainId from network
const chainId = (await provider.getNetwork()).chainId;
const nonce = await provider.getTransactionCount(account.address);

// Estimate gas
const gasLimit = await provider.estimateGas({
from: sendAddress,
to: contractAddress,
value: 0,
data: data,
});

// Estimate maxFeePerGas, we assume maxPriorityFeePerGas's value is 2(gwei).
// The baseFeePerGas is recommended to be 2 times the latest block's baseFeePerGas value.
// maxFeePerGas must not less than baseFeePerGas + maxPriorityFeePerGas
const maxPriorityFeePerGas = utils.parseUnits('2', 'gwei');
const latestBlock = await provider.getBlock('latest');
const suggestBaseFee = latestBlock.baseFeePerGas?.mul(2);
const maxFeePerGas = suggestBaseFee?.add(maxPriorityFeePerGas);

// Create tx object
const unsignedTransaction: UnsignedTransaction = {
to: contractAddress,
value: 0,
data,
nonce,
chainId,
type: 2,
maxPriorityFeePerGas,
maxFeePerGas,
gasLimit,
};

Serialize the transaction to generate the transaction hash

//import block
import { utils } from 'ethers';

// serialize unsignedTransaction and compute the hash value
const serialize = utils.serializeTransaction(unsignedTransaction);
const unsignedHash = utils.keccak256(serialize);

Call the API to create an MPC Sign task

Request Parameters

interface CreateMpcSignRequest {
customerRefId: string;
sourceAccountKey: string;
signAlg: string;
hashs: Array<{
hash: string;
note?:string;
}>;
}

const createRequest: CreateMpcSignRequest = {
customerRefId: uuid(),
sourceAccountKey: 'account4b8d2c00520646c8862b68420aa1bc55',
signAlg: 'Secp256k1',
hashs: [{
// Assume unsignedHash is 0xd061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69
// 32-byte hex string without '0x' prefix
hash: '0xd061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69'.substring(2),
}]
}

Request the Interface

interface CreateMpcSignResponse {
txKey: string;
}

const createResponse = await client.doRequest<CreateMpcSignRequest, CreateMpcSignResponse>('/v1/transactions/mpcsign/create', createRequest);

Example Response Data

tip

txKey is a unique identifier for a transaction.

{
"txKey": "tx46461daa9b7a4612abce99e7ce598844"
}

Approve and sign the transaction

Once an MPC Sign task has been created, the Safeheron API will proceed with either manual or automated approval via the API Co-Signer based on your policies.

You will receive a push notification on the Safeheron App of the transaction created which will be displayed in the "Pending" with "Pending Approval" status. When the approval completes, the last approver will sign the transaction on Safeheron App.

Obtain a transaction signature

The created transaction task will undergo approval, signing, broadcasting, on-chain confirmation, etc. You can track the status of all transactions here. We also suggest using Webhook to acquire time-sensitive status updates and signature results. You can also track the status of transactions and acquire signature results from Retrieve an MPC Sign Transaction.

tip

You can track transaction tasks with webhook by configuring the Webhook URL in Settings -> API on Safeheron Web Console.

Create a signed transaction

Assuming the above steps have been completed, the signature result would be:

b3d5b45dec592d6ca60455f2926e06e5ff1c81cc4115d44d4b4f9953e6260aee55bc80e341977ab77713c80b31d960be01e09bb19014db49484db45859c77fda00
// import block
import { splitSignature } from '@ethersproject/bytes';

const sig = 'b3d5b45dec592d6ca60455f2926e06e5ff1c81cc4115d44d4b4f9953e6260aee55bc80e341977ab77713c80b31d960be01e09bb19014db49484db45859c77fda00';
// Split sig into R, S, V
const r = sig.substring(0, 64);
const s = sig.substring(64, 128);
const v = sig.substring(128);
const signature = {
r: '0x' + r,
s: '0x' + s,
recoveryParam: parseInt(v, 16),
};

const signature = splitSignature(signature);
// serialize with signature
// This unsignedTransaction is re-created when you obtain the signature result. And, the creation method is the same as "Create an unsigned transaction",
// Please note that all of the data used for 2 creation processes shall be the same.
const signedTransaction = utils.serializeTransaction(unsignedTransaction, signature);

Broadcast the transaction

// import block
import { providers } from 'ethers';

const provider = new providers.InfuraProvider('sepolia');
const response = await provider.sendTransaction(signedTransaction);

For Your Reference

The code described in this tutorial is open-sourced on Safeheron's GitHub. For the full source code: