Type/to search
Built-in Functions
Structures
Built-in Variables and Constants
Conventions
Exchanges
Capability Matrix
Centralized Exchanges
Decentralized Exchanges (DEX)
Prediction Markets
Securities and Futures
On-chain & Custom

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

TypeDescription

object

Signature object containing the following fields: r (32 bytes, hexadecimal string), s (32 bytes, hexadecimal string), v (numeric value, 27 or 28), signature (65 bytes r‖s‖v, hexadecimal string), and vs (EIP-2098 compact signature, 32 bytes, hexadecimal string). Returns a null value if the call fails.

Arguments

NameTypeRequiredDescription

k

string

Yes

Function of the exchange.IO() function. Setting it to sign indicates that the function is used to sign a hash.

hash

string

Yes

32-byte hash to be signed, formatted as a hexadecimal string; the 0x prefix is optional. This function does not hash the hash again, so the EIP-712 digest must be computed in advance. Please refer to the example.

key

string

No

Private key used for signing. If it is not passed, the private key configured for the exchange object is used.

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.