如何通过“@solana/web3.js”和“@solana/sol-wallet-adapter”传输自定义 SPL 令牌
How to transfer custom SPL token by '@solana/web3.js' and '@solana/sol-wallet-adapter'
您好,我正在尝试使用 solana 钱包适配器传输自定义 SPL 令牌。
但是我无法获取钱包的秘密 key/signing 交易。
我已经查看了这些关于编写传输代码的答案,但我需要获取 Singer,但我无法弄清楚如何使用 solana 钱包适配器。这些示例对密钥进行了硬编码,因为我使用的是钱包扩展,所以这是不可能的。
根据 webadapter repo https://github.com/solana-labs/wallet-adapter/issues/120 上的这个问题,您需要:
- 创建一个@solana/web3.js 事务对象并向其添加指令
- 用钱包签署交易
- 通过连接发送交易
但是我很难找到关于如何执行第 1 步和第 2 步的示例或文档。
const SendTransaction: React.FC<Props> = ({ children }) => {
const { connection } = useConnection()
const { publicKey, sendTransaction } = useWallet()
const onSendSPLTransaction = useCallback(
async (toPubkey: string, amount: number) => {
if (!toPubkey || !amount) return
const toastId = toast.loading('Processing transaction...')
try {
if (!publicKey) throw new WalletNotConnectedError()
const toPublicKey = new PublicKey(toPubkey)
const mint = new PublicKey('Mint address')
const payer = '????' // how to get this Signer
const token = new Token(connection, mint, TOKEN_PROGRAM_ID, payer)
const fromTokenAccount = await token.getOrCreateAssociatedAccountInfo(publicKey)
const toTokenAccount = await token.getOrCreateAssociatedAccountInfo(toPublicKey)
const transaction = new Transaction().add(
Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
fromTokenAccount.address,
toTokenAccount.address,
publicKey,
[],
0
)
)
const signature = await sendTransaction(transaction, connection)
const response = await connection.confirmTransaction(signature, 'processed')
console.log('response', response)
toast.success('Transaction sent', {
id: toastId,
})
} catch (error) {
toast.error(`Transaction failed: ${error.message}`, {
id: toastId,
})
}
},
[publicKey, sendTransaction, connection]
)
return <>{children(onSendSPLTransaction)}</>
}
更新
我在 sol-wallet-adapter 的 github 回购上发布了这个问题并得到了这个回复:https://github.com/solana-labs/wallet-adapter/issues/189
所以我找到了一种方法来做到这一点,它需要一些清理和错误处理,但允许通过 @solana/wallet-adapter
进行自定义令牌交易。
// sendTransaction.tsx
import { WalletNotConnectedError } from '@solana/wallet-adapter-base'
import { useConnection, useWallet } from '@solana/wallet-adapter-react'
import { Transaction, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'
import React, { useCallback } from 'react'
import { toast } from 'react-hot-toast'
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { getOrCreateAssociatedTokenAccount } from './getOrCreateAssociatedTokenAccount'
import { createTransferInstruction } from './createTransferInstructions'
interface Props {
children: (sendTransaction: OnSendTransaction) => React.ReactNode
}
type OnSendTransaction = (toPublicKey: string, amount: number) => void
// Docs: https://github.com/solana-labs/solana-program-library/pull/2539/files
// https://github.com/solana-labs/wallet-adapter/issues/189
// repo: https://github.com/solana-labs/example-token/blob/v1.1/src/client/token.js
// creating a token for testing: https://learn.figment.io/tutorials/sol-mint-token
const SendTransaction: React.FC<Props> = ({ children }) => {
const { connection } = useConnection()
const { publicKey, signTransaction, sendTransaction } = useWallet()
const onSendSPLTransaction = useCallback(
async (toPubkey: string, amount: number) => {
if (!toPubkey || !amount) return
const toastId = toast.loading('Processing transaction...')
try {
if (!publicKey || !signTransaction) throw new WalletNotConnectedError()
const toPublicKey = new PublicKey(toPubkey)
const mint = new PublicKey('MINT ADDRESS')
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
publicKey,
signTransaction
)
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
toPublicKey,
signTransaction
)
const transaction = new Transaction().add(
createTransferInstruction(
fromTokenAccount.address, // source
toTokenAccount.address, // dest
publicKey,
amount * LAMPORTS_PER_SOL,
[],
TOKEN_PROGRAM_ID
)
)
const blockHash = await connection.getRecentBlockhash()
transaction.feePayer = await publicKey
transaction.recentBlockhash = await blockHash.blockhash
const signed = await signTransaction(transaction)
await connection.sendRawTransaction(signed.serialize())
toast.success('Transaction sent', {
id: toastId,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(`Transaction failed: ${error.message}`, {
id: toastId,
})
}
},
[publicKey, sendTransaction, connection]
)
return <>{children(onSendSPLTransaction)}</>
}
export default SendTransaction
// getOrCreateAssociatedTokenAccount.ts
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { SignerWalletAdapterProps } from '@solana/wallet-adapter-base'
import { Connection, PublicKey, Commitment, Transaction } from '@solana/web3.js'
import { createAssociatedTokenAccountInstruction } from './createAssociatedTokenAccountInstruction'
import { getAccountInfo } from './getAccountInfo'
import { getAssociatedTokenAddress } from './getAssociatedTokerAddress'
export async function getOrCreateAssociatedTokenAccount(
connection: Connection,
payer: PublicKey,
mint: PublicKey,
owner: PublicKey,
signTransaction: SignerWalletAdapterProps['signTransaction'],
allowOwnerOffCurve = false,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
) {
const associatedToken = await getAssociatedTokenAddress(
mint,
owner,
allowOwnerOffCurve,
programId,
associatedTokenProgramId
)
// This is the optimal logic, considering TX fee, client-side computation, RPC roundtrips and guaranteed idempotent.
// Sadly we can't do this atomically.
let account
try {
account = await getAccountInfo(connection, associatedToken, commitment, programId)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// TokenAccountNotFoundError can be possible if the associated address has already received some lamports,
// becoming a system account. Assuming program derived addressing is safe, this is the only case for the
// TokenInvalidAccountOwnerError in this code path.
if (error.message === 'TokenAccountNotFoundError' || error.message === 'TokenInvalidAccountOwnerError') {
// As this isn't atomic, it's possible others can create associated accounts meanwhile.
try {
const transaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
payer,
associatedToken,
owner,
mint,
programId,
associatedTokenProgramId
)
)
const blockHash = await connection.getRecentBlockhash()
transaction.feePayer = await payer
transaction.recentBlockhash = await blockHash.blockhash
const signed = await signTransaction(transaction)
const signature = await connection.sendRawTransaction(signed.serialize())
await connection.confirmTransaction(signature)
} catch (error: unknown) {
// Ignore all errors; for now there is no API-compatible way to selectively ignore the expected
// instruction error if the associated account exists already.
}
// Now this should always succeed
account = await getAccountInfo(connection, associatedToken, commitment, programId)
} else {
throw error
}
}
if (!account.mint.equals(mint.toBuffer())) throw Error('TokenInvalidMintError')
if (!account.owner.equals(owner.toBuffer())) throw new Error('TokenInvalidOwnerError')
return account
}
// createAssociatedTokenAccountInstruction.ts
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'
export function createAssociatedTokenAccountInstruction(
payer: PublicKey,
associatedToken: PublicKey,
owner: PublicKey,
mint: PublicKey,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
): TransactionInstruction {
const keys = [
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: associatedToken, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: false, isWritable: false },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: programId, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
]
return new TransactionInstruction({
keys,
programId: associatedTokenProgramId,
data: Buffer.alloc(0),
})
}
// createTransferInstructions.ts
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { AccountMeta, PublicKey, Signer, TransactionInstruction } from '@solana/web3.js'
import BufferLayout from 'buffer-layout'
import BN from 'bn.js'
export enum TokenInstruction {
InitializeMint = 0,
InitializeAccount = 1,
InitializeMultisig = 2,
Transfer = 3,
Approve = 4,
Revoke = 5,
SetAuthority = 6,
MintTo = 7,
Burn = 8,
CloseAccount = 9,
FreezeAccount = 10,
ThawAccount = 11,
TransferChecked = 12,
ApproveChecked = 13,
MintToChecked = 14,
BurnChecked = 15,
InitializeAccount2 = 16,
SyncNative = 17,
InitializeAccount3 = 18,
InitializeMultisig2 = 19,
InitializeMint2 = 20,
}
/**
* Construct a Transfer instruction
*
* @param source Source account
* @param destination Destination account
* @param owner Owner of the source account
* @param amount Number of tokens to transfer
* @param multiSigners Signing accounts if `owner` is a multisig
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createTransferInstruction(
source: PublicKey,
destination: PublicKey,
owner: PublicKey,
amount: number,
multiSigners: Signer[] = [],
programId = TOKEN_PROGRAM_ID
): TransactionInstruction {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.blob(8, 'amount')])
const keys = addSigners(
[
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
],
owner,
multiSigners
)
const data = Buffer.alloc(dataLayout.span)
dataLayout.encode(
{
instruction: TokenInstruction.Transfer,
amount: new TokenAmount(amount).toBuffer(),
},
data
)
return new TransactionInstruction({ keys, programId, data })
}
export function addSigners(keys: AccountMeta[], ownerOrAuthority: PublicKey, multiSigners: Signer[]): AccountMeta[] {
if (multiSigners.length) {
keys.push({ pubkey: ownerOrAuthority, isSigner: false, isWritable: false })
for (const signer of multiSigners) {
keys.push({ pubkey: signer.publicKey, isSigner: true, isWritable: false })
}
} else {
keys.push({ pubkey: ownerOrAuthority, isSigner: true, isWritable: false })
}
return keys
}
class TokenAmount extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse()
const b = Buffer.from(a)
if (b.length === 8) {
return b
}
if (b.length >= 8) {
throw new Error('TokenAmount too large')
}
const zeroPad = Buffer.alloc(8)
b.copy(zeroPad)
return zeroPad
}
/**
* Construct a TokenAmount from Buffer representation
*/
static fromBuffer(buffer: Buffer): TokenAmount {
if (buffer.length !== 8) {
throw new Error(`Invalid buffer length: ${buffer.length}`)
}
return new BN(
[...buffer]
.reverse()
.map((i) => `00${i.toString(16)}`.slice(-2))
.join(''),
16
)
}
}
// getAccountInfo.ts
import { TOKEN_PROGRAM_ID, AccountLayout } from '@solana/spl-token'
import { Connection, PublicKey, Commitment } from '@solana/web3.js'
export enum AccountState {
Uninitialized = 0,
Initialized = 1,
Frozen = 2,
}
export async function getAccountInfo(
connection: Connection,
address: PublicKey,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const info = await connection.getAccountInfo(address, commitment)
if (!info) throw new Error('TokenAccountNotFoundError')
if (!info.owner.equals(programId)) throw new Error('TokenInvalidAccountOwnerError')
if (info.data.length != AccountLayout.span) throw new Error('TokenInvalidAccountSizeError')
const rawAccount = AccountLayout.decode(Buffer.from(info.data))
return {
address,
mint: rawAccount.mint,
owner: rawAccount.owner,
amount: rawAccount.amount,
delegate: rawAccount.delegateOption ? rawAccount.delegate : null,
delegatedAmount: rawAccount.delegatedAmount,
isInitialized: rawAccount.state !== AccountState.Uninitialized,
isFrozen: rawAccount.state === AccountState.Frozen,
isNative: !!rawAccount.isNativeOption,
rentExemptReserve: rawAccount.isNativeOption ? rawAccount.isNative : null,
closeAuthority: rawAccount.closeAuthorityOption ? rawAccount.closeAuthority : null,
}
}
// getAssociatedTokerAddress.ts
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { PublicKey } from '@solana/web3.js'
export async function getAssociatedTokenAddress(
mint: PublicKey,
owner: PublicKey,
allowOwnerOffCurve = false,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
): Promise<PublicKey> {
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) throw new Error('TokenOwnerOffCurveError')
const [address] = await PublicKey.findProgramAddress(
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
associatedTokenProgramId
)
return address
}
希望这对其他人有帮助。如果哪位有意见指点欢迎评论。
这是您的代码的一个变体,它可以工作并且不需要创建我们自己的传输指令。只需要getOrCreateAssociatedTokenAccount、createAssociatedTokenAccountInstruction、getAccountInfo和getAssociatedTokenAddress(这些功能在另一个答案中指定)。
创建转账指令需要导入spl-token。
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
publicKey,
signTransaction
)
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
toPublicKey,
signTransaction
)
const transaction = new web3.Transaction().add(
Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
fromTokenAccount.address,
toTokenAccount.address,
publicKey,
[],
1
)
)
const signature = await sendTransaction(transaction, connection)
const response = await connection.confirmTransaction(signature, 'processed')
console.log('response', response)
下面是生成密钥、创建代币、为铸币商创建代币账户、将代币铸造到代币账户以及最后将代币转移到另一个代币账户的示例
const splToken = require("@solana/spl-token");
const web3 = require("@solana/web3.js");
const test = async () => {
console.log("solana test");
const cluster_url = "https://api.testnet.solana.com";
//create keys
const mint_authority = web3.Keypair.generate();
const freeze_authority = web3.Keypair.generate();
const connection = new web3.Connection(cluster_url, "confirmed");
//airdrop to mint authority
const airdropSignature = await connection.requestAirdrop(
mint_authority.publicKey,
web3.LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
console.log("mint authority pub key", mint_authority.publicKey.toString());
//check balance of mint authority
console.log(
"SOL balance of minter",
await connection.getBalance(mint_authority.publicKey)
);
//create (but not mint) a new token
const token = await splToken.Token.createMint(
connection,
mint_authority,
mint_authority.publicKey,
null,
9,
splToken.TOKEN_PROGRAM_ID
);
console.log(
"new SOL balance of minter after token creation activity",
await connection.getBalance(mint_authority.publicKey)
);
console.log("new spl token address is", token.publicKey.toString());
//lets mint some of the token INTO the mint authority wallet
const minter_token_account = await token.getOrCreateAssociatedAccountInfo(
mint_authority.publicKey
);
console.log(
"minter_token_account address",
minter_token_account.address.toString()
);
console.log("minting some supply into token account we made for the minter");
console.log(
"minter_token_account.mint (same as token address)",
minter_token_account.mint.toString()
);
console.log(
"minter_token_account.owner (same as pub key of token account)",
minter_token_account.owner.toString()
);
// Mint the tokens - how am I allowed to do this just my using mint_authority.publicKey as the authority, shouldnt I need
// a private key to do this or is it just because I have a 'powerful' reference to the token because of earlier createMint() ????
await token.mintTo(
minter_token_account.address,
mint_authority.publicKey,
[],
10000000
);
//get balance of sol for minter
console.log(
"new SOL balance of minter after token minting activity",
await connection.getBalance(mint_authority.publicKey)
);
//token accounts by owner
const tokenAccountsByOwner = await connection.getTokenAccountsByOwner(
mint_authority.publicKey,
{
mint: token.publicKey,
}
);
console.log(
"tokenAccountsByOwner - token account address",
tokenAccountsByOwner.value[0].pubkey.toString()
);
// console.log(
// "tokenAccountsByOwner - token account lamports",
// tokenAccountsByOwner.value[0].account.lamports
// );
//get token account balance
const tokenAccountBalance = await connection.getTokenAccountBalance(
minter_token_account.address
);
console.log(
"token account balance:",
tokenAccountBalance.value.uiAmountString
);
//now lets create a new solana address
const bob = web3.Keypair.generate();
console.log("bob public address:", bob.publicKey.toString());
console.log(
"SOL balance of minter just before creating account for bob",
await connection.getBalance(mint_authority.publicKey)
);
//create token account fot this address
const bob_token_account = await token.getOrCreateAssociatedAccountInfo(
bob.publicKey
);
console.log(
"SOL balance of minter just after creating account for bob",
await connection.getBalance(mint_authority.publicKey)
); //seems to cost 2044280 lamports .002 SOL
console.log(
"bob_token_account address",
bob_token_account.address.toString()
);
console.log(
"bob_token_account.mint (same as token address)",
bob_token_account.mint.toString()
);
console.log(
"bob_token_account.owner (same as pub key of token account)",
bob_token_account.owner.toString()
);
//transfer from minter wallet to bob
var transferTrx = new web3.Transaction().add(
splToken.Token.createTransferInstruction(
splToken.TOKEN_PROGRAM_ID,
minter_token_account.address,
bob_token_account.address,
mint_authority.publicKey,
[],
1
)
);
let bobTokenAccountBalance = await connection.getTokenAccountBalance(
bob_token_account.address
);
console.log(
"bob_token_account balance before transfer:",
bobTokenAccountBalance.value.uiAmountString
);
console.log(
"SOL balance of minter just before transfering to bob",
await connection.getBalance(mint_authority.publicKey)
);
var signature = await web3.sendAndConfirmTransaction(
connection,
transferTrx,
[mint_authority]
);
console.log(
"SOL balance of minter just AFTER transfering to bob",
await connection.getBalance(mint_authority.publicKey)
); // seems to cost 5000 lamports or .000005 SOL
bobTokenAccountBalance = await connection.getTokenAccountBalance(
bob_token_account.address
);
console.log(
"bob_token_account balance after transfer:",
bobTokenAccountBalance.value.uiAmountString
);
};
return test()
.then()
.catch((err) => {
if (err.logs) {
console.log(err.logs);
} else {
console.error(err.message);
}
});
也可以是:
1、改写:getOrCreateAssociatedTokenAccount函数
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
getAssociatedTokenAddress,
getAccount,
createAssociatedTokenAccountInstruction,
TokenAccountNotFoundError,
TokenInvalidAccountOwnerError,
TokenInvalidMintError,
TokenInvalidOwnerError,
} from '@solana/spl-token'
import { Transaction } from '@solana/web3.js'
/**
* (rewrite)Retrieve the associated token account, or create it if it doesn't exist
*
* @param connection Connection to use
* @param payer Payer of the transaction and initialization fees
* @param mint Mint associated with the account to set or verify
* @param owner Owner of the account to set or verify
* @param sendTransaction
* @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address)
* @param commitment Desired level of commitment for querying the state
* @param programId SPL Token program account
* @param associatedTokenProgramId SPL Associated Token program account
*
* @return Address of the new associated token account
*/
export async function getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
owner,
sendTransaction,
allowOwnerOffCurve = false,
commitment,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
) {
const associatedToken = await getAssociatedTokenAddress(
mint,
owner,
allowOwnerOffCurve,
programId,
associatedTokenProgramId,
)
// This is the optimal logic, considering TX fee, client-side computation, RPC roundtrips and guaranteed idempotent.
// Sadly we can't do this atomically.
let account
try {
account = await getAccount(connection, associatedToken, commitment, programId)
} catch (error) {
// TokenAccountNotFoundError can be possible if the associated address has already received some lamports,
// becoming a system account. Assuming program derived addressing is safe, this is the only case for the
// TokenInvalidAccountOwnerError in this code path.
if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) {
// As this isn't atomic, it's possible others can create associated accounts meanwhile.
try {
const transaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
payer,
associatedToken,
owner,
mint,
programId,
associatedTokenProgramId,
),
)
const signature = await sendTransaction(transaction, connection)
await connection.confirmTransaction(signature)
} catch (error) {
// Ignore all errors; for now there is no API-compatible way to selectively ignore the expected
// instruction error if the associated account exists already.
}
// Now this should always succeed
account = await getAccount(connection, associatedToken, commitment, programId)
} else {
throw error
}
}
if (!account.mint.equals(mint)) throw new TokenInvalidMintError()
if (!account.owner.equals(owner)) throw new TokenInvalidOwnerError()
return account
}
2、转移spl代币
import { Transaction, PublicKey } from '@solana/web3.js'
import { createTransferCheckedInstruction } from '@solana/spl-token'
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
try {
const { sendTransaction, publicKey } = useWallet()
const { connection } = useConnection()
// step 1: create transaction
const toPublicKey = new PublicKey('')
const mint = new PublicKey('token address')
const transaction = new Transaction()
// up rewrite function
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
publicKey,
sendTransaction,
)
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
toPublicKey,
sendTransaction,
)
const instruction = createTransferCheckedInstruction(
fromTokenAccount.address,
mint,
toTokenAccount.address,
publicKey,
1,
0,
)
transaction.add(instruction)
// step 2: sign&send transaction
const result = await sendTransaction(transaction, connection)
} catch (err) {
// err handle
}
您好,我正在尝试使用 solana 钱包适配器传输自定义 SPL 令牌。 但是我无法获取钱包的秘密 key/signing 交易。
我已经查看了这些关于编写传输代码的答案,但我需要获取 Singer,但我无法弄清楚如何使用 solana 钱包适配器。这些示例对密钥进行了硬编码,因为我使用的是钱包扩展,所以这是不可能的。
根据 webadapter repo https://github.com/solana-labs/wallet-adapter/issues/120 上的这个问题,您需要:
- 创建一个@solana/web3.js 事务对象并向其添加指令
- 用钱包签署交易
- 通过连接发送交易
但是我很难找到关于如何执行第 1 步和第 2 步的示例或文档。
const SendTransaction: React.FC<Props> = ({ children }) => {
const { connection } = useConnection()
const { publicKey, sendTransaction } = useWallet()
const onSendSPLTransaction = useCallback(
async (toPubkey: string, amount: number) => {
if (!toPubkey || !amount) return
const toastId = toast.loading('Processing transaction...')
try {
if (!publicKey) throw new WalletNotConnectedError()
const toPublicKey = new PublicKey(toPubkey)
const mint = new PublicKey('Mint address')
const payer = '????' // how to get this Signer
const token = new Token(connection, mint, TOKEN_PROGRAM_ID, payer)
const fromTokenAccount = await token.getOrCreateAssociatedAccountInfo(publicKey)
const toTokenAccount = await token.getOrCreateAssociatedAccountInfo(toPublicKey)
const transaction = new Transaction().add(
Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
fromTokenAccount.address,
toTokenAccount.address,
publicKey,
[],
0
)
)
const signature = await sendTransaction(transaction, connection)
const response = await connection.confirmTransaction(signature, 'processed')
console.log('response', response)
toast.success('Transaction sent', {
id: toastId,
})
} catch (error) {
toast.error(`Transaction failed: ${error.message}`, {
id: toastId,
})
}
},
[publicKey, sendTransaction, connection]
)
return <>{children(onSendSPLTransaction)}</>
}
更新
我在 sol-wallet-adapter 的 github 回购上发布了这个问题并得到了这个回复:https://github.com/solana-labs/wallet-adapter/issues/189
所以我找到了一种方法来做到这一点,它需要一些清理和错误处理,但允许通过 @solana/wallet-adapter
进行自定义令牌交易。
// sendTransaction.tsx
import { WalletNotConnectedError } from '@solana/wallet-adapter-base'
import { useConnection, useWallet } from '@solana/wallet-adapter-react'
import { Transaction, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'
import React, { useCallback } from 'react'
import { toast } from 'react-hot-toast'
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { getOrCreateAssociatedTokenAccount } from './getOrCreateAssociatedTokenAccount'
import { createTransferInstruction } from './createTransferInstructions'
interface Props {
children: (sendTransaction: OnSendTransaction) => React.ReactNode
}
type OnSendTransaction = (toPublicKey: string, amount: number) => void
// Docs: https://github.com/solana-labs/solana-program-library/pull/2539/files
// https://github.com/solana-labs/wallet-adapter/issues/189
// repo: https://github.com/solana-labs/example-token/blob/v1.1/src/client/token.js
// creating a token for testing: https://learn.figment.io/tutorials/sol-mint-token
const SendTransaction: React.FC<Props> = ({ children }) => {
const { connection } = useConnection()
const { publicKey, signTransaction, sendTransaction } = useWallet()
const onSendSPLTransaction = useCallback(
async (toPubkey: string, amount: number) => {
if (!toPubkey || !amount) return
const toastId = toast.loading('Processing transaction...')
try {
if (!publicKey || !signTransaction) throw new WalletNotConnectedError()
const toPublicKey = new PublicKey(toPubkey)
const mint = new PublicKey('MINT ADDRESS')
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
publicKey,
signTransaction
)
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
toPublicKey,
signTransaction
)
const transaction = new Transaction().add(
createTransferInstruction(
fromTokenAccount.address, // source
toTokenAccount.address, // dest
publicKey,
amount * LAMPORTS_PER_SOL,
[],
TOKEN_PROGRAM_ID
)
)
const blockHash = await connection.getRecentBlockhash()
transaction.feePayer = await publicKey
transaction.recentBlockhash = await blockHash.blockhash
const signed = await signTransaction(transaction)
await connection.sendRawTransaction(signed.serialize())
toast.success('Transaction sent', {
id: toastId,
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(`Transaction failed: ${error.message}`, {
id: toastId,
})
}
},
[publicKey, sendTransaction, connection]
)
return <>{children(onSendSPLTransaction)}</>
}
export default SendTransaction
// getOrCreateAssociatedTokenAccount.ts
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { SignerWalletAdapterProps } from '@solana/wallet-adapter-base'
import { Connection, PublicKey, Commitment, Transaction } from '@solana/web3.js'
import { createAssociatedTokenAccountInstruction } from './createAssociatedTokenAccountInstruction'
import { getAccountInfo } from './getAccountInfo'
import { getAssociatedTokenAddress } from './getAssociatedTokerAddress'
export async function getOrCreateAssociatedTokenAccount(
connection: Connection,
payer: PublicKey,
mint: PublicKey,
owner: PublicKey,
signTransaction: SignerWalletAdapterProps['signTransaction'],
allowOwnerOffCurve = false,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
) {
const associatedToken = await getAssociatedTokenAddress(
mint,
owner,
allowOwnerOffCurve,
programId,
associatedTokenProgramId
)
// This is the optimal logic, considering TX fee, client-side computation, RPC roundtrips and guaranteed idempotent.
// Sadly we can't do this atomically.
let account
try {
account = await getAccountInfo(connection, associatedToken, commitment, programId)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
// TokenAccountNotFoundError can be possible if the associated address has already received some lamports,
// becoming a system account. Assuming program derived addressing is safe, this is the only case for the
// TokenInvalidAccountOwnerError in this code path.
if (error.message === 'TokenAccountNotFoundError' || error.message === 'TokenInvalidAccountOwnerError') {
// As this isn't atomic, it's possible others can create associated accounts meanwhile.
try {
const transaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
payer,
associatedToken,
owner,
mint,
programId,
associatedTokenProgramId
)
)
const blockHash = await connection.getRecentBlockhash()
transaction.feePayer = await payer
transaction.recentBlockhash = await blockHash.blockhash
const signed = await signTransaction(transaction)
const signature = await connection.sendRawTransaction(signed.serialize())
await connection.confirmTransaction(signature)
} catch (error: unknown) {
// Ignore all errors; for now there is no API-compatible way to selectively ignore the expected
// instruction error if the associated account exists already.
}
// Now this should always succeed
account = await getAccountInfo(connection, associatedToken, commitment, programId)
} else {
throw error
}
}
if (!account.mint.equals(mint.toBuffer())) throw Error('TokenInvalidMintError')
if (!account.owner.equals(owner.toBuffer())) throw new Error('TokenInvalidOwnerError')
return account
}
// createAssociatedTokenAccountInstruction.ts
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { PublicKey, TransactionInstruction, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'
export function createAssociatedTokenAccountInstruction(
payer: PublicKey,
associatedToken: PublicKey,
owner: PublicKey,
mint: PublicKey,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
): TransactionInstruction {
const keys = [
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: associatedToken, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: false, isWritable: false },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: programId, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
]
return new TransactionInstruction({
keys,
programId: associatedTokenProgramId,
data: Buffer.alloc(0),
})
}
// createTransferInstructions.ts
import { TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { AccountMeta, PublicKey, Signer, TransactionInstruction } from '@solana/web3.js'
import BufferLayout from 'buffer-layout'
import BN from 'bn.js'
export enum TokenInstruction {
InitializeMint = 0,
InitializeAccount = 1,
InitializeMultisig = 2,
Transfer = 3,
Approve = 4,
Revoke = 5,
SetAuthority = 6,
MintTo = 7,
Burn = 8,
CloseAccount = 9,
FreezeAccount = 10,
ThawAccount = 11,
TransferChecked = 12,
ApproveChecked = 13,
MintToChecked = 14,
BurnChecked = 15,
InitializeAccount2 = 16,
SyncNative = 17,
InitializeAccount3 = 18,
InitializeMultisig2 = 19,
InitializeMint2 = 20,
}
/**
* Construct a Transfer instruction
*
* @param source Source account
* @param destination Destination account
* @param owner Owner of the source account
* @param amount Number of tokens to transfer
* @param multiSigners Signing accounts if `owner` is a multisig
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export function createTransferInstruction(
source: PublicKey,
destination: PublicKey,
owner: PublicKey,
amount: number,
multiSigners: Signer[] = [],
programId = TOKEN_PROGRAM_ID
): TransactionInstruction {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.blob(8, 'amount')])
const keys = addSigners(
[
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
],
owner,
multiSigners
)
const data = Buffer.alloc(dataLayout.span)
dataLayout.encode(
{
instruction: TokenInstruction.Transfer,
amount: new TokenAmount(amount).toBuffer(),
},
data
)
return new TransactionInstruction({ keys, programId, data })
}
export function addSigners(keys: AccountMeta[], ownerOrAuthority: PublicKey, multiSigners: Signer[]): AccountMeta[] {
if (multiSigners.length) {
keys.push({ pubkey: ownerOrAuthority, isSigner: false, isWritable: false })
for (const signer of multiSigners) {
keys.push({ pubkey: signer.publicKey, isSigner: true, isWritable: false })
}
} else {
keys.push({ pubkey: ownerOrAuthority, isSigner: true, isWritable: false })
}
return keys
}
class TokenAmount extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): Buffer {
const a = super.toArray().reverse()
const b = Buffer.from(a)
if (b.length === 8) {
return b
}
if (b.length >= 8) {
throw new Error('TokenAmount too large')
}
const zeroPad = Buffer.alloc(8)
b.copy(zeroPad)
return zeroPad
}
/**
* Construct a TokenAmount from Buffer representation
*/
static fromBuffer(buffer: Buffer): TokenAmount {
if (buffer.length !== 8) {
throw new Error(`Invalid buffer length: ${buffer.length}`)
}
return new BN(
[...buffer]
.reverse()
.map((i) => `00${i.toString(16)}`.slice(-2))
.join(''),
16
)
}
}
// getAccountInfo.ts
import { TOKEN_PROGRAM_ID, AccountLayout } from '@solana/spl-token'
import { Connection, PublicKey, Commitment } from '@solana/web3.js'
export enum AccountState {
Uninitialized = 0,
Initialized = 1,
Frozen = 2,
}
export async function getAccountInfo(
connection: Connection,
address: PublicKey,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const info = await connection.getAccountInfo(address, commitment)
if (!info) throw new Error('TokenAccountNotFoundError')
if (!info.owner.equals(programId)) throw new Error('TokenInvalidAccountOwnerError')
if (info.data.length != AccountLayout.span) throw new Error('TokenInvalidAccountSizeError')
const rawAccount = AccountLayout.decode(Buffer.from(info.data))
return {
address,
mint: rawAccount.mint,
owner: rawAccount.owner,
amount: rawAccount.amount,
delegate: rawAccount.delegateOption ? rawAccount.delegate : null,
delegatedAmount: rawAccount.delegatedAmount,
isInitialized: rawAccount.state !== AccountState.Uninitialized,
isFrozen: rawAccount.state === AccountState.Frozen,
isNative: !!rawAccount.isNativeOption,
rentExemptReserve: rawAccount.isNativeOption ? rawAccount.isNative : null,
closeAuthority: rawAccount.closeAuthorityOption ? rawAccount.closeAuthority : null,
}
}
// getAssociatedTokerAddress.ts
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { PublicKey } from '@solana/web3.js'
export async function getAssociatedTokenAddress(
mint: PublicKey,
owner: PublicKey,
allowOwnerOffCurve = false,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
): Promise<PublicKey> {
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer())) throw new Error('TokenOwnerOffCurveError')
const [address] = await PublicKey.findProgramAddress(
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
associatedTokenProgramId
)
return address
}
希望这对其他人有帮助。如果哪位有意见指点欢迎评论。
这是您的代码的一个变体,它可以工作并且不需要创建我们自己的传输指令。只需要getOrCreateAssociatedTokenAccount、createAssociatedTokenAccountInstruction、getAccountInfo和getAssociatedTokenAddress(这些功能在另一个答案中指定)。
创建转账指令需要导入spl-token。
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
publicKey,
signTransaction
)
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
toPublicKey,
signTransaction
)
const transaction = new web3.Transaction().add(
Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
fromTokenAccount.address,
toTokenAccount.address,
publicKey,
[],
1
)
)
const signature = await sendTransaction(transaction, connection)
const response = await connection.confirmTransaction(signature, 'processed')
console.log('response', response)
下面是生成密钥、创建代币、为铸币商创建代币账户、将代币铸造到代币账户以及最后将代币转移到另一个代币账户的示例
const splToken = require("@solana/spl-token");
const web3 = require("@solana/web3.js");
const test = async () => {
console.log("solana test");
const cluster_url = "https://api.testnet.solana.com";
//create keys
const mint_authority = web3.Keypair.generate();
const freeze_authority = web3.Keypair.generate();
const connection = new web3.Connection(cluster_url, "confirmed");
//airdrop to mint authority
const airdropSignature = await connection.requestAirdrop(
mint_authority.publicKey,
web3.LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
console.log("mint authority pub key", mint_authority.publicKey.toString());
//check balance of mint authority
console.log(
"SOL balance of minter",
await connection.getBalance(mint_authority.publicKey)
);
//create (but not mint) a new token
const token = await splToken.Token.createMint(
connection,
mint_authority,
mint_authority.publicKey,
null,
9,
splToken.TOKEN_PROGRAM_ID
);
console.log(
"new SOL balance of minter after token creation activity",
await connection.getBalance(mint_authority.publicKey)
);
console.log("new spl token address is", token.publicKey.toString());
//lets mint some of the token INTO the mint authority wallet
const minter_token_account = await token.getOrCreateAssociatedAccountInfo(
mint_authority.publicKey
);
console.log(
"minter_token_account address",
minter_token_account.address.toString()
);
console.log("minting some supply into token account we made for the minter");
console.log(
"minter_token_account.mint (same as token address)",
minter_token_account.mint.toString()
);
console.log(
"minter_token_account.owner (same as pub key of token account)",
minter_token_account.owner.toString()
);
// Mint the tokens - how am I allowed to do this just my using mint_authority.publicKey as the authority, shouldnt I need
// a private key to do this or is it just because I have a 'powerful' reference to the token because of earlier createMint() ????
await token.mintTo(
minter_token_account.address,
mint_authority.publicKey,
[],
10000000
);
//get balance of sol for minter
console.log(
"new SOL balance of minter after token minting activity",
await connection.getBalance(mint_authority.publicKey)
);
//token accounts by owner
const tokenAccountsByOwner = await connection.getTokenAccountsByOwner(
mint_authority.publicKey,
{
mint: token.publicKey,
}
);
console.log(
"tokenAccountsByOwner - token account address",
tokenAccountsByOwner.value[0].pubkey.toString()
);
// console.log(
// "tokenAccountsByOwner - token account lamports",
// tokenAccountsByOwner.value[0].account.lamports
// );
//get token account balance
const tokenAccountBalance = await connection.getTokenAccountBalance(
minter_token_account.address
);
console.log(
"token account balance:",
tokenAccountBalance.value.uiAmountString
);
//now lets create a new solana address
const bob = web3.Keypair.generate();
console.log("bob public address:", bob.publicKey.toString());
console.log(
"SOL balance of minter just before creating account for bob",
await connection.getBalance(mint_authority.publicKey)
);
//create token account fot this address
const bob_token_account = await token.getOrCreateAssociatedAccountInfo(
bob.publicKey
);
console.log(
"SOL balance of minter just after creating account for bob",
await connection.getBalance(mint_authority.publicKey)
); //seems to cost 2044280 lamports .002 SOL
console.log(
"bob_token_account address",
bob_token_account.address.toString()
);
console.log(
"bob_token_account.mint (same as token address)",
bob_token_account.mint.toString()
);
console.log(
"bob_token_account.owner (same as pub key of token account)",
bob_token_account.owner.toString()
);
//transfer from minter wallet to bob
var transferTrx = new web3.Transaction().add(
splToken.Token.createTransferInstruction(
splToken.TOKEN_PROGRAM_ID,
minter_token_account.address,
bob_token_account.address,
mint_authority.publicKey,
[],
1
)
);
let bobTokenAccountBalance = await connection.getTokenAccountBalance(
bob_token_account.address
);
console.log(
"bob_token_account balance before transfer:",
bobTokenAccountBalance.value.uiAmountString
);
console.log(
"SOL balance of minter just before transfering to bob",
await connection.getBalance(mint_authority.publicKey)
);
var signature = await web3.sendAndConfirmTransaction(
connection,
transferTrx,
[mint_authority]
);
console.log(
"SOL balance of minter just AFTER transfering to bob",
await connection.getBalance(mint_authority.publicKey)
); // seems to cost 5000 lamports or .000005 SOL
bobTokenAccountBalance = await connection.getTokenAccountBalance(
bob_token_account.address
);
console.log(
"bob_token_account balance after transfer:",
bobTokenAccountBalance.value.uiAmountString
);
};
return test()
.then()
.catch((err) => {
if (err.logs) {
console.log(err.logs);
} else {
console.error(err.message);
}
});
也可以是:
1、改写:getOrCreateAssociatedTokenAccount函数
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
getAssociatedTokenAddress,
getAccount,
createAssociatedTokenAccountInstruction,
TokenAccountNotFoundError,
TokenInvalidAccountOwnerError,
TokenInvalidMintError,
TokenInvalidOwnerError,
} from '@solana/spl-token'
import { Transaction } from '@solana/web3.js'
/**
* (rewrite)Retrieve the associated token account, or create it if it doesn't exist
*
* @param connection Connection to use
* @param payer Payer of the transaction and initialization fees
* @param mint Mint associated with the account to set or verify
* @param owner Owner of the account to set or verify
* @param sendTransaction
* @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address)
* @param commitment Desired level of commitment for querying the state
* @param programId SPL Token program account
* @param associatedTokenProgramId SPL Associated Token program account
*
* @return Address of the new associated token account
*/
export async function getOrCreateAssociatedTokenAccount(
connection,
payer,
mint,
owner,
sendTransaction,
allowOwnerOffCurve = false,
commitment,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
) {
const associatedToken = await getAssociatedTokenAddress(
mint,
owner,
allowOwnerOffCurve,
programId,
associatedTokenProgramId,
)
// This is the optimal logic, considering TX fee, client-side computation, RPC roundtrips and guaranteed idempotent.
// Sadly we can't do this atomically.
let account
try {
account = await getAccount(connection, associatedToken, commitment, programId)
} catch (error) {
// TokenAccountNotFoundError can be possible if the associated address has already received some lamports,
// becoming a system account. Assuming program derived addressing is safe, this is the only case for the
// TokenInvalidAccountOwnerError in this code path.
if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) {
// As this isn't atomic, it's possible others can create associated accounts meanwhile.
try {
const transaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
payer,
associatedToken,
owner,
mint,
programId,
associatedTokenProgramId,
),
)
const signature = await sendTransaction(transaction, connection)
await connection.confirmTransaction(signature)
} catch (error) {
// Ignore all errors; for now there is no API-compatible way to selectively ignore the expected
// instruction error if the associated account exists already.
}
// Now this should always succeed
account = await getAccount(connection, associatedToken, commitment, programId)
} else {
throw error
}
}
if (!account.mint.equals(mint)) throw new TokenInvalidMintError()
if (!account.owner.equals(owner)) throw new TokenInvalidOwnerError()
return account
}
2、转移spl代币
import { Transaction, PublicKey } from '@solana/web3.js'
import { createTransferCheckedInstruction } from '@solana/spl-token'
import { useWallet, useConnection } from '@solana/wallet-adapter-react'
try {
const { sendTransaction, publicKey } = useWallet()
const { connection } = useConnection()
// step 1: create transaction
const toPublicKey = new PublicKey('')
const mint = new PublicKey('token address')
const transaction = new Transaction()
// up rewrite function
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
publicKey,
sendTransaction,
)
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
publicKey,
mint,
toPublicKey,
sendTransaction,
)
const instruction = createTransferCheckedInstruction(
fromTokenAccount.address,
mint,
toTokenAccount.address,
publicKey,
1,
0,
)
transaction.add(instruction)
// step 2: sign&send transaction
const result = await sendTransaction(transaction, connection)
} catch (err) {
// err handle
}