Solana -- 转账

5,851 阅读6分钟

小白Solana转账入门

我们分两个部分开讲,一个是原生币,一个是代币。

下面代码经过测试,放心食用~~

SOL转账

import { WalletContextState } from '@solana/wallet-adapter-react';
import {
  clusterApiUrl,
  Connection,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
} from '@solana/web3.js';

const transferNativeSol = async ({
  toPubkey,
  amount,
  payerPublicKey,
  connection,
  wallet,
}: {
  toPubkey: PublicKey;
  amount: number;
  payerPublicKey?: PublicKey;
  connection: Connection;
  wallet: WalletContextState;
}) => {
  if (!payerPublicKey) {
    return console.error('error not connect wallet');
  }

  const instructions: TransactionInstruction[] = [];
  instructions.push(
    SystemProgram.transfer({
      fromPubkey: payerPublicKey,
      lamports: amount,
      toPubkey,
    }),
  );
  const transaction = new Transaction();

  instructions.forEach(instruction => {
    transaction.add(instruction);
  });
  transaction.recentBlockhash = (
    await connection.getRecentBlockhash('max')
  ).blockhash;
  transaction.feePayer = payerPublicKey;
  const signedTransaction = await wallet.signTransaction(transaction);
  const tx = await connection.sendRawTransaction(
    signedTransaction.serialize(),
  );
  return tx
};

测试运行例子(钱包连接需要自己连接哦)

const a = () => {
  const solana = (window as any).solana;
  const connection = new Connection(clusterApiUrl('devnet'), 'recent');

  const toPubkey = new PublicKey(
    'xxxxxxxxxxxxx',
  );
  const amount = 2;

  (window as any).test = async () => {
    transferNativeSol({
      payerPublicKey: solana.publicKey,
      toPubkey,
      amount: amount * 1e9,
      connection,
      wallet: solana as any,
    });
  };
};

代币转账

官方Token源码

官方教程

这个稍微比较复杂,上面只需要transfer,这里需要判断对方是否有对应代币的地址,如果有选取金额最多的地址,如果没有给他创建对应帐户,完成后给他转账。

为了测试,我们需要先有一些代币,所以还得mint对应得币,并把权限给自己,后给自己转点钱。

转账之前,我们需要先完成代币创建和代币的mint to功能。

创建属于自己的代币

在创建之前,先在创建函数签名设置decimals,建议为9.

const decimals = 9;
const createToken = async () => {
    if (!address) {
      return console.error('not wallet');
    }
    const payerPublicKey = address;
    const mintRent = await connection.getMinimumBalanceForRentExemption(
      MintLayout.span,
    );
    const instructions: TransactionInstruction[] = [];
    const signers: Keypair[] = [];
    const mintKey = createMint(
      instructions,
      new PublicKey(address),
      mintRent,
      decimals,
      toPublicKey(payerPublicKey),
      toPublicKey(payerPublicKey),
      signers,
    ).toBase58();
    console.log('mintKey---------->', mintKey);

    sendTransactionWithRetry(
      connection,
      {
        publicKey: toPublicKey(address),
        signTransaction: wallet.signTransaction,
        signAllTransactions: wallet.signAllTransactions,
      },
      instructions,
      signers,
    );
  };

好了代码写完了,上面代码很容易阅读,然后上面涉及到了两个自定义函数,我们实现下。

export function createUninitializedMint(
  instructions: TransactionInstruction[],
  payer: PublicKey,
  amount: number,
  signers: Keypair[],
) {
  const account = Keypair.generate();
  instructions.push(
    SystemProgram.createAccount({
      fromPubkey: payer,
      newAccountPubkey: account.publicKey,
      lamports: amount,
      space: MintLayout.span,
      programId: TOKEN_PROGRAM_ID,
    }),
  );

  signers.push(account);

  return account.publicKey;
}

export function createMint(
  instructions: TransactionInstruction[],
  payer: PublicKey,
  mintRentExempt: number,
  decimals: number,
  owner: PublicKey,
  freezeAuthority: PublicKey,
  signers: Keypair[],
) {
  const account = createUninitializedMint(
    instructions,
    payer,
    mintRentExempt,
    signers,
  );

  instructions.push(
    Token.createInitMintInstruction(
      TOKEN_PROGRAM_ID,
      account,
      decimals,
      owner,
      freezeAuthority,
    ),
  );

  return account;
}


export const sendTransactionWithRetry = async (
  connection: Connection,
  wallet: WalletSigner,
  instructions: TransactionInstruction[],
  signers: Keypair[],
  commitment: Commitment = 'singleGossip',
  includesFeePayer: boolean = false,
  block?: BlockhashAndFeeCalculator,
  beforeSend?: () => void,
) => {
  if (!wallet.publicKey) throw new WalletNotConnectedError();

  let transaction = new Transaction();
  instructions.forEach(instruction => transaction.add(instruction));
  transaction.recentBlockhash = (
    block || (await connection.getRecentBlockhash(commitment))
  ).blockhash;

  if (includesFeePayer) {
    transaction.setSigners(...signers.map(s => s.publicKey));
  } else {
    transaction.setSigners(
      // fee payed by the wallet owner
      wallet.publicKey,
      ...signers.map(s => s.publicKey),
    );
  }

  if (signers.length > 0) {
    transaction.partialSign(...signers);
  }
  if (!includesFeePayer) {
    transaction = await wallet.signTransaction(transaction);
  }

  if (beforeSend) {
    beforeSend();
  }

  const { txid, slot } = await sendSignedTransaction({
    connection,
    signedTransaction: transaction,
  });

  return { txid, slot };
};

上面sendSignedTransaction实现用的是metaplex上面的,但是具体不贴上来,具体代码有点复杂,简单的话,直接用wallet.signTransaction实现即可。关于signTransaction实际是从钱包插件暴露的函数。如果不清楚,可以看钱包插件的文档。

好了,代币我们创建完成,但是我们还不拥有它(有权限,但是是空架子),需要用mintTo来“铸币”,不过“铸币”之前,我们需要先开一个关于这个代币的账户,也就是说,代币必须的接受者只能是相关的account,而不能是钱包地址本身。

那么,我们可以给自己创建相关账户,如果接受者本身没开户怎么办?

没关系,任何人都可以给他“开户”,别担心,虽然任何人都可以给别人开户,但是开户后,权限还是属于被开户者。 且一个钱包他的代币相关账户可以有很多个,类似于,我们可以开很多人银行的账户一样。

创建代币相关账户

const onCreateAccount = () => {
    if (!address) {
      return console.error('not wallet');
    }
    createAccount(toPublicKey(address));
};

const createAccount = async (owner: PublicKey) => {
    if (!address) {
      return console.error('not wallet');
    }
    const associatedAddress = await Token.getAssociatedTokenAddress(
      SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
      TOKEN_PROGRAM_ID,
      mint,
      owner,
    );
    console.log('associatedAddress---------->', associatedAddress.toBase58());
    const payer = toPublicKey(address);

    await sendRawTransaction(
      [
        Token.createAssociatedTokenAccountInstruction(
          SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
          TOKEN_PROGRAM_ID,
          mint,
          associatedAddress,
          owner,
          payer,
        ),
      ],
      address,
    );
    return associatedAddress;
  };  

没错,就这么简单。

上面sendRawTransaction函数你是不是担心从哪里来, 这个是对签名函数的一个封装而已,因为等下我们使用它的频率有点高。

const sendRawTransaction = async (
    instructions: TransactionInstruction[],
    feePayer: string,
  ) => {
    const transaction = new Transaction();
    instructions.forEach(instruction => {
      transaction.add(instruction);
    });
    transaction.recentBlockhash = (
      await connection.getRecentBlockhash('max')
    ).blockhash;
    transaction.feePayer = new PublicKey(feePayer);
    const signedTransaction = await wallet.signTransaction(transaction);
    const tx = await connection.sendRawTransaction(
      signedTransaction.serialize(),
    );
    console.log('tx----------->', tx);
  };

好了,我们已经可以开始铸币了。

铸币(Mint to)

const TokenMintTo = async () => {
    if (!address) {
      return console.error('not wallet');
    }

    const receiverWallet = address;
    const destAccount: StringPublicKey = (
      await findProgramAddress(
        [
          toPublicKey(receiverWallet).toBuffer(),
          TOKEN_PROGRAM_ID.toBuffer(),
          mint.toBuffer(),
        ],
        SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
      )
    )[0];

    console.log('destAccount-------->', destAccount);

    const amount = parseInt(new BigNumber(10000).pow(decimals).toFixed(6));
    const signers: Keypair[] = [];

    sendRawTransaction(
      [
        Token.createMintToInstruction(
          TOKEN_PROGRAM_ID,
          mint,
          new PublicKey(destAccount),
          new PublicKey(address),
          signers,
          amount,
        ),
      ],
      address,
    );
  };

没错,这样就完成了。

不过,我们现在可能不清楚上面的mint是从哪里来的,这个就是刚刚拿到的代币地址哦。

如下定义它

const mint = new PublicKey('BkfWNsq3papRx1JXTxBVtCHmr1UhZGNrXEvDM4UmmGEs');

另外,上面的amount的值是使用Bignumber来创建的,如果你愿意,直接数字乘以decimals也可以,不过,数字可能比较大的时候会出问题,所以我们使用工具来。

另外,上面我没特意说的函数,都是web3插件里面的。

现在,我们已经完成了代币的初始工作,它已经可以流入市场了。

我们可以对它进行转账

ERC20的转账

代币是ERC20标准,也可以叫SPL-TOKEN。

本身只是智能合约的一个实现而已,所有的操作其实是对合约的操作,而上面有些方法虽然没有直接进行写指令,那是这个合约比较特殊,所以为了便利于是有了@solana/spl-token这个库。

好,我们开始转账

  const TokenTransfer = async () => {
    const toWalletAddress: StringPublicKey = 'xxx';
    const amount = parseFloat(new BigNumber(10).pow(decimals).toFixed(6));
    if (!address) {
      return console.error('not wallet');
    }
    const myAssociatedAddress = await Token.getAssociatedTokenAddress(
      SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
      TOKEN_PROGRAM_ID,
      mint,
      toPublicKey(address),
    );

    let toAssociatedAddress = await Token.getAssociatedTokenAddress(
      SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
      TOKEN_PROGRAM_ID,
      mint,
      toPublicKey(toWalletAddress),
    );

    const toAssociatedAccount = await connection.getTokenAccountsByOwner(
      myAssociatedAddress,
      {
        mint: mint,
      },
    );
    if (!toAssociatedAccount) {
      return console.error('not toAssociatedAccount');
    }

    console.log('toAssociatedAccount------------->', toAssociatedAccount);
    if (toAssociatedAccount.value.length === 0) {
      const _toAssociatedAddress = await createAccount(
        toPublicKey(toWalletAddress),
      );
      if (!_toAssociatedAddress) {
        return console.error('error _toAssociatedAddress');
      }
      toAssociatedAddress = _toAssociatedAddress;
    }

    if (!toAssociatedAddress) {
      return console.error('error toAssociatedAddress');
    }
    sendRawTransaction(
      [
        Token.createTransferInstruction(
          TOKEN_PROGRAM_ID,
          myAssociatedAddress,
          toAssociatedAddress,
          toPublicKey(address),
          [],
          amount,
        ),
      ],
      address,
    );
  };

OK, 收工回家。上面变量就两个,一个是对方的钱包地址,一个是转账金额。

对于收款账户,我们做了一个判断,先根据钱包地址去计算收款第一个账户,然后再去链上查询对应数量,假设为0,那么手动为他创建账户,如果不为空,取第一个账户。

获取某地址Token的余额

import { PublicKey } from "@solana/web3.js";
import { Connection } from '@solana/web3.js';
import BN from "bn.js";
import { TokenAccountParser, toPublicKey } from "npms/oystoer";

export const getTokenBalanceByOwnerLargest = async ({
  walletAddress,
  mintKey,
  connection,
}: {
  walletAddress: PublicKey;
  mintKey: string;
  connection: Connection;
}) => {
  try {
    const mintOwnerAccounts = await connection.getTokenAccountsByOwner(
      walletAddress,
      {
        mint: toPublicKey(mintKey),
      },
    );
    const ownerList = mintOwnerAccounts.value
      .map(account => {
        return TokenAccountParser(account.pubkey.toString(), account.account);
      })
      .find(e => e?.info.amount.gt(new BN(0)));
    if(!ownerList){
      return console.log('not token')
    }
    const balanceData = await connection.getTokenAccountBalance(toPublicKey(ownerList.pubkey))
    if(balanceData){
      return {
        account: ownerList.pubkey,
        balance: balanceData.value.uiAmount || 0,
      }
    }
  } catch (error) {
    console.log('error--->', error)
  }
}

这样即可,解析部分代码

export const TokenAccountParser = (
  pubKey: string,
  info: AccountInfo<Buffer>,
) => {
  // Sometimes a wrapped sol account gets closed, goes to 0 length,
  // triggers an update over wss which triggers this guy to get called
  // since your UI already logged that pubkey as a token account. Check for length.
  if (info.data.length > 0) {
    const buffer = Buffer.from(info.data);
    const data = deserializeAccount(buffer);

    const details = {
      pubkey: pubKey,
      account: {
        ...info,
      },
      info: data,
    } as TokenAccount;

    return details;
  }
};

export const deserializeAccount = (data: Buffer) => {
  const accountInfo = AccountLayout.decode(data);
  accountInfo.mint = new PublicKey(accountInfo.mint);
  accountInfo.owner = new PublicKey(accountInfo.owner);
  accountInfo.amount = u64.fromBuffer(accountInfo.amount);

  if (accountInfo.delegateOption === 0) {
    accountInfo.delegate = null;
    accountInfo.delegatedAmount = new u64(0);
  } else {
    accountInfo.delegate = new PublicKey(accountInfo.delegate);
    accountInfo.delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount);
  }

  accountInfo.isInitialized = accountInfo.state !== 0;
  accountInfo.isFrozen = accountInfo.state === 2;

  if (accountInfo.isNativeOption === 1) {
    accountInfo.rentExemptReserve = u64.fromBuffer(accountInfo.isNative);
    accountInfo.isNative = true;
  } else {
    accountInfo.rentExemptReserve = null;
    accountInfo.isNative = false;
  }

  if (accountInfo.closeAuthorityOption === 0) {
    accountInfo.closeAuthority = null;
  } else {
    accountInfo.closeAuthority = new PublicKey(accountInfo.closeAuthority);
  }

  return accountInfo;
};

其他

上面代码走一遍后,关于授权、销毁等操作其实也明白了,就不增加篇幅了。

--------完--------