使用 MPC Sign
提示
学习本教程的前提是您已经拥有了一个资产钱包,您也可以查看 创建一个钱包
教程学习如何创建钱包。
MPC Sign 签名任务是交易任务的一种,点击了解交易任务。
通过此教程,您将发起一笔 MPC Sign 签名任务,包括以下内容:
- 构建未签名交易
- 序列化交易生成交易 Hash
- 调用 API 创建 MPC Sign 签名任务
- 交易审核和签名
- 获取交易签名结果
- 构建签名交易
- 广播交易
警告
- 目前 MPC 协议支持 Secp256k1、Ed25519 签名算法,未来会增加对 BLS、Schnorr 等签名算法的支持。
- MPC Sign 签名任务存在一定的安全风险,操作不当可能对您造成资产损失,推荐您尽可能采用 Web3 签名任务来完成交易签名。
发起 MPC Sign 签名任务
我们以 Sepolia Token USDT 为例,调用 USDT 合约的 transfer
方法,从钱包 A 转出一笔 USDT Token 到一个陌生地址上。尽管 Safeheron 不支持 Sepolia USDT Token,但由于 Sepolia 测试网采用的是 Secp256k1 签名算法,所以可以使用 MPC Sign 来完成交易签名。假设场景数据如下:
数据项 | 值 |
---|---|
钱包 A 的 accountKey | account4b8d2c00520646c8862b68420aa1bc55 |
钱包 A 的 EVM 地址 | 0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022 |
Sepolia Token USDT 合约地址 | 0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0 |
接收 USDT Token 的陌生地址 | 0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5 |
调用的合约方法 | transfer |
转账 Token 数量 | 10000 |
构建未签名交易
- 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;
序列化交易生成交易 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);
调用 API 创建 MPC Sign 签名任务
请求参数
- 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: [{
// 假设得到的 unsignedHash 值为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: [{
// 假设得到的 unsignedHash 值为0xd061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69
// 32-byte hex string without '0x' prefix
hash: '0xd061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69'.substring(2),
}]
}
CreateMpcSignRequest.java
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();
// 假设得到的 unsignedHash 值为0xd061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69
// without '0x' prefix
hashItem.setHash("d061e9c5891f579fd548cfd22ff29f5c642714cc7e7a9215f0071ef5a5723f69");
request.setHashs(Arrays.asList(hashItem));
请求接口
- 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);
CreateMpcSignResponse.java
public class CreateMpcSignResponse {
private String txKey;
}
CreateMpcSignResponse response = ServiceExecutor.execute(mpcSignApi.createMpcSign(request));
响应数据示例
提示
txKey
唯一代表一笔交易。
{
"txKey": "tx46461daa9b7a4612abce99e7ce598844"
}
交易任务审批和签名
MPC Sign 签名任务创建后,将根据您的策略决定,此签名任务是由人工审核签名还是由 API Co-Signer 自动化审核签名。
- 人工审核签名
- API Co-Signer 审核签名
签名任务创建后,您手机 App 会收到推送通知,并且代办任务列表会显示此笔待审核状态的交易,审核通过后,由最后一个审批人的 App 参与完成签名。
API Co-Signer 会自动获取到待审核状态的交易,并向您的业务系统发起审核请求,审核通过后,将由API Co-Signer 参与完成签名。
获取交易签名结果
MPC Sign 签名任务创建后,会经历审批,签名等过程,您可以点此查看交易任务生命周期所经历的全部状态,我们推荐您使用 Webhook
来感知签名任务状态的变化,并获取签名结果。您也可以通过查询单笔 MPC Sign 交易接口获取状态和签名结果。
提示
通过 Webhook 来感知交易任务的状态变化,需要在 Safeheron Web 控制台 API 管理页面中配置 Webhook URL。
构建签名交易
假设通过以上步骤,我们获取到的交易签名结果为:
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
// 这里的 unsignedTransaction 是获取到签名结果后重新构建的,构建方法与上文中的 "构建未签名交易" 完全一致,
// 需要注意的是,两次构建过程中使用的所有数据必须保持完全一致。
const signedTransaction = utils.serializeTransaction(unsignedTransaction, signature);
sig := "b3d5b45dec592d6ca60455f2926e06e5ff1c81cc4115d44d4b4f9953e6260aee55bc80e341977ab77713c80b31d960be01e09bb19014db49484db45859c77fda00";
// Add 0x prefix to sig
sigByte, _ := hexutil.Decode("0x" + sig[:])
// Encode unsignedTransaction with sig
// 这里的 unsignedTransaction 是获取到签名结果后重新构建的,构建方法与上文中的 "构建未签名交易" 完全一致,
// 需要注意的是,两次构建过程中使用的所有数据必须保持完全一致。
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));
// 这里的 unsignedTransaction 是获取到签名结果后重新构建的,构建方法与上文中的 "构建未签名交易" 完全一致,
// 需要注意的是,两次构建过程中使用的所有数据必须保持完全一致。
byte[] signedMessage = TransactionEncoder.encode(unsignedTransaction, signatureData);
String signedTransaction = Numeric.toHexString(signedMessage);
广播交易
- 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();
下一步
本教程涉及到的代码,已经在 Github 开源,获取完整源代码: