exchange.IO("sign", ...)
使用secp256k1私钥对32字节哈希进行签名,返回r、s、v等签名数据,适用于EIP-712结构化数据签名(如ERC-20 Permit授权、1inch限价单)等需要链下签名的场景。
exchange.IO(k, hash)
exchange.IO(k, hash, key)示例
以以太坊主网USDC的ERC-20 Permit(EIP-2612)授权签名为例:首先读取链上参数,使用exchange.IO("encode", ...)和exchange.IO("hash", "keccak256", ...)计算EIP-712摘要,然后使用exchange.IO("sign", ...)对摘要进行签名,最后通过eth_call模拟调用USDC合约的permit方法以验证签名(仅模拟执行,不发送交易,不消耗gas)。bytes32类型的返回值以字节数组形式返回,范例中使用bytesToHex函数将其转换为十六进制字符串。
javascript
// 为以太坊主网 USDC 签署 EIP-2612 Permit,并通过 eth_call 由 USDC 合约验证签名(仅模拟执行,不发送交易、不消耗 gas)
var USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // 以太坊主网 USDC 合约地址
var SPENDER = "0x1111111254EEB25477B68fb85Ed929f73A960582" // 被授权方地址(示例:1inch Router v5)
var VALUE = "1000000" // 授权额度:1 USDC(精度为 6 位)
// keccak256 → 以 0x 开头的 hex 字符串。fmt = "raw"(按字符串处理)或 "hex"(按十六进制数据处理)
function keccak(fmt, data) {
return "0x" + String(exchange.IO("hash", "keccak256", fmt, "hex", data)).replace(/^0x/, "")
}
function strip0x(h) { return String(h).replace(/^0x/, "") }
// bytesN 类型的返回值为字节数组(与旧版一致),此处转换为以 0x 开头的 hex 字符串
function bytesToHex(v) {
if (typeof v === "string") return "0x" + strip0x(v).toLowerCase()
return "0x" + v.map(function (b) { return ("0" + b.toString(16)).slice(-2) }).join("")
}
// 函数选择器 + 标准 ABI 编码参数 → calldata
function calldata(signature, types, values) {
var args = exchange.IO.apply(exchange, ["encode", types].concat(values))
return keccak("raw", signature).slice(0, 10) + strip0x(args)
}
// eth_call 模拟执行:成功时返回 true,revert 时返回错误原因
function simulate(data) {
var r = exchange.IO("api", "eth", "eth_call", { from: SPENDER, to: USDC, data: data }, "latest")
return r === null ? GetLastError() : true
}
function main() {
// 1. 注册所需的 ABI 片段,读取链上参数
exchange.IO("abi", USDC, JSON.stringify([
{ type: "function", name: "name", stateMutability: "view", inputs: [],
outputs: [{ type: "string" }] },
{ type: "function", name: "version", stateMutability: "view", inputs: [],
outputs: [{ type: "string" }] },
{ type: "function", name: "nonces", stateMutability: "view", inputs: [{ type: "address" }],
outputs: [{ type: "uint256" }] },
{ type: "function", name: "DOMAIN_SEPARATOR", stateMutability: "view", inputs: [],
outputs: [{ type: "bytes32" }] }
]))
var owner = exchange.IO("address")
var name = exchange.IO("api", USDC, "name")
var version = exchange.IO("api", USDC, "version")
var nonce = exchange.IO("api", USDC, "nonces", owner)
var chainId = parseInt(exchange.IO("api", "eth", "eth_chainId"), 16)
var deadline = Math.floor(Date.now() / 1000) + 3600
Log("owner:", owner, "| token:", name, "v" + version, "| nonce:", nonce, "| chainId:", chainId)
// 2. 计算 EIP-712 域分隔符,并与合约的 DOMAIN_SEPARATOR() 返回值比对
var domainSeparator = keccak("hex", exchange.IO("encode",
"bytes32,bytes32,bytes32,uint256,address",
keccak("raw",
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak("raw", name), keccak("raw", version), chainId, USDC))
var onchain = bytesToHex(exchange.IO("api", USDC, "DOMAIN_SEPARATOR"))
Log("domainSeparator:", domainSeparator,
domainSeparator === onchain ? "与链上一致" : "与链上不一致: " + onchain)
// 3. Permit 结构体哈希 → EIP-712 摘要 → 签名
var structHash = keccak("hex", exchange.IO("encode",
"bytes32,address,address,uint256,uint256,uint256",
keccak("raw",
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
owner, SPENDER, VALUE, nonce, deadline))
var digest = keccak("hex", "1901" + strip0x(domainSeparator) + strip0x(structHash))
var sig = exchange.IO("sign", digest)
Log("digest:", digest)
Log("sign:", sig)
// 4. 由 USDC 合约验证签名:permit(owner, spender, value, deadline, v, r, s)
Log("permit(v,r,s) 模拟:", simulate(calldata(
"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)",
"address,address,uint256,uint256,uint8,bytes32,bytes32",
[owner, SPENDER, VALUE, deadline, sig.v, sig.r, sig.s])))
// USDC v2.2 另提供接收 bytes 类型签名的重载方法:permit(owner, spender, value, deadline, signature)
Log("permit(bytes) 模拟:", simulate(calldata(
"permit(address,address,uint256,uint256,bytes)",
"address,address,uint256,uint256,bytes",
[owner, SPENDER, VALUE, deadline, sig.signature])))
// 5. 反例:篡改 v 值后,合约应当拒绝
Log("篡改 v 后模拟:", simulate(calldata(
"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)",
"address,address,uint256,uint256,uint8,bytes32,bytes32",
[owner, SPENDER, VALUE, deadline, sig.v === 27 ? 28 : 27, sig.r, sig.s])))
}返回值
| 类型 | 描述 |
object | 签名对象,包含以下字段: |
参数
| 名称 | 类型 | 必填 | 描述 |
k | string | 是 | 固定为 |
hash | string | 是 | 待签名的32字节哈希,格式为十六进制字符串, |
key | string | 否 | 签名所使用的私钥,未传入时使用 |
参考
备注
EIP-712结构化数据签名可以直接使用exchange.IO("signTypedData", ...),传入domain、types、message即可,不需要自己计算摘要;EIP-191消息签名使用exchange.IO("signMessage", ...)。
返回的s均为低位值(不大于曲线阶的一半),符合EIP-2规范,可直接用于OpenZeppelin ECDSA.recover等校验场景。
合约接收(v, r, s)三个参数时(如ERC-20 Permit的permit(owner, spender, value, deadline, v, r, s)),使用v、r、s字段;接收bytes signature时,使用signature字段;接收EIP-2098紧凑签名(r, vs)时(如1inch限价单协议),使用r、vs字段。
exchange.IO("hash", "sign", ...)同样可以对32字节哈希进行签名,但其返回的是拼接后的65字节数据,且v为0或1,与旧版本行为保持一致;对于EIP-712等链上校验场景,建议使用exchange.IO("sign", ...)。
hash不是32字节时报错。签名与链无关,波场(TRON)的交易所对象同样可用。