跳至主要內容

Solana 上的 SPL 代幣轉帳:完整指南

更新於
2025年11月26日

閱讀時間 19 分鐘

本指南提供 Solana Web3.js(舊版 v 1.x)和 Solana Kit(v 2.x)兩種版本。請選擇相應的分頁,以查看您偏好的函式庫所對應的程式碼片段和操作說明:

概覽

傳送 Solana 程式庫(SPL)代幣是 Solana 開發中的關鍵機制。無論您是向社群空投白名單代幣、批量將 NFT 傳送至其他錢包,還是管理託管帳戶之間的代幣流動,都不可避免地需要能夠轉移 SPL 代幣。轉移 SPL 代幣與傳送 SOL 略有不同,因此在您的 Solana 開發過程中,了解其運作原理至關重要。

您將負責的工作內容

在本指南中,您將使用 TypeScript 撰寫一個腳本,將 SPL 代幣從一個帳戶轉移到另一個帳戶,操作環境為 Solana 的開發網。

如果您想了解如何在使用 Anchor Framework 的程式中轉移 SPL 代幣和 SOL,請參閱我們的指南:《如何使用 Anchor 轉移 SOL 和 SPL 代幣》

想看影片教學嗎?跟著 Sahil 一起學習如何在 Solana Devnet 上轉移 SPL 代幣。
訂閱我們的 YouTube 頻道,觀看更多影片!

您需要準備的物品

  • 已安裝Node.js(版本 20.0 或更高)
  • 已具備 TypeScript 經驗,並已安裝ts-node

SPL 代幣帳戶概覽

在開始之前,了解 Solana 代幣計畫帳戶中的幾個組成部分——鑄造 ID 和相關聯的代幣帳戶——是如何運作的,將有所幫助。

Mint ID

每個 SPL 代幣都有一個唯一的鑄造 ID,可用於將其與任何其他類型的代幣區分開來。 例如,$USDC SPL 代幣的鑄造 ID 為EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,而 $SAMO 的鑄造 ID 則為7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU

值得注意的是,每個 NFT 都有一個獨一無二的鑄造地址(這正是使其具有不可替代性的部分原因)。例如, 「Famous Fox #4679」的鑄造 ID 為GS2AtoEoL9DgaYg9xC7cUumftB7vb2CPMrczZCKgJv1Y,而「Famous Fox #6562」的鑄造 ID 則為7FTdQdMqkk5Xc2oFsYR88BuJt2yyCPReTpqr3viH6b6C。 正如您稍後將看到的,這正是這兩枚代幣之間的一項重要區別。

若想進一步了解如何鑄造 SPL 代幣,請參閱我們的指南: 《如何使用新的 Metaplex 代幣標準創建可互換的 SPL 代幣》 以及 《如何在 Solana 上鑄造 NFT》

相關代幣帳戶

Solana 代幣計畫會「根據用戶的主要系統帳戶地址和代幣鑄造地址,生成一個代幣帳戶金鑰,讓用戶能夠為其持有的每種代幣建立一個主要代幣帳戶」(來源:spl.solana.com)。該帳戶被稱為「關聯代幣帳戶」(Associated Token Account,簡稱 ATA)。實際上,ATA 是一個與用戶及特定代幣鑄造相關聯的唯一帳戶。 舉例來說,假設我是帳號「DEMO...1234」的所有者。我可以擁有多個 ATA——每個 ATA 對應我持有的每種代幣鑄造(例如,每個 $USDC、$SAMO 等代幣各對應一個唯一的 ATA,如下圖所示)。

代幣轉帳必須始終發生在兩個與同一鑄幣地址相關聯的 ATA 之間。這意味著我們無法將 $USDC 從我的 ATA 轉帳至您的 $SAMO ATA(參見下方範例)。

正如您稍後將在我們的程式碼中看到的,並非每個帳戶都已針對每個鑄幣廠擁有 ATA。如果使用者先前未曾與某個代幣進行過互動,發送者必須「建立」該 ATA,並存入必要的租金,以維持該帳戶的活躍狀態。

這個概念起初可能有些棘手,但非常重要,所以請花點時間確保自己確實理解!我發現透過 Solana Explorer 瀏覽我的錢包,並查看代幣詳細資訊頁面(https://explorer.solana.com/address/YOUR_WALLET_ADDRESS/tokens?display=detail)會很有幫助。你可以在該頁面看到你擁有的每枚 ATA、其帳戶 ID 以及對應的鑄造 ID:

若您想進一步了解 Solana SPL 代幣計畫函式庫,請前往spl.solana.com

準備好開始寫程式了嗎?我也是!

設定您的專案

請在終端機中使用以下指令建立一個新的專案目錄:

mkdir spl-transfer-kit
cd spl-transfer-kit

為您的應用程式建立一個名為app.ts 的檔案:

echo > app.ts

請使用「yes」參數初始化您的專案,以便為新套件採用預設值:

yarn init --yes
#或
npm init --yes

建立一個採用現代模組解析方式的tsconfig.json檔案:

tsc --init 

請將您的tsconfig.json更新為以下內容:

{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"target": "ESNext"
}
}

安裝 Solana Kit 的依賴項

在本次練習中,我們需要安裝 Solana Kit 以及 Token 程式庫。請在終端機中輸入:

yarn add @solana/kit @solana-program/token
#或
npm install @solana/kit @solana-program/token

建立錢包並領取 SOL 空投

我們首先需要完成的任務,是建立一個附帶錢包的帳戶,並為其充值。我們將使用下方的便捷工具,自動生成一個新錢包,並向其中空投 1 SOL。

🔑使用 Devnet SOL建立新的錢包

注意:若過於頻繁地發送空投請求,可能會觸發 429(請求過多)錯誤。

成功產生金鑰對後,您會發現有兩個新的常數: 秘密 以及 FROM_KEYPAIR, 一組金鑰對。該 秘密 是一個 32 位元組的陣列,用於產生公鑰和私鑰。該 FROM_KEYPAIR 這是一個用於簽署交易的 Keypair 實例(我們已空投了一些開發網 SOL 來支付 gas 費用)。若您尚未將其加入程式碼中,請務必在其他常數下方將其加入。

在您的 `imports` 下方,貼上您的新秘密陣列:

const secret = [0,...,0]; // 👈 請替換為您的密鑰

鑄造或空投一些 SPL 代幣

要完成本指南,您需要在 Devnet 上取得一些 SPL 代幣。取得 Devnet SPL 代幣的方法有幾種——以下列舉其中幾種。

  1. 發行您自己的可互換代幣(指南:如何使用新的 Metaplex 代幣標準創建可互換的 SPL 代幣)。
  2. 使用 Candy Machine 鑄造一個或多個 NFT(指南:如何使用 Sugar(Candy Machine)在 Solana 上部署 NFT 系列》)。
  3. SPL 代幣水龍頭申請 $DUMMY 代幣空投。*
  4. 如果您已在其他錢包中持有 Devnet SPL 代幣,您可以透過 Phantom 或其他錢包介面將其轉出。*

在繼續之前,您應能前往 Solana 區塊鏈瀏覽器,並確認您的錢包在開發網(devnet)上擁有 SOL 餘額以及至少一種 SPL 代幣(https://explorer.solana.com/address/YOUR_WALLET_ADDRESS/tokens?cluster=devnet)。畫面應類似如下:

我們開始吧。

設定您的應用程式

匯入必要的依賴項

開啟app.ts 檔案,並將以下導入語句貼到第 1 行

import { 
createSolanaRpc,
createSolanaRpcSubscriptions,
address,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
airdropFactory,
lamports,
getSignatureFromTransaction,
createKeyPairFromBytes,
createSignerFromKeyPair
} from '@solana/kit';
import {
TOKEN_PROGRAM_ADDRESS,
fetchToken,
getTransferInstruction,
findAssociatedTokenPda,
getCreateAssociatedTokenIdempotentInstruction
} from '@solana-program/token';

除了在上一步驟中建立的錢包之外,我們還從 Solana Kit 和 Token 程式庫中導入了必要的方法和類別。

設定您的 Quicknode 端點

若要在 Solana 上進行開發,您需要一個 API 端點來連線至網路。您可以使用公開節點,或自行部署並管理基礎架構;不過,若您希望將回應時間縮短 8 倍,不妨將繁重的工作交給我們處理。了解為何超過 50% 的 Solana 專案選擇 Quicknode,並在此註冊帳號。我們將使用一個 Solana Devnet 節點。

複製 HTTP 提供者連結:

app.ts檔案中,於 import 語句下方宣告您的 RPC,並建立與 Solana 的連線:

const QUICKNODE_RPC = 'https://example.solana-devnet.quiknode.pro/0123456/';
const QUICKNODE_RPC_SUBSCRIPTIONS = 'wss://example.solana-devnet.quiknode.pro/0123456/';
const rpc = createSolanaRpc(QUICKNODE_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(QUICKNODE_RPC_SUBSCRIPTIONS);
const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });

請務必將 QUICKNODE_RPC 以及 QUICKNODE_RPC_SUBSCRIPTIONS 請填入您實際的 Quicknode 端點網址(來自 Quicknode 儀表板的 https 和 wss 連結)。

宣告變數

要執行您的腳本,您需要宣告幾個變數:目標帳戶(您希望將代幣轉帳給的持有者)、您要轉帳的代幣的鑄造地址,以及要轉帳的代幣數量。請在 RPC 設定下方加入以下宣告:

const DESTINATION_WALLET = 地址('DemoKMZWkk483hX4mUrcJoo3zVvsKhm8XXs28TuwZw9H'); 
const MINT_ADDRESS = 地址('DoJuta7joTSuuoozqQtjtnASRYiVsT435gh4srh5LLGK'); //您必須變更此值!
const TRANSFER_AMOUNT = 1n; // 使用 BigInt 來精確表示代幣金額

替換 MINT_ADDRESS 並填入您打算發送的代幣的鑄造地址。如果您不知道該代幣的鑄造地址,可以前往 https://explorer.solana.com/address/YOUR_WALLET_ADDRESS/tokens?cluster=devnet 然後點擊 複製 圖示以擷取您的 Mint 地址。將其貼上至 MINT_ADDRESS 變數。

注意:您也可以更新您的 DESTINATION_WALLET 地址及 TRANSFER_AMOUNT ,但在此範例中,這兩種數值皆可正常運作。

取得小數位數

如果您之前曾創建過可互換的 SPL 代幣,您就會知道小數位數至關重要。 由於區塊鏈將代幣供應量以整數值表示,我們必須根據代幣元數據中分配的小數位數來轉換代幣數量(例如,若鑄造時設定了三位小數,則必須傳送 1,000 單位才能有效轉移一個代幣 [1,000 / 10**3])。

要取得小數資料,我們將呼叫rpc實例上的getAccountInfo 函式,並解析 mint 帳戶資料。請在宣告語句下方加入此函式:

async function getNumberDecimals(mintAddress) {

// getAccountInfo defaults to binary, using `jsonParsed` lets us read parsed.info.decimals without manual byte offsets.
const accountInfo = await rpc.getAccountInfo(mintAddress, { encoding: 'jsonParsed' }).send();

const data = accountInfo.value?.data;
if (!data || !('parsed' in data) || !data.parsed?.info) {
throw new Error('Failed to find mint account');
}

return (data.parsed.info as { decimals: number }).decimals;
}

嚴格來說,如果你知道代幣鑄造時的小數位數(可在 Solana Explorer 的代幣頁面中查閱),其實就不需要這個參數。不過,我們還是希望將其納入,因為這樣一來,你就能在處理不同鑄造時修改程式碼,而無需變更其他變數。

現在就來談談有趣的部分吧!

建立傳遞函數

建立一個新的非同步函式sendTokens

async 函式 sendTokens() {
}

首先,我們需要根據秘密陣列建立簽署金鑰對,並為其注入 SOL。在 `sendTokens` 函式內部,新增步驟 1:

    console.log(`Sending ${TRANSFER_AMOUNT} ${MINT_ADDRESS} to ${DESTINATION_WALLET}.`)

//Step 1
console.log(`1 - Creating keypair and funding with SOL`);
const keyPair = await createKeyPairFromBytes(new Uint8Array(secret));
const FROM_KEYPAIR = await createSignerFromKeyPair(keyPair);


// Fund the keypair with SOL for transaction fees
// (you can skip this step if you already have devnet SOL in your FROM_KEYPAIR)
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: FROM_KEYPAIR.address,
lamports: lamports(1_000_000_000n), // 1 SOL
commitment: 'confirmed',
});

console.log(` From wallet: ${FROM_KEYPAIR.address}`);

注意:在正式環境中,您應使用 generateKeyPairFromBytes(new Uint8Array(secret)) 請使用您實際的密鑰,而非產生一個新的密鑰。

接下來,我們將為來源帳戶和目的地帳戶推導出「關聯代幣帳戶」(「ATA」)。請記住,每個帳戶都必須有一個 ATA,用以將錢包與鑄造方關聯起來;一個 ATA 只能持有單一鑄造方的代幣;且發送方與接收方的關聯代幣帳戶所對應的鑄造方必須相同。新增步驟 2:

    //Step 2
console.log(`2 - Deriving Associated Token Accounts`);

const [sourceTokenAccount] = await findAssociatedTokenPda({
owner: FROM_KEYPAIR.address,
mint: MINT_ADDRESS,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

const [destinationTokenAccount] = await findAssociatedTokenPda({
owner: DESTINATION_WALLET,
mint: MINT_ADDRESS,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

console.log(` Source Token Account: ${sourceTokenAccount}`);
console.log(` Destination Token Account: ${destinationTokenAccount}`);

接下來,我們需要檢查目標代幣帳戶是否存在,並在必要時建立該帳戶。新增步驟 3:

    console.log(`3 - Checking Destination Token Account`);
let needsDestinationAccount = false;
try {
await rpc.getAccountInfo(destinationTokenAccount).send();
} catch (error) {
needsDestinationAccount = true;
}

if (needsDestinationAccount) {
console.log(` Destination token account does not exist, will create it`);
} else {
console.log(` Destination token account exists`);
}

為了正確計算金額,讓我們取得小數位數。新增步驟 4:

    //Step 4
console.log(`4 - Fetching Number of Decimals for Mint: ${MINT_ADDRESS}`);
const numberDecimals = await getNumberDecimals(MINT_ADDRESS);
console.log(` Number of Decimals: ${numberDecimals}`);

const transferAmountWithDecimals = TRANSFER_AMOUNT * BigInt(Math.pow(10, numberDecimals));

最後,我們需要使用 Solana Kit 的函式式編程方法來組裝並發送交易。新增步驟 5:

    //Step 5
console.log(`5 - Creating and Sending Transaction`);

const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(FROM_KEYPAIR, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => {
// Add create ATA instruction if needed
if (needsDestinationAccount) {
return appendTransactionMessageInstruction(
getCreateAssociatedTokenIdempotentInstruction({
payer: FROM_KEYPAIR,
ata: destinationTokenAccount,
owner: DESTINATION_WALLET,
mint: MINT_ADDRESS,
}),
tx
);
}
return tx;
},
(tx) => appendTransactionMessageInstruction(
getTransferInstruction({
source: sourceTokenAccount,
destination: destinationTokenAccount,
authority: FROM_KEYPAIR,
amount: transferAmountWithDecimals,
}),
tx
)
);

const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransaction(signedTransaction, { commitment: 'confirmed' });
const signature = await getSignatureFromTransaction(signedTransaction);

console.log(
'\x1b[32m', //Green Text
` Transaction Success!🎉`,
`\n https://explorer.solana.com/tx/${signature}?cluster=devnet`
);

我們在此要做的,是指示我們的應用程式將TRANSFER_AMOUNT個代幣(請注意,必須乘以 10 的numberDecimals 次方)從sourceTokenAccount轉移至destinationTokenAccount

Solana Kit 的做法採用函式式程式設計風格,並結合了 管子 函式,用於逐步組建交易。這能提供更好的類型安全性,並使交易建構過程更加明確。此外,若目標 ATA 不存在,我們也會在同一筆交易中透過幂等指令來建立它;此指令即使帳戶已存在也不會失敗。

別在意那個 console.log——那只是個小技巧,用來讓成功訊息以綠色文字顯示。

🏃‍♂️ 執行您的程式碼

最後,在程式碼結尾處呼叫sendTokens

sendTokens().catch(控制台.error);

在終端機中,輸入以下指令來執行您的程式碼:

ts-node app.ts

恭喜!如果您已按照上述步驟操作,應該會看到類似以下的內容:

後續步驟與總結

太棒了!現在你已經知道如何將 SPL 代幣從一位使用者轉移給另一位使用者了!想更進一步嗎?以下提供幾個建議,讓你能將這項功能發揮得更徹底:

  1. 你能寫一個腳本,將你所有的 NFT 轉移到另一個錢包嗎?(提示:你可能需要先研究一下如何取得使用者錢包中的所有代幣)。

  2. 你能撰寫一個腳本,將 SPL 代幣透過 Airdrop 發放給一批錢包嗎?這些錢包可能與你的鑄造相關聯,也可能沒有。(提示:不妨參考我們關於批量發送 SOL 分發的指南,以獲取靈感)。

如果你願意試試看,或者有任何問題想問我們,請告訴我們!歡迎在DiscordTwitter 上找到我們!

我們 ❤️ 您的回饋!

如果您有任何意見回饋或對新主題的建議,請告訴我們。我們非常樂意聽取您的意見。

分享這份指南