DEX API

EVM Signature#

EVM Signature SDK#

// signOrderRFQ.js
import { Wallet, ethers } from "ethers";

/**
 * Sign an OrderRFQ-typed struct and return the signature string
 *
 * @param {string} privateKey - Signer's private key (EOA)
 * @param {string} verifyingContract - Address of the contract used for signature verification
 * @param {number} chainId - Current chain ID
 * @param {object} order - Order object containing fields like rfqId, expiration, etc.
 * @returns {Promise<string>} - EIP-712 signature string
 */
export async function signOrderRFQ({ privateKey, verifyingContract, chainId, order }) {
  const wallet = new Wallet(privateKey);

  const domain = {
    name: "OnChain Labs PMM Protocol",
    version: "1.0",
    chainId,
    verifyingContract,
  };

  // OrderRFQ typehash from Solidity - must match exactly
  const ORDER_RFQ_TYPEHASH = ethers.keccak256(ethers.toUtf8Bytes(
    "OrderRFQ(uint256 rfqId,uint256 expiry,address makerAsset,address takerAsset,address makerAddress,uint256 makerAmount,uint256 takerAmount,bool usePermit2,bytes permit2Signature,bytes32 permit2Witness,string permit2WitnessType)"
  ));

  // Domain separator calculation matching Solidity
  const EIP712_DOMAIN_TYPEHASH = ethers.keccak256(ethers.toUtf8Bytes(
    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
  ));
  
  const domainSeparator = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
    ["bytes32", "bytes32", "bytes32", "uint256", "address"],
    [
      EIP712_DOMAIN_TYPEHASH,
      ethers.keccak256(ethers.toUtf8Bytes(domain.name)),
      ethers.keccak256(ethers.toUtf8Bytes(domain.version)),
      domain.chainId,
      domain.verifyingContract
    ]
  ));

  // Struct hash calculation matching Solidity OrderRFQLib.hash()
  const structHash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
    ["bytes32", "uint256", "uint256", "address", "address", "address", "uint256", "uint256", "bool", "bytes32", "bytes32", "bytes32"],
    [
      ORDER_RFQ_TYPEHASH,
      order.rfqId,
      order.expiry,
      order.makerAsset,
      order.takerAsset,
      order.makerAddress,
      order.makerAmount,
      order.takerAmount,
      order.usePermit2,
      ethers.keccak256(order.permit2Signature), // Hashed like in Solidity
      order.permit2Witness,
      ethers.keccak256(ethers.toUtf8Bytes(order.permit2WitnessType)) // Hashed like in Solidity
    ]
  ));

  // Final digest calculation matching ECDSA.toTypedDataHash
  const digest = ethers.keccak256(ethers.concat([
    "0x1901",
    domainSeparator,
    structHash
  ]));

  // Sign the digest directly (EIP-712 signature, no Ethereum message prefix)
  // Use signingKey.sign() to sign the raw digest without any prefixes
  const sig = wallet.signingKey.sign(digest);
  
  // Reconstruct signature as r + s + v to match Solidity abi.encodePacked(r, s, v)
  const rearrangedSignature = ethers.concat([sig.r, sig.s, ethers.toBeHex(sig.v, 1)]);
  
  return ethers.hexlify(rearrangedSignature);
}

export const EXAMPLE_WITNESS_TYPEHASH = ethers.keccak256(ethers.toUtf8Bytes("ExampleWitness(address user)"));
export const WITNESS_TYPE_STRING = "ExampleWitness witness)ExampleWitness(address user)TokenPermissions(address token,uint256 amount)"
export const TOKEN_PERMISSIONS_TYPEHASH = ethers.keccak256(ethers.toUtf8Bytes("TokenPermissions(address token,uint256 amount)"));

/**
 * Calculate permit2 witness hash from witness data
 *
 * @param {object} witnessData - Witness data object (e.g., { user: address })
 * @param {string} witnessTypehash - Keccak256 hash of the witness type string
 * @returns {string} - Witness hash as bytes32
 */
export function calculateWitness(witnessData, witnessTypehash = EXAMPLE_WITNESS_TYPEHASH) {
  // For ExampleWitness struct: { user: address }
  const encodedWitness = ethers.AbiCoder.defaultAbiCoder().encode(
    ["bytes32", "address"],
    [witnessTypehash, witnessData.user]
  );
  return ethers.keccak256(encodedWitness);
}

/**
 * Sign Permit2 with witness support
 *
 * @param {object} permit - Permit2 PermitTransferFrom object with { permitted: { token, amount }, nonce, deadline }
 * @param {string} spender - Spender address (usually the PMM contract)
 * @param {string} witness - Witness hash (bytes32)
 * @param {string} witnessTypeString - Full witness type string for EIP-712
 * @param {string} privateKey - Signer's private key
 * @param {string} permit2DomainSeparator - Permit2 contract's domain separator
 * @returns {Promise<string>} - Permit2 signature
 */
export async function signPermit2WithWitness({
  permit,
  spender,
  witness,
  witnessTypeString,
  privateKey,
  permit2DomainSeparator
}) {
  const wallet = new Wallet(privateKey);
  
  const TOKEN_PERMISSIONS_TYPEHASH = ethers.keccak256(
    ethers.toUtf8Bytes("TokenPermissions(address token,uint256 amount)")
  );

  // Construct the full type hash for PermitWitnessTransferFrom
  const PERMIT_WITNESS_TRANSFER_FROM_TYPEHASH = ethers.keccak256(
    ethers.toUtf8Bytes(
      `PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,${witnessTypeString}`
    )
  );

  // Encode the TokenPermissions struct
  const tokenPermissionsHash = ethers.keccak256(
    ethers.AbiCoder.defaultAbiCoder().encode(
      ["bytes32", "address", "uint256"],
      [TOKEN_PERMISSIONS_TYPEHASH, permit.permitted.token, permit.permitted.amount]
    )
  );

  // Encode the main struct
  const structHash = ethers.keccak256(
    ethers.AbiCoder.defaultAbiCoder().encode(
      ["bytes32", "bytes32", "address", "uint256", "uint256", "bytes32"],
      [
        PERMIT_WITNESS_TRANSFER_FROM_TYPEHASH,
        tokenPermissionsHash,
        spender,
        permit.nonce,
        permit.deadline,
        witness
      ]
    )
  );

  // Create the final digest
  const digest = ethers.keccak256(
    ethers.concat([
      "0x1901",
      permit2DomainSeparator,
      structHash
    ])
  );

  // Sign the digest directly (EIP-712 signature, no Ethereum message prefix)
  // Use _signingKey().sign() to sign the raw digest without any prefixes
  const sig = wallet.signingKey.sign(digest);
  
  // Reconstruct signature as r + s + v to match Solidity abi.encodePacked(r, s, v)
  const rearrangedSignature = ethers.concat([sig.r, sig.s, ethers.toBeHex(sig.v, 1)]);
  
  return ethers.hexlify(rearrangedSignature);
}

EVM Signing Example#

import { signOrderRFQ, calculateWitness, WITNESS_TYPE_STRING, signPermit2WithWitness } from "./signOrderRFQ.js";

const currentTime = Math.floor(Date.now() / 1000);
const expiry = currentTime + 90;

const MAKER_ADDRESS = "YOUR_ADDRESS";
const privateKey = "YOUR_PRIVATE_KEY";

const VERIFYING_CONTRACT = "0x5C1c902e7E04DE98b49aCd3De68E12BEE2d7908D";
const PERMIT2_DOMAIN_SEPARATOR = "0x8a6e6e19bdfb3db3409910416b47c2f8fc28b49488d6555c7fceaa4479135bc3";

const MAKER_ASSET = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
const TAKER_ASSET = "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9";

const MAKER_AMOUNT = 400000000000000;
const TAKER_AMOUNT = 1000;

const chainId = 42161;
const rfqId = 42;

// Order 1: usePermit2: false
const order1 = {
    privateKey: privateKey,
    verifyingContract: VERIFYING_CONTRACT,
    chainId: chainId,
    order: {
        rfqId: rfqId,
        expiry: expiry,
        makerAsset: MAKER_ASSET,
        takerAsset: TAKER_ASSET,
        makerAddress: MAKER_ADDRESS,
        makerAmount: MAKER_AMOUNT,
        takerAmount: TAKER_AMOUNT,
        usePermit2: false,
        permit2Signature: "0x",
        permit2Witness: "0x0000000000000000000000000000000000000000000000000000000000000000",
        permit2WitnessType: ""
    },
};

// Order 2: usePermit2: true, no witness
const order2 = {
    privateKey: privateKey,
    verifyingContract: VERIFYING_CONTRACT,
    chainId: chainId,
    order: {
        rfqId: rfqId,
        expiry: expiry,
        makerAsset: MAKER_ASSET,
        takerAsset: TAKER_ASSET,
        makerAddress: MAKER_ADDRESS,
        makerAmount: MAKER_AMOUNT,
        takerAmount: TAKER_AMOUNT,
        usePermit2: true,
        permit2Signature: "0x",
        permit2Witness: "0x0000000000000000000000000000000000000000000000000000000000000000",
        permit2WitnessType: ""
    },
};

// Order 3: usePermit2: true, with witness
const order3 = {
    privateKey: privateKey,
    verifyingContract: VERIFYING_CONTRACT,
    chainId: chainId,
    order: {
        rfqId: rfqId,
        expiry: expiry,
        makerAsset: MAKER_ASSET,
        takerAsset: TAKER_ASSET,
        makerAddress: MAKER_ADDRESS,
        makerAmount: MAKER_AMOUNT,
        takerAmount: TAKER_AMOUNT,
        usePermit2: true,
        permit2Signature: await signPermit2WithWitness({
            permit: {
                permitted: {
                    token: MAKER_ASSET,
                    amount: MAKER_AMOUNT
                },
                nonce: rfqId,
                deadline: expiry
            },
            spender: VERIFYING_CONTRACT,
            witness: calculateWitness({ user: MAKER_ADDRESS }),
            witnessTypeString: WITNESS_TYPE_STRING,
            privateKey: privateKey,
            permit2DomainSeparator: PERMIT2_DOMAIN_SEPARATOR
        }),
        permit2Witness: calculateWitness({ user: MAKER_ADDRESS }),
        permit2WitnessType: WITNESS_TYPE_STRING
    },
};

console.log("Signature 1:", await signOrderRFQ(order1));
console.log("Signature 2:", await signOrderRFQ(order2));
console.log("Signature 3:", await signOrderRFQ(order3));
console.log("permit2Signature (Order 3):", order3.order.permit2Signature);