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 millisecondsorderly-account-id: Your Orderly Account IDorderly-key: Your Orderly Keyorderly-signature: Ed25519 signature of the request message
{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.Example code
Example code
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 where:
InternalTransfer:"InternalTransfer": [
{ "name": "receiver", "type": "bytes32" },
{ "name": "token", "type": "string" },
{ "name": "amount", "type": "uint256" },
{ "name": "transferNonce", "type": "uint64" }
]
| Name | Type | Required | Description |
|---|---|---|---|
| receiver | bytes32 | Y | The Orderly Account ID of the receiver (must be converted to bytes32 format for EIP-712 signing) |
| token | string | Y | The token symbol (e.g., “USDC”) |
| amount | uint256 | Y | The amount to transfer (raw value with decimals, e.g., 1000000 for 1 USDC) |
| transferNonce | uint64 | Y | The 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.Example code
Example code
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:where:
[!WARNING] EIP-712 Signature vs. API Payload Mismatch: The REST API request bodymessagestructure includes helper fieldschainIdandchainType. These fields must NOT be included in the EIP-712 message object during cryptography signing. If you include them in the EIP-712 signaturemessagehash, 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"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
| message.receiver | string | Y | The Orderly Account ID of the receiver |
| message.token | string | Y | Token symbol |
| message.amount | string | Y | Transfer amount |
| message.transferNonce | string | Y | Must match the nonce used in the signature |
| message.chainId | string | Y | Chain ID |
| message.chainType | string | Y | Chain type: EVM or SOL |
| signature | string | Y | The EIP-712 signature string |
| userAddress | string | Y | The wallet address of the sender (must match the signer) |
| verifyingContract | string | Y | The 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()