bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 39쪽
f it failed, retry the burn. |
| Attestation status remains `pending` | Waiting for block confirmations | Wait for sufficient confirmations based on finality threshold |
| Have attestation but mint fails | Gas issues, incorrect parameters, or nonce already used | See [Retry a failed mint](/cctp/howtos/retry-failed-mint) |
| `expirationBlock` has passed before you submitted the mint | Attestations encode an `expirationBlock` 24 hours in the future. Once that block passes on the destination blockchain, the existing attestation can no longer be used to mint. | Call [`POST /v2/reattest/{nonce}`](/api-reference/cctp/all/reattest-message) to request a new attestation with a refreshed `expirationBlock`, then poll [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) and re-mint. |
<Note>
**Expired messages are not permanently stuck**
There is no deadline after which re-attestation becomes unavailable. As long as
the original burn transaction still exists on the source blockchain, you can
request a fresh attestation at any time using the
[re-attest endpoint](/api-reference/cctp/all/reattest-message).
</Note>
# Withdraw USDC from HyperCore to EVM chains
Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-evm
This guide shows how to withdraw USDC from a HyperCore `spot` or `perp` balance
to an external EVM blockchain (such as Arbitrum, Ethereum, or Base) using the
HyperCore API. Withdrawals from HyperCore to EVM chains default to the Fast
Transfer method, due to the fast finality of HyperEVM.
The withdrawal process:
1. Debits your HyperCore balance (`spot` or `perp`)
2. Routes through HyperEVM where USDC is burned via CCTP
3. CCTP attests to the burn and mints on the destination chain
4. If automatic forwarding is enabled, the recipient receives funds directly
<Note>
Withdrawals include a HyperCore fee and (if using the Forwarding Service) a CCTP
forwarding fee. Ensure your withdrawal amount exceeds combined fees depending on
your transfer.
</Note>
## Important considerations
Keep these things in mind when withdrawing USDC from HyperCore to EVM chains:
* **Data field:** If the data field is empty, the `CoreDepositWallet`
automatically sets a default hook that enables automatic message forwarding on
the destination blockchain, provided that the blockchain supports CCTP
forwarding. If the data field is not empty, its contents are passed to the
CCTP protocol as the value of the `hookData` field.
* **Destination caller:** The CCTP `destinationCaller` is always set to the zero
address. Passing your own hook data means that anyone can receive the message
on the destination blockchain.
* **Withdrawal fees:** In addition to the `maxFee` charged by the HyperCore
blockchain, an additional fixed forwarding fee may be charged by CCTP if
automatic forwarding is enabled. The forwarding fee amount depends on the
destination blockchain and can be viewed by querying the `CoreDepositWallet`
smart contract. Initially, the fee for forwarding to Arbitrum is 0.2 USDC. If
the withdrawal includes custom hook data, the forwarding fee is not set and
users have to receive the message on the destination blockchain themselves.
* **Minimum withdrawal amount:** If the withdrawal amount is less than the
required forwarding fee, the transaction on HyperEVM reverts. Make sure the
withdrawal amount is larger than the fees.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.18+](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
npm install -D typescript @types/node
```
* Created a `.env` file with required environment variables:
```text theme={null}
PRIVATE_KEY=0x...
DESTINATION_RECIPIENT=0x... # Recipient address on destination chain
```
## Steps
Use the following steps to withdraw USDC from HyperCore to an EVM blockchain.
### Step 1. 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 address on the destination blockchain
* `addressEncoding`: `hex` for EVM chains or `base58` for Solana
* `destinationChainId`: The CCTP destination domain ID (for example, `3` for
Arbitrum, `0` for Ethereum, `6` for Base)
* `gasLimit`: Gas limit for the transaction on the destination chain
* `data`: CCTP hook data (use `"0x"` for automatic forwarding)
* `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: "0x1234567890123456789012345678901234567890",
addressEncoding: "hex",
destinationChainId: 3, // Arbitrum CCTP domain
gasLimit: 200000,
data: "0x", // "0x" enables automatic forwarding on the destination
nonce: Date.now(),
};
```
### Step 2. 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 = parseInt(signatureChainId, 16);
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: "0x...",
addressEncoding: "hex",
destinationChainId: 3,
gasLimit: BigInt(200000),
data: "0x",
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 3. 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: timestamp,
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 an
external EVM blockchain. By default, it withdraws 10 USDC from your perp balance
to Arbitrum testnet with automatic forwarding enabled. For other destination
chains, update `destinationChainId` (CCTP domain ID) and `signatureChainId` (the
ID of the chain used when signing in hexadecimal format) accordingly.
```ts TypeScript expandable theme={null}
/**
* Script: Withdraw USDC from HyperCore to EVM chain
* - Signs EIP-712 sendToEvmWithData action
* - Submits to Hyperliquid /exchange API
*/
import { Wallet, Signature } from "ethers";
// -------- 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
// Destination parameters
destinationRecipient: process.env.DESTINATION_RECIPIENT as string,
destinationChainId: Number(process.env.DESTINATION_CHAIN_ID || 3), // 3 = Arbitrum
addressEncoding: process.env.ADDRESS_ENCODING || "hex",
gasLimit: Number(process.env.GAS_LIMIT || 200000),
data: process.env.DATA || "0x", // "0x" enables automatic forwarding
// 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.destinationRecipient) {
throw new Error("Set DESTINATION_RECIPIENT");
}
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("Destination recipient:", config.destinationRecipient);
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: config.destinationRecipient,
addressEncoding: config.addressEncoding,
destinationChainId: config.destinationChainId,
gasLimit: BigInt(config.gasLimit),
data: config.data,
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: config.destinationRecipient,
addressEncoding: config.addressEncoding,
destinationChainId: config.destinationChainId,
gasLimit: config.gasLimit,
data: config.data,
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}
node --env-file=.env script.ts
```
# Withdraw USDC from HyperCore to HyperEVM
Source: https://developers.circle.com/cctp/howtos/withdraw-usdc-from-hypercore-to-hyperevm
This guide shows how to withdraw USDC from a HyperCore `spot` or `perp` balance
to HyperEVM using the HyperCore API.
<Note>
You can only withdraw USDC from HyperCore to the same address on HyperEVM. It's
not possible to specify a different recipient address.
</Note>
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.18+](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
npm install -D typescript @types/node
```
* Created a `.env` file with required environment variables:
```text theme={null}
PRIVATE_KEY=0x...
```
## Steps
Use the following steps to withdraw USDC from HyperCore to HyperEVM.
### Step 1. Construct the `sendAsset` action
Create a `sendAsset` action object with the following parameters:
* `type`: `sendAsset`
* `hyperliquidChain`: `Mainnet` (or `Testnet` for testnet)
* `signatureChainId`: An EVM chain ID used for EIP-712 replay protection. Must
match between signing and the action payload, but can be any valid chain ID
(for example, `"0xa4b1"` for Arbitrum)
* `destination`: The USDC token system address
(`0x2000000000000000000000000000000000000000`)
* `sourceDex`: `"spot"` to withdraw from spot balance, or `""` for perp balance
* `destinationDex`: `"spot"`
* `token`: `USDC`
* `amount`: The amount of USDC as a human-readable string (for example, `"10"`
for 10 USDC)
* `fromSubAccount`: Set to `""` for main account, or the subaccount address
* `nonce`: Current timestamp in milliseconds
```ts TypeScript theme={null}
const action = {
type: "sendAsset",
hyperliquidChain: "Testnet",
signatureChainId: "0xa4b1", // EVM chain ID for EIP-712 replay protection
destination: "0x2000000000000000000000000000000000000000",
sourceDex: "", // "" for perp, "spot" for spot
destinationDex: "spot",
token: "USDC",
amount: "10", // 10 USDC (human-readable)
fromSubAccount: "",
nonce: Date.now(),
};
```
### Step 2. 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";
async function signSendAssetAction(
action: any,
privateKey: string,
): Promise<{ r: string; s: string; v: number }> {
const wallet = new Wallet(privateKey);
// Convert chainId from hex to number
const chainId = parseInt(action.signatureChainId, 16);
// EIP-712 domain
const domain = {
name: "HyperliquidSignTransaction",
version: "1",
chainId,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
// EIP-712 types (must match Hyperliquid SDK's SEND_ASSET_SIGN_TYPES)
const types = {
"HyperliquidTransaction:SendAsset": [
{ name: "hyperliquidChain", type: "string" },
{ name: "destination", type: "string" },
{ name: "sourceDex", type: "string" },
{ name: "destinationDex", type: "string" },
{ name: "token", type: "string" },
{ name: "amount", type: "string" },
{ name: "fromSubAccount", type: "string" },
{ name: "nonce", type: "uint64" },
],
};
// Message to sign (only fields defined in EIP-712 types, not signatureChainId)
const value = {
hyperliquidChain: action.hyperliquidChain,
destination: action.destination,
sourceDex: action.sourceDex,
destinationDex: action.destinationDex,
token: action.token,
amount: action.amount,
fromSubAccount: action.fromSubAccount,
nonce: BigInt(action.nonce),
};
// Sign the typed data
const signature = await wallet.signTypedData(domain, types, value);
// Split signature into r, s, v components
const sig = Signature.from(signature);
return {
r: sig.r,
s: sig.s,
v: sig.v,
};
}
```
### Step 3. Submit the signed action to the exchange API
Call the
[exchange](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#send-asset)
endpoint with the action, nonce, and signature.
```ts TypeScript theme={null}
async function submitSendAsset(
action: any,
signature: { r: string; s: string; v: number },
) {
// Use https://api.hyperliquid.xyz for mainnet
const response = await fetch("https://api.hyperliquid-testnet.xyz/exchange", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action: action,
nonce: action.nonce,
signature: signature,
}),
});
const data = await response.json();
if (data.status === "ok") {
console.log("Withdrawal successful:", data);
return data;
} else {
throw new Error(`Withdrawal failed: ${JSON.stringify(data)}`);
}
}
```
## Full example code
The following is a complete example of how to withdraw USDC from HyperCore to
HyperEVM. By default, it withdraws 10 USDC from your perp balance to HyperEVM
testnet.
```ts TypeScript expandable theme={null}
/**
* Script: Withdraw USDC from HyperCore to HyperEVM
* - Constructs a SendAsset action
* - Signs the action using EIP-712
* - Submits the signed action to the HyperCore API
*/
import { Wallet, Signature } from "ethers";
// -------- Configuration --------
const config = {
privateKey: process.env.PRIVATE_KEY as string,
// Transfer parameters
amount: process.env.AMOUNT || "10", // 10 USDC (human-readable)
sourceDex: process.env.SOURCE_DEX || "", // "" for perp, "spot" for spot
// Hyperliquid environment
isMainnet:
String(process.env.HL_IS_MAINNET || "false").toLowerCase() === "true",
};
// System address for USDC token on HyperCore
const USDC_SYSTEM_ADDRESS = "0x2000000000000000000000000000000000000000";
// -------- Main Function --------
async function main() {
if (!config.privateKey) {
throw new Error("Set PRIVATE_KEY");
}
const apiUrl = config.isMainnet
? "https://api.hyperliquid.xyz"
: "https://api.hyperliquid-testnet.xyz";
const hyperliquidChain = config.isMainnet ? "Mainnet" : "Testnet";
const signingChainId = "0xa4b1"; // EVM chain ID for EIP-712 signing (any valid chain ID works)
const chainId = parseInt(signingChainId, 16);
const timestamp = Date.now();
const wallet = new Wallet(config.privateKey);
console.log("Withdrawing from HyperCore to HyperEVM:", hyperliquidChain);
console.log("User Address:", wallet.address);
console.log("Source balance:", config.sourceDex || "perp");
console.log("Amount (USDC):", config.amount);
// EIP-712 Domain
const domain = {
name: "HyperliquidSignTransaction",
version: "1",
chainId,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
// Build action for signing
const actionForSigning = {
hyperliquidChain,
signatureChainId: signingChainId,
destination: USDC_SYSTEM_ADDRESS,
sourceDex: config.sourceDex,
destinationDex: "spot",
token: "USDC",
amount: config.amount,
fromSubAccount: "",
nonce: timestamp,
};
// EIP-712 Types (must match Hyperliquid SDK's SEND_ASSET_SIGN_TYPES)
const types = {
"HyperliquidTransaction:SendAsset": [
{ nam프로젝트로 돌아가기