小白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,
});
};
};
代币转账
这个稍微比较复杂,上面只需要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;
};
其他
上面代码走一遍后,关于授权、销毁等操作其实也明白了,就不增加篇幅了。
--------完--------