Create an MPC Sign
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:
- Create an unsigned transaction
- Serialize the transaction to generate the transaction hash
- Call the API to create an MPC Sign task
- Approve and sign the transaction
- Obtain a transaction signature
- Create a signed transaction
- Broadcast the transaction
- 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.
- 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:
Item | Data |
---|---|
Account Key of Wallet A | account4b8d2c00520646c8862b68420aa1bc55 |
EVM Address of Wallet A | 0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022 |
Contract Address of UDST | 0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0 |
New Receiving Address | 0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5 |
Method Called | transfer |
Amount Transferred | 10000 |
Create an unsigned transaction
- TypeScript
- Golang
- Java
// 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,
};
// import block
import (
"context"
"crypto/ecdsa"
"encoding/json"
"math"
"math/big"
ethUnit "github.com/DeOne4eg/eth-unit-converter"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"golang.org/x/crypto/sha3"
)
const sendAddress = "0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022";
const contractAddress = "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0";
const contractMethod = "transfer(address,uint256)";
const recipientAddress = "0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5";
const amount = "10000";
client, _ = ethclient.Dial("your url");
chainId, _ := client.NetworkID(context.Background())
tokenAmount := new(big.Int)
big.NewFloat(0).Mul(big.NewFloat(amount),
big.NewFloat(math.Pow(10, float64(decimals)))).Int(tokenAmount)
fnSignature := []byte(contractMethod)
fnHash := sha3.NewLegacyKeccak256()
var data []byte
data = append(data, fnHash.Sum(nil)[:4]...)
data = append(data,
common.LeftPadBytes(common.HexToAddress(recipientAddress).Bytes(), 32)...)
data = append(data, common.LeftPadBytes(tokenAmount.Bytes(), 32)...)
// Get data from block chain: nonce, gasPrice
nonce, _ := client.PendingNonceAt(context.Background(), common.HexToAddress(sendAddress))
// Estimate gas
gasLimit, _ := client.EstimateGas(context.Background(), ethereum.CallMsg{
From: common.HexToAddress(sendAddress),
To: &common.HexToAddress(contractAddress),
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
maxPriorityFeePerGas := ethUnit.NewGWei(big.NewFloat(2)).Wei()
lastBlockHeader, _ := client.HeaderByNumber(context.Background(), nil)
baseFeeFloat := big.NewFloat(0).SetInt(lastBlockHeader.BaseFee)
suggestBaseFee := big.NewFloat(0).Mul(baseFeeFloat, big.NewFloat(2)))
maxFeePerGas, _ := big.NewFloat(0).Add(suggestBaseFee, big.NewFloat(0).SetInt(maxPriorityFeePerGas)).Int(nil)
// Create unsigned transaction
unsignedTransaction := types.NewTx(&types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
To: &common.HexToAddress(contractAddress),
Value: ethUnit.NewEther(big.NewFloat(0)).Wei(),
Data: data,
Gas: gasLimit,
GasTipCap: maxPriorityFeePerGas,
GasFeeCap: maxFeePerGas,
})
String fromAddress = "0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022";
String contractAddress = "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0";
String toAddress = "0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5";
// Get data from block chain: nonce, chainId, gasLimit and latestBlock's baseFeePerGas
EthGetTransactionCount ethGetTransactionCount = web3
.ethGetTransactionCount(fromAddress, DefaultBlockParameterName.LATEST).send();
BigInteger nonce = ethGetTransactionCount.getTransactionCount();
EthChainId ethChainId = web3.ethChainId().send();
BigDecimal tokenValue = new BigDecimal("10000").multiply(new BigDecimal(Math.pow(10, decimals.longValue())));
Function function = new Function(
"transfer",
Arrays.asList(new Address(toAddress), new Uint256(tokenValue.toBigInteger())),
Arrays.asList(new TypeReference<Type>() {
}));
String data = FunctionEncoder.encode(function);
// Estimate gas
Transaction transaction = Transaction.createEthCallTransaction(fromAddress, contractAddress, data);
EthEstimateGas gasLimit = web3.ethEstimateGas(transaction).send();
if(gasLimit.hasError()){
throw new Exception(String.format("error estimate gas:%s-%s", gasLimit.getError().getCode(), gasLimit.getError().getMessage()));
}
// Estimate maxFeePerGas, we assume maxPriorityFeePerGas's value is 2(gwei)
BigInteger maxPriorityFeePerGas = Convert.toWei("2", Convert.Unit.GWEI).toBigInteger();
EthBlock.Block latestBlock = web3.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).send().getBlock();
// The baseFeePerGas is recommended to be 2 times the latest block's baseFeePerGas value
// maxFeePerGas must not less than baseFeePerGas + maxPriorityFeePerGas
BigDecimal maxFeePerGas = new BigDecimal(latestBlock.getBaseFeePerGas())
.multiply(new BigDecimal("2"))
.add(new BigDecimal(maxPriorityFeePerGas));
// Create raw transaction
RawTransaction rawTransaction = RawTransaction.createTransaction(
ethChainId.getChainId().longValue(),
nonce,
gasLimit.getAmountUsed(),
contractAddress,
Convert.toWei(value, Convert.Unit.ETHER).toBigInteger(),
data,
maxPriorityFeePerGas,
maxFeePerGas.toBigInteger());
return rawTransaction;
Serialize the transaction to generate the transaction hash
- TypeScript
- Golang
- Java
//import block
import { utils } from 'ethers';
// serialize unsignedTransaction and compute the hash value
const serialize = utils.serializeTransaction(unsignedTransaction);
const unsignedHash = utils.keccak256(serialize);
client, _ = ethclient.Dial("your url");
chainId, _ := client.NetworkID(context.Background());
londonSigner := types.NewLondonSigner(chainId);
// Encode unsigned transaction and compute the hash value
unsignedHash := eip1559Signer.Hash(unsignedTransaction).Hex()
// Encode the transaction and compute the hash value
byte[] encodedRawTransaction = TransactionEncoder.encode(unsignedTransaction);
String unsignedHash = Numeric.toHexString(Hash.sha3(encodedRawTransaction)).substring(2);
Call the API to create an MPC Sign task
Request Parameters
- TypeScript
- Golang
- Java
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),
}]
}
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),
}]
}
public class CreateMpcSignRequest {
private String customerRefId;
private String sourceAccountKey;
private String signAlg;
private List<Hash> hashs;
static class Hash{
private String hash;
private String note;
}
}
CreateMpcSignRequest request = new CreateMpcSignRequest();
request.setCustomerRefId(UUID.randomUUID().toString());
request.setSourceAccountKey("account4b8d2c00520646c8862b68420aa1bc55");
request.setSignAlg("Secp256k1");
CreateMpcSignRequest.Hash hashItem = new CreateMpcSignRequest.Hash();
// Assume unsignedHash is 0xd061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69
// without '0x' prefix
hashItem.setHash("d061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69");
request.setHashs(Arrays.asList(hashItem));
Request the Interface
- TypeScript
- Golang
- Java
interface CreateMpcSignResponse {
txKey: string;
}
const createResponse = await client.doRequest<CreateMpcSignRequest, CreateMpcSignResponse>('/v1/transactions/mpcsign/create', createRequest);
interface CreateMpcSignResponse {
txKey: string;
}
const createResponse = await client.doRequest<CreateMpcSignRequest, CreateMpcSignResponse>('/v1/transactions/mpcsign/create', createRequest);
public class CreateMpcSignResponse {
private String txKey;
}
CreateMpcSignResponse response = ServiceExecutor.execute(mpcSignApi.createMpcSign(request));
Example Response Data
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.
- Manual Approval
- API Co-Signer Approval
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.
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
- TypeScript
- Golang
- Java
// 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);
sig := "b3d5b45dec592d6ca60455f2926e06e5ff1c81cc4115d44d4b4f9953e6260aee55bc80e341977ab77713c80b31d960be01e09bb19014db49484db45859c77fda00";
// Add 0x prefix to sig
sigByte, _ := hexutil.Decode("0x" + sig[:])
// Encode unsignedTransaction with sig
// 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.
signedTransaction, err := unsignedTransaction.WithSignature(londonSigner, sigByte)
// Encode transaction with signature
String sig = "b3d5b45dec592d6ca60455f2926e06e5ff1c81cc4115d44d4b4f9953e6260aee55bc80e341977ab77713c80b31d960be01e09bb19014db49484db45859c77fda00";
// Split sig into R, S, V
String sigR = sig.substring(0, 64);
String sigS = sig.substring(64, 128);
String sigV = sig.substring(128);
Integer v = Integer.parseInt(sigV, 16) + 27;
// Create Sign.SignatureData Object
Sign.SignatureData signatureData = new Sign.SignatureData(v.byteValue(),
Numeric.hexStringToByteArray(sigR),
Numeric.hexStringToByteArray(sigS));
// 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.
byte[] signedMessage = TransactionEncoder.encode(unsignedTransaction, signatureData);
String signedTransaction = Numeric.toHexString(signedMessage);
Broadcast the transaction
- TypeScript
- Golang
- Java
// import block
import { providers } from 'ethers';
const provider = new providers.InfuraProvider('sepolia');
const response = await provider.sendTransaction(signedTransaction);
// Send transaction
err = client.SendTransaction(context.Background(), signedTransaction)
if err != nil {
log.Fatal(err)
}
Web3j web3 = Web3j.build(new HttpService("your http url",
new OkHttpClient.Builder().build()));
// Send transaction
EthSendTransaction ethSendTransaction = web3.ethSendRawTransaction(signedTransaction).send();
For Your Reference
The code described in this tutorial is open-sourced on Safeheron's GitHub. For the full source code: