bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 40쪽
e: "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 message = {
hyperliquidChain: actionForSigning.hyperliquidChain,
destination: actionForSigning.destination,
sourceDex: actionForSigning.sourceDex,
destinationDex: actionForSigning.destinationDex,
token: actionForSigning.token,
amount: actionForSigning.amount,
fromSubAccount: actionForSigning.fromSubAccount,
nonce: BigInt(actionForSigning.nonce),
};
// Sign the message using EIP-712
const sigHex = await wallet.signTypedData(domain, types, message);
const sig = Signature.from(sigHex);
// Build action payload for API (includes type and signatureChainId)
const action: any = {
type: "sendAsset",
...actionForSigning,
};
// 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
```
# How-to: Withdraw USDC from HyperCore to Solana
Source: https://developers.circle.com/cctp/howtos/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 to Solana have a higher CCTP forwarding fee for handling Associated
Token Account creation. If the withdrawal amount is less than this fee, 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
```
# Migrate from CCTP V1 (Legacy) to V2
Source: https://developers.circle.com/cctp/migration-from-v1-to-v2
Complete migration guide for developers upgrading CCTP integrations
This guide provides a summary of the breaking changes when migrating from
Cross-Chain Transfer Protocol (CCTP) V1 to V2. CCTP V2 introduces enhancements
including Fast Transfer, Hooks features, and improved API endpoints, but
requires updating your integration due to breaking changes.
<Important>
**Important**: CCTP V2 isn't backward compatible with V1. It uses separate
contracts, APIs, and transfer speeds. It also introduces new blockchain support,
while deprecating some chains. Plan for a complete integration update rather
than incremental changes.
Failure to migrate will eventually result in loss of crosschain capabilities for
your integration.
</Important>
The [Bridge](https://docs.arc.io/app-kit/bridge) capability in Arc App Kits can
help simplify your migration to CCTP V2. See the
[Migrating with App Kits](#migrating-with-app-kits) section for more
information.
## V1 deprecation
Circle is deprecating CCTP V1 to focus on the newer version, which is upgradable
and provides a faster, more secure, and more robust crosschain experience across
a wider network of blockchains.
### Naming changes
CCTP V2 is now referred to as CCTP (except in this document). The V1 version of
CCTP is now CCTP V1 (Legacy).
### Deprecation timeline
CCTP V1 (Legacy) deprecation begins October 31, 2026 and completes on December
1, 2026, in favor of CCTP V2. CCTP V2 contracts are available on all CCTP V1
chains except for Noble and Sui. Sui will be supported by V2 before the
deprecation begins.
### Access to funds
You will not lose access to funds during the V1 phase out. All pending
redemptions will remain available as CCTP V1 (legacy) begins its phase out.
Circle will maintain minter allowances greater than the total of pending
attestations, ensuring every redemption can be processed before V1 contracts are
fully paused.
The deprecation process is designed to wind down activity gradually, message
limits will tighten over time until no new burns can be initiated, bringing
transfer volume to zero before contracts are fully paused.
### Additional resources
In addition to this guide and [Arc App Kits](https://docs.arc.io/app-kit), you
can contact the Circle team on the
[BuildOnCircle Discord](https://discord.com/invite/buildoncircle) for questions
and migration support.
## Summary of breaking changes
The latest version of CCTP introduces architectural changes that make it
incompatible with V1 integrations. You must update your implementation to use
the new contracts, APIs, and transfer speeds. Additionally, the overall flow of
the protocol has been streamlined, which means you need to update your
integration to use the new functions.
* Contracts are deployed at
[different addresses](/cctp/references/contract-addresses) than V1 contracts.
You should update your integration to point to the new contract addresses.
* [Contract interfaces](/cctp/references/contract-interfaces) have changed.
Importantly, the
[`depositForBurn` function](/cctp/references/contract-interfaces#depositforburn)
now takes additional parameters. You should update your integration to use the
new ABIs and contract calls.
* CCTP now allows you to specify a transfer speed. The `finalityThreshold`
parameter specifies whether the transfer should be a
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
or a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times).
* You no longer need to extract the message from the onchain transaction to
fetch an attestation. Instead, you can call the new
`/v2/messages/{sourceDomainId}` endpoint with the transaction hash to get the
message and attestation in a single call.
* API endpoints have changed. The new `/v2/` endpoints have different functions
than the old `/v1/` endpoints. You should update your integration to use the
new endpoints. Review the
[CCTP API reference](/api-reference/cctp/all/get-public-keys-v2) for details
on the changes to the CCTP offchain API.
* [Fees](/cctp/concepts/fees) have been introduced. Fast Transfer has a variable
fee based on the source chain. You should update your integration to account
for the new fees.
## Migrating with App Kits
[Arc App Kits](https://docs.arc.io/app-kit) provides a simplified migration path
by abstracting routine setup steps and standardizing bridging flows. This
enables you to integrate bridging operations with minimal code.
### Benefits of using App Kits to bridge
* **No contract management**: App Kits handles contract addresses, ABIs, and
function calls for you.
* **No attestation polling**: Automatically retrieves attestations without
manual API calls.
* **Built-in CCTP features**: Access Fast Transfer and other capabilities
through simple configuration.
* **Type-safe interface**: Compatible with `viem` and `ethers` for safer
development.
* **Fee collection**: Optionally collect fees from transfers to monetize your
application.
### Example migration
Replace manual contract calls and API polling with a single method:
```typescript theme={null}
import { AppKit } from "@circle-fin/app-kit";
import { createViemAdapterFromPrivateKey } from "@circle-fin/adapter-viem-v2";
// Initialize the App Kit SDK
const kit = new AppKit();
// Create adapter for your wallet
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
});
// Transfer USDC with Fast Transfer
const result = await kit.bridge({
from: { adapter, chain: "Ethereum" },
to: { adapter, chain: "Base" },
amount: "100",
config: {
transferSpeed: "FAST", // Use Fast Transfer
maxFee: "5000000", // Max 5 USDC fee (optional)
},
});
// Result includes transaction details and explorer URLs
console.log("Transfer complete:", result.steps);
```
프로젝트로 돌아가기