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:
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;
  }
}
def get_transfer_nonce(client):
    try:
        response = client._sign_request("GET", "/v1/transfer_nonce")
        if 'data' in response and 'transfer_nonce' in response['data']:
            return response['data']['transfer_nonce']
        elif 'transfer_nonce' in response:
            return response['transfer_nonce']
        return None
    except Exception:
        return None
// 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);
import os
from eth_account import Account
from eth_account.messages import encode_typed_data

chain_id = 42161  # Arbitrum One
ledger_contract_address = "0x..."  # Get from addresses page
private_key = os.environ.get("PRIVATE_KEY")
if not private_key:
    raise ValueError("PRIVATE_KEY environment variable is not set")
receiver_account_id = "0x9ff99a5d6cb71a3ef897b0fff5f5801af6dc5f72d8f1608e61409b8fc965bd68"

# Construct EIP-712 Data
# Note: receiver must be converted from hex string to bytes32 for signing
data = {
    "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"},
        ],
    },
    "domain": {
        "name": "Orderly",
        "version": "1",
        "chainId": chain_id,
        "verifyingContract": ledger_contract_address,
    },
    "primaryType": "InternalTransfer",
    "message": {
        "receiver": bytes.fromhex(receiver_account_id[2:]),  # Convert hex string to bytes32
        "token": "USDC",
        "amount": 1000000,
        "transferNonce": 1
    },
}

# Sign
account = Account.from_key(private_key)
signable_message = encode_typed_data(full_message=data)
signed_message = account.sign_message(signable_message)
signature = signed_message.signature.hex()
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:
[!WARNING] EIP-712 Signature vs. API Payload Mismatch: The REST API request body message structure includes helper fields chainId and chainType. These fields must NOT be included in the EIP-712 message object during cryptography signing. If you include them in the EIP-712 signature message hash, the gateway validation will fail.
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": "0x1826B75e2ef249173FC735149AE4B8e9ea10abff"
}

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"
    : "0x1826B75e2ef249173FC735149AE4B8e9ea10abff";
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;
    }
}

async 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
): Promise<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 = {
        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 = await wallet.signTypedData(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 = await 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();
from eth_account import Account
from eth_account.messages import encode_typed_data
from orderly_evm_connector.rest import Rest as Client
from orderly_evm_connector.error import ClientError

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

def get_transfer_nonce(client):
    try:
        response = client._sign_request("GET", "/v1/transfer_nonce")
        if 'data' in response and 'transfer_nonce' in response['data']:
            return response['data']['transfer_nonce']
        elif 'transfer_nonce' in response:
            return response['transfer_nonce']
        return None
    except Exception:
        return None

def sign_internal_transfer_message(
    receiver_account_id: str,
    token: str,
    amount: int,
    transfer_nonce: int,
    chain_id: int,
    ledger_contract_address: str,
    private_key: str
):
    receiver_hex = receiver_account_id[2:] if receiver_account_id.startswith('0x') else receiver_account_id
    receiver_bytes = bytes.fromhex(receiver_hex)

    data = {
        "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"},
            ],
        },
        "domain": {
            "name": "Orderly",
            "version": "1",
            "chainId": chain_id,
            "verifyingContract": ledger_contract_address,
        },
        "primaryType": "InternalTransfer",
        "message": {
            "receiver": receiver_bytes,
            "token": token,
            "amount": amount,
            "transferNonce": transfer_nonce,
        },
    }

    account = Account.from_key(private_key)
    signable_message = encode_typed_data(full_message=data)
    signed_message = account.sign_message(signable_message)
    return signed_message.signature.hex()

def create_internal_transfer_v2(
    client,
    receiver_account_id: str,
    token: str,
    amount: str,
    transfer_nonce: int,
    chain_id: int,
    chain_type: str,
    signature: str,
    user_address: str,
    verifying_contract: str
):
    payload = {
        "message": {
            "receiver": receiver_account_id,
            "token": token,
            "amount": amount,
            "transferNonce": str(transfer_nonce),
            "chainId": str(chain_id),
            "chainType": chain_type,
        },
        "signature": signature,
        "userAddress": user_address,
        "verifyingContract": verifying_contract,
    }

    try:
        return client._sign_request("POST", "/v2/internal_transfer", payload=payload)
    except ClientError:
        return None
    except Exception:
        return None

def test_internal_transfer_v2():
    try:
        client = Client(
            orderly_key=ORDERLY_KEY,
            orderly_secret=ORDERLY_SECRET,
            orderly_account_id=ACCOUNT_ID,
            wallet_secret=WALLET_SECRET,
            orderly_testnet=ORDERLY_TESTNET,
            debug=True
        )

        transfer_nonce = get_transfer_nonce(client)
        if not transfer_nonce:
            return None

        signature = sign_internal_transfer_message(
            receiver_account_id=RECEIVER_ACCOUNT_ID,
            token="USDC",
            amount=TRANSFER_AMOUNT,
            transfer_nonce=transfer_nonce,
            chain_id=CHAIN_ID,
            ledger_contract_address=LEDGER_CONTRACT_ADDRESS,
            private_key=WALLET_SECRET
        )

        if not signature:
            return None

        return create_internal_transfer_v2(
            client=client,
            receiver_account_id=RECEIVER_ACCOUNT_ID,
            token="USDC",
            amount=str(TRANSFER_AMOUNT),
            transfer_nonce=transfer_nonce,
            chain_id=CHAIN_ID,
            chain_type="EVM",
            signature=signature,
            user_address=USER_ADDRESS,
            verifying_contract=LEDGER_CONTRACT_ADDRESS
        )
    except Exception:
        return None

if __name__ == "__main__":
    test_internal_transfer_v2()