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.
This API requires Orderly authentication headers:
  • orderly-timestamp: Current timestamp in milliseconds
  • orderly-account-id: Your Orderly Account ID
  • orderly-key: Your Orderly Key
  • orderly-signature: Ed25519 signature of the request message
The signature is generated by signing {timestamp}{http_method}{url_path}{payload} using your Orderly Secret. For production use, it’s recommended to use the Orderly SDK which handles authentication automatically.
interface Client {
    _sign_request(method: string, path: string, payload?: any): Promise<any>;
}

async function get_transfer_nonce(client: Client): Promise<number | null> {
    try {
        const response = await client._sign_request("GET", "/v1/transfer_nonce");
        if (response.data && response.data.transfer_nonce) {
            return response.data.transfer_nonce;
        } else if (response.transfer_nonce) {
            return response.transfer_nonce;
        }
        return null;
    } catch (error) {
        return null;
    }
}
// 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 (must be converted to bytes32 format for EIP-712 signing)
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
// Note: ethers.js signTypedData automatically converts hex strings to bytes32
const receiverAccountId = "0x9ff99a5d6cb71a3ef897b0fff5f5801af6dc5f72d8f1608e61409b8fc965bd68";
const message = {
  receiver: receiverAccountId, // ethers.js automatically handles hex string to bytes32 conversion
  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:
Note that in the API request, receiver is a string (hex format with 0x prefix), while in the EIP-712 signature it must be bytes32. The userAddress must be an EVM address format (0x-prefixed hex string) for EVM chains, or a Solana address for SOL chains.

{
"message": {
"receiver": "0x9ff99a5d6cb71a3ef897b0fff5f5801af6dc5f72d8f1608e61409b8fc965bd68",
"token": "USDC",
"amount": "1000000",
"transferNonce": "2",
"chainId": "421614",
"chainType": "EVM"
},
"signature": "0xa9f5ef503a95af0bb0211858e8b6a83a3d23d7a84b8db3c2aa3327deb34b084577758cc6d8cbc9a047d60436c62d0591ee6693f34487bea785247608516360991b",
"userAddress": "0xb6309c90b5ecc2f8a87987294af03951e80bfef0",
"verifyingContract": "0x8794E7260517B1766fc7b55cAfcd56e6bf08600e"
}

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 { ethers } from 'ethers';

const ACCOUNT_ID = "0x...";
const ENV = 'mainnet';
const ORDERLY_KEY = "ed25519:xxx";
const ORDERLY_SECRET = "ed25519:xxx";
const WALLET_SECRET = "70xxxf9";
const USER_ADDRESS = "0x...";
const CHAIN_ID = ENV === 'mainnet' ? 42161 : 421614;
const LEDGER_CONTRACT_ADDRESS = ENV === 'mainnet' 
    ? "0x6F7a338F2aA472838dEFD3283eB360d4Dff5D203" 
    : "0x8794E7260517B1766fc7b55cAfcd56e6bf08600e";
const RECEIVER_ACCOUNT_ID = "0x...";
const TRANSFER_AMOUNT = 1000000;
const ORDERLY_TESTNET = ENV === 'testnet';

interface Client {
    _sign_request(method: string, path: string, payload?: any): Promise<any>;
}

async function get_transfer_nonce(client: Client): Promise<number | null> {
    try {
        const response = await client._sign_request("GET", "/v1/transfer_nonce");
        if (response.data && response.data.transfer_nonce) {
            return response.data.transfer_nonce;
        } else if (response.transfer_nonce) {
            return response.transfer_nonce;
        }
        return null;
    } catch (error) {
        return null;
    }
}

function sign_internal_transfer_message(
    receiver_account_id: string,
    token: string,
    amount: number,
    transfer_nonce: number,
    chain_id: number,
    ledger_contract_address: string,
    private_key: string
): string {
    const receiver_hex = receiver_account_id.startsWith('0x') 
        ? receiver_account_id.slice(2) 
        : receiver_account_id;
    const receiver_bytes32 = ethers.zeroPadValue('0x' + receiver_hex, 32);
    
    const domain = {
        name: "Orderly",
        version: "1",
        chainId: chain_id,
        verifyingContract: ledger_contract_address,
    };
    
    const types = {
        EIP712Domain: [
            { name: "name", type: "string" },
            { name: "version", type: "string" },
            { name: "chainId", type: "uint256" },
            { name: "verifyingContract", type: "address" },
        ],
        InternalTransfer: [
            { name: "receiver", type: "bytes32" },
            { name: "token", type: "string" },
            { name: "amount", type: "uint256" },
            { name: "transferNonce", type: "uint64" },
        ],
    };
    
    const message = {
        receiver: receiver_bytes32,
        token: token,
        amount: amount,
        transferNonce: transfer_nonce,
    };
    
    const wallet = new ethers.Wallet(private_key);
    const signature = wallet.signTypedDataSync(domain, types, message);
    return signature;
}

async function create_internal_transfer_v2(
    client: Client,
    receiver_account_id: string,
    token: string,
    amount: string,
    transfer_nonce: number,
    chain_id: number,
    chain_type: string,
    signature: string,
    user_address: string,
    verifying_contract: string
): Promise<any> {
    const payload = {
        message: {
            receiver: receiver_account_id,
            token: token,
            amount: amount,
            transferNonce: String(transfer_nonce),
            chainId: String(chain_id),
            chainType: chain_type,
        },
        signature: signature,
        userAddress: user_address,
        verifyingContract: verifying_contract,
    };
    
    try {
        return await client._sign_request("POST", "/v2/internal_transfer", payload);
    } catch (error) {
        return null;
    }
}

async function test_internal_transfer_v2(): Promise<any> {
    try {
        const { Rest: Client } = await import('orderly-evm-connector');
        
        const client = new Client({
            orderly_key: ORDERLY_KEY,
            orderly_secret: ORDERLY_SECRET,
            orderly_account_id: ACCOUNT_ID,
            wallet_secret: WALLET_SECRET,
            orderly_testnet: ORDERLY_TESTNET,
            debug: true
        });
        
        const transfer_nonce = await get_transfer_nonce(client);
        if (!transfer_nonce) {
            return null;
        }
        
        const signature = sign_internal_transfer_message(
            RECEIVER_ACCOUNT_ID,
            "USDC",
            TRANSFER_AMOUNT,
            transfer_nonce,
            CHAIN_ID,
            LEDGER_CONTRACT_ADDRESS,
            WALLET_SECRET
        );
        
        if (!signature) {
            return null;
        }
        
        return await create_internal_transfer_v2(
            client,
            RECEIVER_ACCOUNT_ID,
            "USDC",
            String(TRANSFER_AMOUNT),
            transfer_nonce,
            CHAIN_ID,
            "EVM",
            signature,
            USER_ADDRESS,
            LEDGER_CONTRACT_ADDRESS
        );
    } catch (error) {
        return null;
    }
}

test_internal_transfer_v2();