如何使用'@solana/web3.js'和'@solana/sol-wallet-adapter'转移自定义SPL代币

20

我正在尝试使用solana-wallet适配器转移自定义的SPL代币。然而,我无法获取钱包的密钥并签署交易。

我查看了以下答案以编写转移代码,但我需要获取签名者,而我在solana-wallet适配器中遇到了困难:

如何使用web3.js sdk为Solana转移SOL?

如何使用'@solana/web3.js'转移自定义代币

这些示例硬编码了秘密密钥,但由于我正在使用钱包扩展,这是不可能的。

根据webadapter存储库上的此问题https://github.com/solana-labs/wallet-adapter/issues/120,您需要:

  1. 创建@solana/web3.js事务对象并向其中添加指令
  2. 使用钱包签署交易
  3. 通过连接发送交易

但我在找到有关如何执行步骤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)}</>
}
6个回答

11

我找到了一种方法来实现这个,需要进行一些清理和错误处理,但可以通过@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
}

希望这能帮助其他人。如果有任何评论或建议,请留言。

请查看Prateekeshwar Singh的答案,以获取最新和更正确的解决方案。 - Martin

4
这是生成密钥、创建令牌、为铸造者创建令牌账户、将令牌铸造到令牌账户中,最后将令牌转移到另一个令牌账户的示例。
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);
    }
  });


3

以上回答存在两个问题:

  1. 首先,当我们可以使用'@solana/wallet-adapter'、'@solana/spl-token'和'@solana/web3.js'的内置函数时,我们需要编写大量自定义代码。
  2. 其次,如果接收方账户不存在,用户将被任何连接的钱包收取两次费用。第一次会收取创建帐户的费用,第二次会收取转移spl代币的费用。此外,它还会打开钱包批准模态框两次,这对用户来说也是一个坏事。

下面的代码将转移1个USDC(USDC是Solana区块链上的一种spl代币)。

import React from 'react';
import {
  WalletNotConnectedError,
  SignerWalletAdapterProps
} from '@solana/wallet-adapter-base';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import {
  createTransferInstruction,
  createAssociatedTokenAccountInstruction,
  getAssociatedTokenAddress,
  getAccount
} from '@solana/spl-token';
import {
  PublicKey,
  Transaction,
  Connection,
  TransactionInstruction
} from '@solana/web3.js';

export const configureAndSendCurrentTransaction = async (
  transaction: Transaction,
  connection: Connection,
  feePayer: PublicKey,
  signTransaction: SignerWalletAdapterProps['signTransaction']
) => {
  const blockHash = await connection.getLatestBlockhash();
  transaction.feePayer = feePayer;
  transaction.recentBlockhash = blockHash.blockhash;
  const signed = await signTransaction(transaction);
  const signature = await connection.sendRawTransaction(signed.serialize());
  await connection.confirmTransaction({
    blockhash: blockHash.blockhash,
    lastValidBlockHeight: blockHash.lastValidBlockHeight,
    signature
  });
  return signature;
};

const SendSolanaSplTokens: React.FC = () => {
  const { connection } = useConnection();
  const { publicKey, signTransaction } = useWallet();

  const handlePayment = async () => {
    try {
      if (!publicKey || !signTransaction) {
        throw new WalletNotConnectedError();
      }
      const mintToken = new PublicKey(
        '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'
      ); // 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU is USDC token address on solana devnet
      const recipientAddress = new PublicKey(
        'token receiver solana account address'
      );

      const transactionInstructions: TransactionInstruction[] = [];
      const associatedTokenFrom = await getAssociatedTokenAddress(
        mintToken,
        publicKey
      );
      const fromAccount = await getAccount(connection, associatedTokenFrom);
      const associatedTokenTo = await getAssociatedTokenAddress(
        mintToken,
        recipientAddress
      );
      if (!(await connection.getAccountInfo(associatedTokenTo))) {
        transactionInstructions.push(
          createAssociatedTokenAccountInstruction(
            publicKey,
            associatedTokenTo,
            recipientAddress,
            mintToken
          )
        );
      }
      transactionInstructions.push(
        createTransferInstruction(
          fromAccount.address, // source
          associatedTokenTo, // dest
          publicKey,
          1000000 // transfer 1 USDC, USDC on solana devnet has 6 decimal
        )
      );
      const transaction = new Transaction().add(...transactionInstructions);
      const signature = await configureAndSendCurrentTransaction(
        transaction,
        connection,
        publicKey,
        signTransaction
      );
      // signature is transaction address, you can confirm your transaction on 'https://explorer.solana.com/?cluster=devnet'
    } catch (error) {}
  };

  return <button onClick={handlePayment}>Transfer spl token</button>;
};

export default SendSolanaSplTokens;

没错,写这篇文章时,软件包并没有真正支持我需要做的事情,这个问题正在重写中得到解决,所以我不得不从可工作的PR中提取一些代码。现在已经合并了,做起来容易多了。 - Rick Lancee
"getAccount(connection, associatedTokenFrom)" 会导致一个 TokenAccountNotFoundError 错误。请帮我解决。 - hungtran273

2

这是您的代码变体,它可以工作而且不需要创建我们自己的转账指令。它只需要使用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)

0

也可以是:

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
}

0

将OP在问题中的解决方案迁移至回答中:

我在sol-wallet-adapter的GitHub存储库上发布了这个问题,并得到了以下回复: https://github.com/solana-labs/wallet-adapter/issues/189 嗯,这不是wallet-adapter的问题。这是Token类的一个缺点--它无法使用你没有Keypair / Signer的钱包。
有一个TypeScript版本的@ spl-token,使一些东西变得更容易,但仍在审核中,尚未在NPM上发布: solana-labs/solana-program-library#2539 我建议检查相关代币账户,并在与钱包签名的交易指令中自行创建它。该开放PR中的这些链接可能有助于理解如何执行此操作: https://github.com/solana-labs/solana-program-library/blob/jsx/token-ts/token/ts/src/actions/getOrCreateAssociatedTokenAccount.ts https://github.com/solana-labs/solana-program-library/blob/jsx/token-ts/token/ts/src/state/mint.ts#L114-L129 https://github.com/solana-labs/solana-program-library/blob/jsx/token-ts/token/ts/src/instructions/transfer.ts 由于这超出了该库的范围,所以我将其关闭,但希望这可以帮助你。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接