Ethereal is in testnet and under development so documentation may be outdated!
Page cover image

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:

  1. Token approval: ERC20 token approval to allow Ethereal to transfer tokens on your behalf

  2. Transact: Execute the deposit transaction, specifying the token, amount, and target subaccount

  3. Pending: Once deposited, tokens enter a pending state while confirmation occurs

  4. Confirmation: The deposit is synchronised with offchain systems

  5. Availability: After confirmation, funds become available for trading

Token deposits to a fresh subaccount will also automatically create a subaccount. See subaccounts and signers section to learn more.

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

Visit supported tokens and subaccounts and signers to learn more.

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:

  1. Initiation: Withdrawals begin with a request through the trading API

  2. Deduction: Once request is processed, funds are immediately deducted from the subaccount

  3. Onchain relay: The initiate withdrawal request is relayed onchain

  4. Pending period: Withdrawals enter a pending state with a mandatory lockout period

  5. Finalize: Users can claim their withdrawal after the lockout period expires

The withdrawal lockout period is a configurable parameter and currently set to 15 seconds.

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;

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