Skip to main content

Internal Transfer With Wallet Signature

The Internal Transfer with Wallet Signature API allows users to transfer assets between Orderly accounts. This API requires a wallet EIP-712 signature to verify the ownership and authorization of the transfer. To create an internal transfer request, users can follow the following steps:
1

Obtain a transfer nonce

Get a valid transfer nonce from the Get Transfer Nonce API.
// Response Example
{
  "success": true,
  "timestamp": 1702989203989,
  "data": {
    "transfer_nonce": 1
  }
}
2

Obtain a signature from EIP-712

Sign an EIP-712 message of message type InternalTransfer:
"InternalTransfer": [
    { "name": "receiver", "type": "bytes32" },
    { "name": "token", "type": "string" },
    { "name": "amount", "type": "uint256" },
    { "name": "transferNonce", "type": "uint64" }
]
where:
NameTypeRequiredDescription
receiverbytes32YThe Orderly Account ID of the receiver (in bytes32 format)
tokenstringYThe token symbol (e.g., “USDC”)
amountuint256YThe amount to transfer (raw value with decimals, e.g., 1000000 for 1 USDC)
transferNonceuint64YThe nonce obtained from Step 1
The EIP-712 signature uses the on-chain domain. The verifyingContract should be the Ledger contract address, which can be found here.
import { ethers } from "ethers";

const chainId = 42161; // Arbitrum One
const ledgerContractAddress = "0x..."; // Get from addresses page

// Define Domain
const domain = {
  name: "Orderly",
  version: "1",
  chainId: chainId,
  verifyingContract: ledgerContractAddress
};

// Define Types
const types = {
  InternalTransfer: [
    { name: "receiver", type: "bytes32" },
    { name: "token", type: "string" },
    { name: "amount", type: "uint256" },
    { name: "transferNonce", type: "uint64" }
  ]
};

// Define Message Data
const receiverAccountId = "0x9ff99a5d6cb71a3ef897b0fff5f5801af6dc5f72d8f1608e61409b8fc965bd68";
const message = {
  receiver: receiverAccountId,
  token: "USDC",
  amount: 1000000n, // 1 USDC (assuming 6 decimals)
  transferNonce: 1 // From API
};

// Sign
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);
const signature = await wallet.signTypedData(domain, types, message);
3

Make an internal transfer request

Call the Create Internal Transfer With Wallet Signature API.The request body must include the generated signature and the message parameters used to create it:
{
  "message": {
    "receiver": "0x9ff99a5d6cb71a3ef897b0fff5f5801af6dc5f72d8f1608e61409b8fc965bd68",
    "token": "USDC",
    "amount": "1000000",
    "transferNonce": "1",
    "chainId": "42161",
    "chainType": "EVM"
  },
  "signature": "0xa9f5ef503a95af0bb0211858e8b6a83a3d23d7a84b8db3c2aa3327deb34b084577758cc6d8cbc9a047d60436c62d0591ee6693f34487bea785247608516360991b",
  "userAddress": "0xUserWalletAddress...",
  "verifyingContract": "0xLedgerContractAddress..."
}
where:
ParameterTypeRequiredDescription
message.receiverstringYThe Orderly Account ID of the receiver
message.tokenstringYToken symbol
message.amountstringYTransfer amount
message.transferNoncestringYMust match the nonce used in the signature
message.chainIdstringYChain ID
message.chainTypestringYChain type: EVM or SOL
signaturestringYThe EIP-712 signature string
userAddressstringYThe wallet address of the sender (must match the signer)
verifyingContractstringYThe contract address used in the EIP-712 domain
Ensure the userAddress matches the wallet address associated with the Orderly Account ID making the request. The standard Orderly authentication headers (Orderly Key signature) are also required for this API call.

Full example

import { config } from "dotenv";
import { ethers } from "ethers";

config();

const BASE_URL = "https://testnet-api.orderly.org";
const CHAIN_ID = 421614; // Arbitrum Sepolia
const LEDGER_CONTRACT_ADDRESS = "0x..."; // Get from addresses page
const RECEIVER_ACCOUNT_ID = "0x9ff99a5d6cb71a3ef897b0fff5f5801af6dc5f72d8f1608e61409b8fc965bd68";

async function internalTransfer(): Promise<void> {
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!);

  // Step 1: Get transfer nonce
  // Note: This requires Orderly authentication headers
  const nonceRes = await fetch(`${BASE_URL}/v1/transfer_nonce`, {
    method: "GET",
    headers: {
      // Add Orderly authentication headers here
      "Content-Type": "application/json"
    }
  });
  const nonceData = await nonceRes.json();
  const transferNonce = nonceData.data.transfer_nonce;

  // Step 2: Sign EIP-712 message
  const domain = {
    name: "Orderly",
    version: "1",
    chainId: CHAIN_ID,
    verifyingContract: LEDGER_CONTRACT_ADDRESS
  };

  const types = {
    InternalTransfer: [
      { name: "receiver", type: "bytes32" },
      { name: "token", type: "string" },
      { name: "amount", type: "uint256" },
      { name: "transferNonce", type: "uint64" }
    ]
  };

  const message = {
    receiver: RECEIVER_ACCOUNT_ID,
    token: "USDC",
    amount: 1000000n, // 1 USDC
    transferNonce: transferNonce
  };

  const signature = await wallet.signTypedData(domain, types, message);

  // Step 3: Make transfer request
  const transferRes = await fetch(`${BASE_URL}/v2/internal_transfer`, {
    method: "POST",
    headers: {
      // Add Orderly authentication headers here
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      message: {
        receiver: RECEIVER_ACCOUNT_ID,
        token: "USDC",
        amount: "1000000",
        transferNonce: transferNonce.toString(),
        chainId: CHAIN_ID.toString(),
        chainType: "EVM"
      },
      signature: signature,
      userAddress: await wallet.getAddress(),
      verifyingContract: LEDGER_CONTRACT_ADDRESS
    })
  });

  const result = await transferRes.json();
  console.log("Transfer result:", result);
}

internalTransfer();