> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How-to: Withdraw USDC from HyperCore to Solana

> Withdraw USDC from a HyperCore spot or perp balance to a Solana associated token account using CCTP.

Withdraw USDC from a HyperCore `spot` or `perp` balance directly to a Solana
associated token account (ATA) using CCTP. The withdrawal burns USDC on HyperEVM
and mints it on Solana in a single operation. For withdrawals to EVM
blockchains, see
[Withdraw USDC from HyperCore to EVM chains](./withdraw-usdc-from-hypercore-to-evm).
For CCTP domain IDs, see
[Supported blockchains and domains](../concepts/supported-chains-and-domains).

## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/)

* Prepared an EVM wallet with the private key available

* Funded your HyperCore account with USDC in either `spot` or `perp` balance

* Created a new Node project and installed dependencies:

  ```bash theme={null}
  npm install ethers @solana/web3.js @solana/spl-token
  npm install -D tsx typescript @types/node
  ```

* Created a `.env` file with required environment variables. Open the file in
  your editor and add your private key and Solana addresses:

  ```text theme={null}
  PRIVATE_KEY=0x...
  SOLANA_ATA_OWNER=...  # Solana wallet address that owns the recipient ATA
  SOLANA_USDC_MINT=...  # USDC mint address on the target Solana network
  ```

  Keep your private key safe and never commit `.env` to version control. Add
  `.env` to your `.gitignore` file.

<Warning>
  Withdrawals also include a HyperCore fee, and when `createATA` hook data is
  included in the `data` field, an additional CCTP forwarding fee is added. If the
  withdrawal amount is less than the HyperCore and CCTP forwarding fees combined,
  the transaction reverts on HyperEVM.
</Warning>

## Steps

### Step 1. Derive the recipient ATA and createATA hook data

For Solana, the mint recipient is the associated token account for the USDC mint
and recipient owner. The `createATA` hook data tells CCTP to create that ATA
when needed.

```ts TypeScript theme={null}
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";

function encodeCreateAtaHookData(ataOwner: PublicKey): `0x${string}` {
  const magic = Buffer.from("cctp-forward", "utf-8")
    .toString("hex")
    .padEnd(48, "0");
  const createAta = "01";
  const owner = Buffer.from(ataOwner.toBytes()).toString("hex");

  return `0x${magic}${createAta}${owner}`;
}

const ataOwner = new PublicKey(process.env.SOLANA_ATA_OWNER as string);
const usdcMint = new PublicKey(process.env.SOLANA_USDC_MINT as string);
const recipientAta = getAssociatedTokenAddressSync(usdcMint, ataOwner);
const createAtaHookData = encodeCreateAtaHookData(ataOwner);
```

### Step 2. Construct the `sendToEvmWithData` action

Create a `sendToEvmWithData` action object with the following parameters:

* `type`: `sendToEvmWithData`
* `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet)
* `signatureChainId`: The ID of the chain used when signing in hexadecimal
  format (for example, `"0xa4b1"` for Arbitrum).
* `token`: `USDC`
* `amount`: The amount of USDC as a string (for example, `"10"` for 10 USDC,
  `"1.5"` for 1.5 USDC)
* `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for `perp`
  balance
* `destinationRecipient`: The recipient ATA on Solana. This is the ATA for the
  recipient owner and the USDC mint, not the owner wallet address.
* `addressEncoding`: `base58` for Solana recipients
* `destinationChainId`: `5`, the CCTP destination domain ID for Solana
* `gasLimit`: Gas limit for the transaction on the destination blockchain
* `data`: The `createATA` hook data from the previous step, which includes the
  recipient owner public key as `ataOwner`. The CCTP `destinationCaller` is
  always set to the zero address, so any caller can submit the CCTP receive
  instruction on Solana. The USDC still mints only to `destinationRecipient`.
* `nonce`: Current timestamp in milliseconds

```ts TypeScript theme={null}
// Example action payload for sendToEvmWithData
const action = {
  type: "sendToEvmWithData",
  hyperliquidChain: "Mainnet",
  signatureChainId: "0xa4b1", // Chain ID used when signing
  token: "USDC",
  amount: "10", // 10 USDC
  sourceDex: "spot", // or "" for perp
  destinationRecipient: recipientAta.toBase58(),
  addressEncoding: "base58",
  destinationChainId: 5, // Solana CCTP domain
  gasLimit: 200000,
  data: createAtaHookData,
  nonce: Date.now(),
};
```

### Step 3. Sign the action using EIP-712

Sign the action using the EIP-712 typed data signing standard. The signature
proves that you authorize this withdrawal.

The signing domain should include:

* `name`: `"HyperliquidSignTransaction"`
* `version`: `"1"`
* `chainId`: The chain ID from `signatureChainId` (as a number)
* `verifyingContract`: `"0x0000000000000000000000000000000000000000"`

```ts TypeScript theme={null}
import { Wallet, Signature } from "ethers";

// Sign the action using EIP-712
const wallet = new Wallet(privateKey);
const chainId = Number(BigInt(action.signatureChainId));

const domain = {
  name: "HyperliquidSignTransaction",
  version: "1",
  chainId,
  verifyingContract: "0x0000000000000000000000000000000000000000",
};

const types = {
  "HyperliquidTransaction:SendToEvmWithData": [
    { name: "hyperliquidChain", type: "string" },
    { name: "token", type: "string" },
    { name: "amount", type: "string" },
    { name: "sourceDex", type: "string" },
    { name: "destinationRecipient", type: "string" },
    { name: "addressEncoding", type: "string" },
    { name: "destinationChainId", type: "uint32" },
    { name: "gasLimit", type: "uint64" },
    { name: "data", type: "bytes" },
    { name: "nonce", type: "uint64" },
  ],
};

const message = {
  hyperliquidChain: "Mainnet",
  token: "USDC",
  amount: "10",
  sourceDex: "spot",
  destinationRecipient: recipientAta.toBase58(),
  addressEncoding: "base58",
  destinationChainId: 5,
  gasLimit: BigInt(200000),
  data: createAtaHookData,
  nonce: BigInt(Date.now()),
};

const sigHex = await wallet.signTypedData(domain, types, message);
const sig = Signature.from(sigHex);
const signature = { r: sig.r, s: sig.s, v: sig.v };
```

### Step 4. Submit the signed action to the exchange API

Call the
[exchange](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint)
endpoint with the action, nonce, and signature.

```ts TypeScript theme={null}
const response = await fetch("https://api.hyperliquid.xyz/exchange", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    action,
    nonce: action.nonce,
    signature,
  }),
});

const result = await response.json();

if (response.status === 200 && result.status === "ok") {
  console.log("Withdrawal initiated successfully:", result);
} else {
  throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
}
```

## Full example code

The following is a complete example of how to withdraw USDC from HyperCore to
Solana. By default, it withdraws 10 USDC from your perp balance. Update
`destinationChainId` only if the CCTP Solana domain changes, and set
`signatureChainId` to the ID of the chain used when signing in hexadecimal
format.

```ts TypeScript expandable theme={null}
/**
 * Script: Withdraw USDC from HyperCore to Solana
 * - Derives the recipient associated token account
 * - Encodes createATA hook data
 * - Signs EIP-712 sendToEvmWithData action
 * - Submits to Hyperliquid /exchange API
 */

import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
import { Wallet, Signature } from "ethers";

function encodeCreateAtaHookData(ataOwner: PublicKey): `0x${string}` {
  const magic = Buffer.from("cctp-forward", "utf-8")
    .toString("hex")
    .padEnd(48, "0");
  const createAta = "01";
  const owner = Buffer.from(ataOwner.toBytes()).toString("hex");

  return `0x${magic}${createAta}${owner}`;
}

// -------- Configuration --------
const config = {
  privateKey: process.env.PRIVATE_KEY as string,

  // Transfer parameters
  amount: process.env.AMOUNT || "10", // 10 USDC
  sourceDex: process.env.SOURCE_DEX || "", // "" for perp, "spot" for spot

  // Solana destination parameters
  ataOwner: process.env.SOLANA_ATA_OWNER as string,
  usdcMint: process.env.SOLANA_USDC_MINT as string,
  destinationChainId: 5, // Solana CCTP domain
  addressEncoding: "base58",
  gasLimit: Number(process.env.GAS_LIMIT || 200000),

  // Hyperliquid environment
  isMainnet:
    String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true",
  signatureChainId: "0xa4b1", // Chain ID used when signing
};

// -------- Main Function --------
async function main() {
  // Validate required parameters
  if (!config.privateKey) {
    throw new Error("Set PRIVATE_KEY");
  }
  if (!config.ataOwner) {
    throw new Error("Set SOLANA_ATA_OWNER");
  }
  if (!config.usdcMint) {
    throw new Error("Set SOLANA_USDC_MINT");
  }

  const ataOwner = new PublicKey(config.ataOwner);
  const usdcMint = new PublicKey(config.usdcMint);
  const recipientAta = getAssociatedTokenAddressSync(usdcMint, ataOwner);
  const createAtaHookData = encodeCreateAtaHookData(ataOwner);

  const apiUrl = config.isMainnet
    ? "https://api.hyperliquid.xyz"
    : "https://api.hyperliquid-testnet.xyz";
  const hyperliquidChain = config.isMainnet ? "Mainnet" : "Testnet";
  const chainId = parseInt(config.signatureChainId, 16);
  const timestamp = Date.now();

  console.log("Withdrawing from HyperCore:", hyperliquidChain);
  console.log("Source balance:", config.sourceDex || "perp");
  console.log("Amount (USDC):", config.amount);
  console.log("Solana ATA owner:", ataOwner.toBase58());
  console.log("Destination recipient ATA:", recipientAta.toBase58());
  console.log("Destination chain ID:", config.destinationChainId);
  console.log("Gas limit:", config.gasLimit);

  // EIP-712 Domain
  const domain = {
    name: "HyperliquidSignTransaction",
    version: "1",
    chainId,
    verifyingContract: "0x0000000000000000000000000000000000000000",
  };

  // EIP-712 Types
  const types = {
    "HyperliquidTransaction:SendToEvmWithData": [
      { name: "hyperliquidChain", type: "string" },
      { name: "token", type: "string" },
      { name: "amount", type: "string" },
      { name: "sourceDex", type: "string" },
      { name: "destinationRecipient", type: "string" },
      { name: "addressEncoding", type: "string" },
      { name: "destinationChainId", type: "uint32" },
      { name: "gasLimit", type: "uint64" },
      { name: "data", type: "bytes" },
      { name: "nonce", type: "uint64" },
    ],
  };

  // Message to sign
  const message = {
    hyperliquidChain,
    token: "USDC",
    amount: config.amount,
    sourceDex: config.sourceDex,
    destinationRecipient: recipientAta.toBase58(),
    addressEncoding: config.addressEncoding,
    destinationChainId: config.destinationChainId,
    gasLimit: BigInt(config.gasLimit),
    data: createAtaHookData,
    nonce: BigInt(timestamp),
  };

  // Sign the message using EIP-712
  const wallet = new Wallet(config.privateKey);
  const sigHex = await wallet.signTypedData(domain, types, message);
  const sig = Signature.from(sigHex);

  // Build action payload
  const action = {
    type: "sendToEvmWithData",
    hyperliquidChain,
    signatureChainId: config.signatureChainId,
    token: "USDC",
    amount: config.amount,
    sourceDex: config.sourceDex,
    destinationRecipient: recipientAta.toBase58(),
    addressEncoding: config.addressEncoding,
    destinationChainId: config.destinationChainId,
    gasLimit: config.gasLimit,
    data: createAtaHookData,
    nonce: timestamp,
  };

  // Submit to Hyperliquid exchange API
  const response = await fetch(`${apiUrl}/exchange`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      action,
      nonce: timestamp,
      signature: { r: sig.r, s: sig.s, v: sig.v },
    }),
  });

  const result = await response.json();

  console.log("\nStatus:", response.status);
  console.log("Response:", JSON.stringify(result, null, 2));

  if (response.status === 200 && result.status === "ok") {
    console.log("\nWithdrawal initiated successfully");
  } else {
    throw new Error(`Withdrawal failed: ${JSON.stringify(result)}`);
  }
}

// Run
main().catch((error) => {
  console.error("Error:", error.message);
  process.exit(1);
});
```

Run the script:

```bash theme={null}
npx tsx script.ts
```
