exchange.IO("sign", ...)
Signs a 32-byte hash with a secp256k1 private key and returns signature data such as r, s, and v. It is suitable for scenarios that require off-chain signing, such as EIP-712 structured data signing (e.g., ERC-20 Permit approvals, 1inch limit orders).
exchange.IO(k, hash)
exchange.IO(k, hash, key)Examples
Taking the ERC-20 Permit (EIP-2612) authorization signature for USDC on Ethereum mainnet as an example: first read the on-chain parameters, use exchange.IO("encode", ...) and exchange.IO("hash", "keccak256", ...) to compute the EIP-712 digest, then use exchange.IO("sign", ...) to sign the digest, and finally simulate a call to the permit method of the USDC contract via eth_call to verify the signature (simulation only; no transaction is sent and no gas is consumed). Return values of type bytes32 are returned as byte arrays; the example uses the bytesToHex function to convert them to hexadecimal strings.
javascript
// Sign an EIP-2612 Permit for USDC on Ethereum mainnet, and have the USDC contract verify the
// signature via eth_call (simulation only; no transaction is sent and no gas is consumed)
// USDC contract address on Ethereum mainnet
var USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
// Spender address (example: 1inch Router v5)
var SPENDER = "0x1111111254EEB25477B68fb85Ed929f73A960582"
var VALUE = "1000000" // Allowance: 1 USDC (6 decimals)
// keccak256 → hex string prefixed with 0x. fmt = "raw" (treat as string) or "hex" (treat as
// hexadecimal data)
function keccak(fmt, data) {
return "0x" + String(exchange.IO("hash", "keccak256", fmt, "hex", data)).replace(/^0x/, "")
}
function strip0x(h) { return String(h).replace(/^0x/, "") }
// Return values of type bytesN are byte arrays (consistent with previous versions); convert them to
// a hex string prefixed with 0x here
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("")
}
// Function selector + standard ABI-encoded arguments → 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 simulation: returns true on success, or the error reason on 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. Register the required ABI fragments and read the on-chain parameters
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. Compute the EIP-712 domain separator and compare it with the value returned by the
// contract's 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 ? "matches on-chain value" : "does not match on-chain value: " + onchain)
// 3. Permit struct hash → EIP-712 digest → signature
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. Have the USDC contract verify the signature: permit(owner, spender, value, deadline, v, r,
// s)
Log("permit(v,r,s) simulation:", 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 also provides an overloaded method that accepts a bytes-type signature:
// permit(owner, spender, value, deadline, signature)
Log("permit(bytes) simulation:", simulate(calldata(
"permit(address,address,uint256,uint256,bytes)",
"address,address,uint256,uint256,bytes",
[owner, SPENDER, VALUE, deadline, sig.signature])))
// 5. Negative case: after tampering with the v value, the contract should reject the signature
Log("simulation after tampering with 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])))
}Returns
| Type | Description |
object | Signature object containing the following fields: |
Arguments
| Name | Type | Required | Description |
k | string | Yes | Function of the |
hash | string | Yes | 32-byte hash to be signed, formatted as a hexadecimal string; the |
key | string | No | Private key used for signing. If it is not passed, the private key configured for the |
See Also
Remarks
For EIP-712 typed data, exchange.IO("signTypedData", ...) can be used directly by passing domain, types and message, with no need to compute the digest yourself; for EIP-191 message signing use exchange.IO("signMessage", ...).
The returned s is always a low-s value (not greater than half of the curve order), which complies with the EIP-2 specification and can be used directly in verification scenarios such as OpenZeppelin ECDSA.recover.
When a contract accepts the three parameters (v, r, s) (e.g., ERC-20 Permit's permit(owner, spender, value, deadline, v, r, s)), use the v, r, and s fields; when it accepts bytes signature, use the signature field; when it accepts an EIP-2098 compact signature (r, vs) (e.g., the 1inch Limit Order Protocol), use the r and vs fields.
exchange.IO("hash", "sign", ...) can also sign a 32-byte hash, but it returns the concatenated 65-byte data with v being 0 or 1, consistent with the behavior of previous versions; for on-chain verification scenarios such as EIP-712, it is recommended to use exchange.IO("sign", ...).
A hash that is not 32 bytes is an error. Signing does not depend on the chain and works on a TRON exchange object as well.