Token Transfers
Overview
Token transfers encompass the management and tracking of supported tokens moving between your wallet and subaccounts on the Ethereal exchange. These movements primarily fall into two categories: deposits and withdrawals.
How Do I Deposit?
Deposits allow you to move tokens from your wallet into a specified subaccount on the Ethereal exchange. The process follows these steps:
Token approval: ERC20 token approval to allow Ethereal to transfer tokens on your behalf
Transact: Execute the deposit transaction, specifying the token, amount, and target subaccount
Pending: Once deposited, tokens enter a pending state while confirmation occurs
Confirmation: The deposit is synchronised with offchain systems
Availability: After confirmation, funds become available for trading
Default gas estimation can occasionally be too low, where explicit setting the gas limit for the transaction will be needed. If you encounter the DelegationFailed()
error when depositing, try adding gas: 250_000n
to the viem
deposit call.
Here's an example of how you could deposit using viem
in TypeScript:
import {
defineChain,
erc20Abi,
toHex,
parseUnits,
createWalletClient,
http,
createPublicClient,
getAddress,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { exchangeContractAbi } from './sample/exchange.abi';
export const ETHEREAL_TESTNET_CHAIN = defineChain({
id: 657468,
name: 'Ethereal Testnet',
nativeCurrency: {
decimals: 18,
name: 'USDe',
symbol: 'USDe',
},
rpcUrls: {
default: {
http: ['https://rpc.etherealtest.net'],
webSocket: ['wss://rpc.etherealtest.net'],
},
},
testnet: true,
});
const DEFAULT_SUBACCOUNT = toHex('primary', { size: 32 });
// @see: https://explorer.etherealtest.net/address/0xa1623E0AA40B142Cf755938b325321fB2c61Cf05
const USDE_ADDRESS = '0xa1623E0AA40B142Cf755938b325321fB2c61Cf05';
const MAKER_MAKER_PK = '0x...'; // Your test private key
// `verifyingContract` can be found via `HTTP GET /v1/rpc/config`
const exchangeContract = getAddress(domain.verifyingContract);
const wallet = createWalletClient({
account: privateKeyToAccount(MAKER_MAKER_PK),
chain: ETHEREAL_TESTNET_CHAIN,
transport: http(),
});
const publicClient = createPublicClient({ chain: ETHEREAL_TESTNET_CHAIN, transport: http() });
const deposit = async (amount: string) => {
const nativeAmount = parseUnits(amount, 18); // USDe has 18 deciamls.
const approveHash = await wallet.writeContract({
address: USDE_ADDRESS,
abi: erc20Abi,
functionName: 'approve',
args: [exchangeContract, nativeAmount],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
const hash = await wallet.writeContract({
address: exchangeContract,
abi: exchangeContractAbi,
functionName: 'deposit',
args: [DEFAULT_SUBACCOUNT, USDE_ADDRESS, nativeAmount, toHex('refCode', { size: 32 })],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log('Deposited!');
};
deposit('1000') // Deposit 1000 USDe
Once your deposit is confirmed, you can view your subaccount balance by calling:
curl -X 'GET' \
'https://api.etherealtest.net/v1/subaccount/balance?subaccountId=45783bec-4675-4116-8829-f277afe063d7' \
-H 'accept: application/json'
{
"hasNext": false,
"data": [
{
"subaccountId": "45783bec-4675-4116-8829-f277afe063d7",
"tokenId": "ff761c8b-6248-4673-b63d-d1f980551959",
"tokenAddress": "0xa1623E0AA40B142Cf755938b325321fB2c61Cf05",
"tokenName": "USD",
"amount": "35.644112906",
"available": "12005.889199245",
"totalUsed": "326.590624473",
"updatedAt": 1743851518800
}
]
}
Each token in your subaccount has two balance components:
Available (free): Tokens that can be used to open new positions or withdraw
TotalUsed (locked): Tokens currently allocated as margin for open positions and resting orders
Important to raise that the trading API does NOT allow users to withdraw unrealized profits. Any unrealized profits must be realized before they become available margin. Refer to margin health to learn more about initial and maintenance margin.
Depositing USDe
Ethereal is designed to support any approved ERC20 token, though USDe will be the only depositable token available at launch. Since USDe serves as the native gas token on the Ethereal appchain, it requires an additional wrapping step before deposit.
Users must first wrap their USDe to WUSDe
using our immutable wrapping contract, which implements the same interface as WETH
for familiar functionality. The WUSDe
contract address can be retrieved by querying the USD
endpoint in our API, and once wrapped, the tokens can be deposited normally into the platform.
curl -X 'GET' \
'https://api.etherealtest.net/v1/token?depositEnabled=true&withdrawEnabled=true&orderBy=createdAt' \
-H 'accept: application/json'
{
"hasNext": false,
"data": [
{
"id": "049dbd1a-bc21-4219-8264-886b64c0c046",
"address": "0x7e3203241340C579d6f5061419E9d352Eff1d9F2",
"name": "USD",
"erc20Name": "Wrapped USDe",
"erc20Symbol": "WUSDe",
"erc20Decimals": 18,
"depositEnabled": true,
"withdrawEnabled": true,
"depositFee": "0",
"withdrawFee": "1",
"minDeposit": "10",
"createdAt": 1750401257026
}
]
}
However, this process can be cumbersome, so we recommend using the provided depositUsd
convenience method instead, which handles the wrapping and deposit process automatically.
const exchange = getContract({
address: exchangeAddress,
abi: exchangeGatewayAbi,
client: { public: publicClient, wallet },
});
const refCode = toHex("refcode", { size: 32 });
const hash = await exchange.write.depositUsd([this.subaccountName, refCode], {
value: 1e18, // 1 USDe
});
await publicClient.waitForTransactionReceipt({ hash });
Initiate Token Withdrawals
Withdrawals move tokens from your subaccount back to your wallet. The process follows these steps:
Initiation: Withdrawals begin with a request through the trading API
Deduction: Once request is processed, funds are immediately deducted from the subaccount
Onchain relay: The initiate withdrawal request is relayed onchain
Pending period: Withdrawals enter a pending state with a mandatory lockout period
Finalize: Users can claim their withdrawal after the lockout period expires
To initiate a token withdrawal:
curl -X 'POST' \
'https://api.etherealtest.net/v1/token/ff761c8b-6248-4673-b63d-d1f980551959/withdraw' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"signature": "string",
"data": {
"account": "<address_of_account>",
"subaccount": "<bytes32_subaccount_name>",
"token": "<token_address_to_withdraw>",
"amount": "<amount_to_withdrawl_decimal>",
"nonce": "<nonce_in_nanoseconds_as_string>",
"signedAt": <timestamp_in_seconds>
}
}'
You can query any pending or past withdrawals by:
curl -X 'GET' \
'https://api.etherealtest.net/v1/token/withdraw?subaccountId=45783bec-4675-4116-8829-f277afe063d7&orderBy=createdAt' \
-H 'accept: application/json'
{
"hasNext": false,
"data": [
{
"id": "34510374-0010-4630-9954-46bb196806b2",
"initiatedBlockNumber": "11272338",
"finalizedBlockNumber": "11272403",
"status": "COMPLETED",
"subaccount": "0x7072696d61727900000000000000000000000000000000000000000000000000",
"token": "0xa1623E0AA40B142Cf755938b325321fB2c61Cf05",
"amount": "9",
"isReady": false,
"createdAt": 1743812502148
}
]
}
Once relayed onchain and after the lockout period has past, users can finalize their withdrawal:
/// @notice Actions pending withdraw for a subaccount.
/// @param account Address of the account to perform on behalf of
/// @param subaccount bytes32 encoded string of the subaccount
function finalizeWithdraw(address account, bytes32 subaccount) external;
There can only ever be one pending withdrawal at any time. The previous withdrawal must be finalized before initiating a new.
Retrieve Transfer History
To view all historical transfers for a specific subaccount, query:
curl -X 'GET' \
'https://api.etherealtest.net/v1/token/transfer?subaccountId=45783bec-4675-4116-8829-f277afe063d7&statuses=COMPLETED&orderBy=createdAt' \
-H 'accept: application/json'
{
"hasNext": false,
"data": [
{
"id": "34510374-0010-4630-9954-46bb196806b2",
"initiatedBlockNumber": "11272338",
"finalizedBlockNumber": "11272403",
"status": "COMPLETED",
"subaccount": "0x7072696d61727900000000000000000000000000000000000000000000000000",
"token": "0xa1623E0AA40B142Cf755938b325321fB2c61Cf05",
"type": "WITHDRAW",
"amount": "9",
"createdAt": 1743812502148,
"initiatedTransactionHash": "0xe21b386030fcbc1ff6948aac02e9e83db150e03ef9d2383c2a579d42be055a2b",
"finalizedTransactionHash": "0xe0b33c8e92f164b9bf1114987a7712e30bafbba23490436f4cc5e7ff1a44b9d7"
},
{
"id": "8cfae486-714f-4731-85ca-1b5da32e3755",
"initiatedBlockNumber": "11272299",
"finalizedBlockNumber": "11272306",
"status": "COMPLETED",
"subaccount": "0x7072696d61727900000000000000000000000000000000000000000000000000",
"token": "0xa1623E0AA40B142Cf755938b325321fB2c61Cf05",
"type": "DEPOSIT",
"amount": "10.000000001",
"createdAt": 1743812469703,
"initiatedTransactionHash": "0x6729fd9d7a2e154aa3704849fb720c3ec2f10f3d145adc0290b43761e25aacef",
"finalizedTransactionHash": "0xf22706a0c292061e3e9d843acad98f33a96b8b7e70b3920da81c1d032f18295d"
},
{
"id": "2eb362ff-f317-4865-9bee-032b4caf0b0d",
"initiatedBlockNumber": "11205366",
"finalizedBlockNumber": "11205371",
"status": "COMPLETED",
"subaccount": "0x7072696d61727900000000000000000000000000000000000000000000000000",
"token": "0xa1623E0AA40B142Cf755938b325321fB2c61Cf05",
"type": "DEPOSIT",
"amount": "20000",
"createdAt": 1743745535947,
"initiatedTransactionHash": "0x4591653ab33603c934b9c3041edad1af779293f347ec71e2c89c0f8b22cfc8a5",
"finalizedTransactionHash": "0xa11fbd46d39d8249820619a91e81962e4922546b6a13ce453b0db2b5bc9e3c5d"
}
]
}
Token Transfer Fees
Fees on transfers primarily serve to deter spam and minimize network congestion, rather than as a revenue source. Fee structures are configurable per token and may be adjusted over time.
Both deposit and withdrawals can be configured to charge fees (denomianted in native token units). As of writing right now, there are zero deposit fees however USDe is configured to charge a nominal withdrawal fee of 1.
Last updated