跳到主要内容

使用 MPC Sign

提示

学习本教程的前提是您已经拥有了一个资产钱包,您也可以查看 创建一个钱包 教程学习如何创建钱包。

MPC Sign 签名任务是交易任务的一种,点击了解交易任务

通过此教程,您将发起一笔 MPC Sign 签名任务,包括以下内容:

  1. 构建未签名交易
  2. 序列化交易生成交易 Hash
  3. 调用 API 创建 MPC Sign 签名任务
  4. 交易审核和签名
  5. 获取交易签名结果
  6. 构建签名交易
  7. 广播交易
警告
  1. 目前 MPC 协议支持 Secp256k1、Ed25519 签名算法,未来会增加对 BLS、Schnorr 等签名算法的支持。
  2. MPC Sign 签名任务存在一定的安全风险,操作不当可能对您造成资产损失,推荐您尽可能采用 Web3 签名任务来完成交易签名。

发起 MPC Sign 签名任务

我们以 Sepolia Token USDT 为例,调用 USDT 合约的 transfer 方法,从钱包 A 转出一笔 USDT Token 到一个陌生地址上。尽管 Safeheron 不支持 Sepolia USDT Token,但由于 Sepolia 测试网采用的是 Secp256k1 签名算法,所以可以使用 MPC Sign 来完成交易签名。假设场景数据如下:

数据项
钱包 A 的 accountKeyaccount4b8d2c00520646c8862b68420aa1bc55
钱包 A 的 EVM 地址0x1eC4FB20D8955d9D6A4aE45f01Af04e170C0c022
Sepolia Token USDT 合约地址0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0
接收 USDT Token 的陌生地址0x9437A77E6BE3a7Bf5F3cfE611BfCd1Fd30BF95f5
调用的合约方法transfer
转账 Token 数量10000

构建未签名交易

// 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,
};

序列化交易生成交易 Hash

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

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

调用 API 创建 MPC Sign 签名任务

请求参数

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 CreateMpcSignResponse {
txKey: string;
}

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

响应数据示例

提示

txKey 唯一代表一笔交易。

{
"txKey": "tx46461daa9b7a4612abce99e7ce598844"
}

交易任务审批和签名

MPC Sign 签名任务创建后,将根据您的策略决定,此签名任务是由人工审核签名还是由 API Co-Signer 自动化审核签名。

签名任务创建后,您手机 App 会收到推送通知,并且代办任务列表会显示此笔待审核状态的交易,审核通过后,由最后一个审批人的 App 参与完成签名。

获取交易签名结果

MPC Sign 签名任务创建后,会经历审批,签名等过程,您可以点此查看交易任务生命周期所经历的全部状态,我们推荐您使用 Webhook 来感知签名任务状态的变化,并获取签名结果。您也可以通过查询单笔 MPC Sign 交易接口获取状态和签名结果。

提示

通过 Webhook 来感知交易任务的状态变化,需要在 Safeheron Web 控制台 API 管理页面中配置 Webhook URL。

构建签名交易

假设通过以上步骤,我们获取到的交易签名结果为:

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
// 这里的 unsignedTransaction 是获取到签名结果后重新构建的,构建方法与上文中的 "构建未签名交易" 完全一致,
// 需要注意的是,两次构建过程中使用的所有数据必须保持完全一致。
const signedTransaction = utils.serializeTransaction(unsignedTransaction, signature);

广播交易

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

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

下一步

本教程涉及到的代码,已经在 Github 开源,获取完整源代码: