Transaction Ngoại tuyến
Ký Transaction
Để tạo một transaction ngoại tuyến, bạn phải ký lên transaction và sau đó bất kỳ ai đều có thể gửi transaction đó lên trên mạng lưới.
Press </> button to view full source
import {
clusterApiUrl,
Connection,
Keypair,
Transaction,
SystemProgram,
LAMPORTS_PER_SOL,
Message,
} from "@solana/web3.js";
import * as nacl from "tweetnacl";
import * as bs58 from "bs58";
// to complete a offline transaction, I will seperate them into four steps
// 1. Create Transaction
// 2. Sign Transaction
// 3. Recover Transaction
// 4. Send Transaction
(async () => {
// create connection
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// create a example tx, alice transfer to bob and feePayer is `feePayer`
// alice and feePayer are signer in this tx
const feePayer = Keypair.generate();
await connection.confirmTransaction(
await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL)
);
const alice = Keypair.generate();
await connection.confirmTransaction(
await connection.requestAirdrop(alice.publicKey, LAMPORTS_PER_SOL)
);
const bob = Keypair.generate();
// 1. Create Transaction
let tx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: alice.publicKey,
toPubkey: bob.publicKey,
lamports: 0.1 * LAMPORTS_PER_SOL,
})
);
tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
tx.feePayer = feePayer.publicKey;
let realDataNeedToSign = tx.serializeMessage(); // the real data singer need to sign.
// 2. Sign Transaction
// use any lib you like, the main idea is to use ed25519 to sign it.
// the return signature should be 64 bytes.
let feePayerSignature = nacl.sign.detached(
realDataNeedToSign,
feePayer.secretKey
);
let aliceSignature = nacl.sign.detached(realDataNeedToSign, alice.secretKey);
// 3. Recover Tranasction
// you can verify signatures before you recovering the transaction
let verifyFeePayerSignatureResult = nacl.sign.detached.verify(
realDataNeedToSign,
feePayerSignature,
feePayer.publicKey.toBytes() // you should use the raw pubkey (32 bytes) to verify
);
console.log(`verify feePayer signature: ${verifyFeePayerSignatureResult}`);
let verifyAliceSignatureResult = nacl.sign.detached.verify(
realDataNeedToSign,
aliceSignature,
alice.publicKey.toBytes()
);
console.log(`verify alice signature: ${verifyAliceSignatureResult}`);
// there are two ways you can recover the tx
// 3.a Recover Tranasction (use populate then addSignauture)
{
let recoverTx = Transaction.populate(Message.from(realDataNeedToSign));
recoverTx.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
recoverTx.addSignature(alice.publicKey, Buffer.from(aliceSignature));
// 4. Send transaction
console.log(
`txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
);
}
// or
// 3.b. Recover Tranasction (use populate with signature)
{
let recoverTx = Transaction.populate(Message.from(realDataNeedToSign), [
bs58.encode(feePayerSignature),
bs58.encode(aliceSignature),
]);
// 4. Send transaction
console.log(
`txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
);
}
// if this process takes too long, your recent blockhash will expire (after 150 blocks).
// you can use `durable nonce` to get rid of it.
})();
// there are two ways you can recover the tx
// 3.a Recover Tranasction (use populate then addSignauture)
{
let recoverTx = Transaction.populate(Message.from(realDataNeedToSign));
recoverTx.addSignature(feePayer.publicKey, Buffer.from(feePayerSignature));
recoverTx.addSignature(alice.publicKey, Buffer.from(aliceSignature));
// 4. Send transaction
console.log(
`txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
);
}
// or
// 3.b. Recover Tranasction (use populate with signature)
{
let recoverTx = Transaction.populate(Message.from(realDataNeedToSign), [
bs58.encode(feePayerSignature),
bs58.encode(aliceSignature),
]);
// 4. Send transaction
console.log(
`txhash: ${await connection.sendRawTransaction(recoverTx.serialize())}`
);
}
Ký một phần Transaction
Khi một transaction yêu cầu nhiều chữ ký, bạn có thể ký một phần của nó. Những chữ ký khác sẽ được ký sau đó và sẽ được gửi đi nếu đủ chữ ký cần thiết.
Một vài ví dụ khi nào bạn nên dùng:
- Gửi một SPL token để thanh toán
- Ký một transaction để bạn có thể kiểm tra tính đúng đắn của nó sau này
- Gọi một program tuỳ chỉnh bằng một transaction yêu cầu chữ ký của bạn
Trong ví dụ này Bob sẽ gửi có Alice một SPL token thể thanh toán cho cô ấy:
Press </> button to view full source
import {
createTransferCheckedInstruction,
getAssociatedTokenAddress,
getMint,
getOrCreateAssociatedTokenAccount,
} from "@solana/spl-token";
import {
clusterApiUrl,
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import base58 from "bs58";
/* The transaction:
* - sends 0.01 SOL from Alice to Bob
* - sends 1 token from Bob to Alice
* - is partially signed by Bob, so Alice can approve + send it
*/
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const alicePublicKey = new PublicKey(
"5YNmS1R9nNSCDzb5a7mMJ1dwK9uHeAAF4CmPEwKgVWr8"
);
const bobKeypair = Keypair.fromSecretKey(
base58.decode(
"4NMwxzmYj2uvHuq8xoqhY8RXg63KSVJM1DXkpbmkUY7YQWuoyQgFnnzn6yo3CMnqZasnNPNuAT2TLwQsCaKkUddp"
)
);
const tokenAddress = new PublicKey(
"Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"
);
const bobTokenAddress = await getAssociatedTokenAddress(
tokenAddress,
bobKeypair.publicKey
);
// Alice may not have a token account, so Bob creates one if not
const aliceTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
bobKeypair, // Bob pays the fee to create it
tokenAddress, // which token the account is for
alicePublicKey // who the token account is for
);
// Get the details about the token mint
const tokenMint = await getMint(connection, tokenAddress);
// Get a recent blockhash to include in the transaction
const { blockhash } = await connection.getLatestBlockhash("finalized");
const transaction = new Transaction({
recentBlockhash: blockhash,
// Alice pays the transaction fee
feePayer: alicePublicKey,
});
// Transfer 0.01 SOL from Alice -> Bob
transaction.add(
SystemProgram.transfer({
fromPubkey: alicePublicKey,
toPubkey: bobKeypair.publicKey,
lamports: 0.01 * LAMPORTS_PER_SOL,
})
);
// Transfer 1 token from Bob -> Alice
transaction.add(
createTransferCheckedInstruction(
bobTokenAddress, // source
tokenAddress, // mint
aliceTokenAccount.address, // destination
bobKeypair.publicKey, // owner of source account
1 * 10 ** tokenMint.decimals, // amount to transfer
tokenMint.decimals // decimals of token
)
);
// Partial sign as Bob
transaction.partialSign(bobKeypair);
// Serialize the transaction and convert to base64 to return it
const serializedTransaction = transaction.serialize({
// We will need Alice to deserialize and sign the transaction
requireAllSignatures: false,
});
const transactionBase64 = serializedTransaction.toString("base64");
return transactionBase64;
// The caller of this can convert it back to a transaction object:
const recoveredTransaction = Transaction.from(
Buffer.from(transactionBase64, "base64")
);
})();
// 1. Add an instruction to send the token from Bob to Alice
transaction.add(
createTransferCheckedInstruction(
bobTokenAddress, // source
tokenAddress, // mint
aliceTokenAccount.address, // destination
bobKeypair.publicKey, // owner of source account
1 * 10 ** tokenMint.decimals, // amount to transfer
tokenMint.decimals // decimals of token
)
);
// 2. Bob partially signs the transaction
transaction.partialSign(bobKeypair);
// 3. Serialize the transaction without requiring all signatures
const serializedTransaction = transaction.serialize({
requireAllSignatures: false,
});
// 4. Alice can deserialize the transaction
const recoveredTransaction = Transaction.from(
Buffer.from(transactionBase64, "base64")
);
Nonce vĩnh viễn
RecentBlockhash
là một giá trị quan trọng cho một transaction. Transaction của bạn sẽ bị từ chối nếu bạn sử dụng một blockhash đã quá hạn (sau 150 blocks). Bạ có thể sử dụng durable nonce
để có được một blockhash không bao giờ hết hạn. Để kích hoạt cơ chế, transaction của bạn phải
- Sử dụng một
nonce
lưu trongnonce account
như là blockhash hiện tại - Đặt cơ chế
nonce advance
trong chỉ thị đầu tiên
Tạo Nonce Account
Press </> button to view full source
import {
clusterApiUrl,
Connection,
Keypair,
Transaction,
NONCE_ACCOUNT_LENGTH,
SystemProgram,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
(async () => {
// Setup our connection and wallet
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const feePayer = Keypair.generate();
// Fund our wallet with 1 SOL
const airdropSignature = await connection.requestAirdrop(
feePayer.publicKey,
LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
const nonceAccountAuth = Keypair.generate();
let nonceAccount = Keypair.generate();
console.log(`nonce account: ${nonceAccount.publicKey.toBase58()}`);
let tx = new Transaction().add(
// create nonce account
SystemProgram.createAccount({
fromPubkey: feePayer.publicKey,
newAccountPubkey: nonceAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
),
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
}),
// init nonce account
SystemProgram.nonceInitialize({
noncePubkey: nonceAccount.publicKey, // nonce account pubkey
authorizedPubkey: nonceAccountAuth.publicKey, // nonce account authority (for advance and close)
})
);
console.log(
`txhash: ${await connection.sendTransaction(tx, [feePayer, nonceAccount])}`
);
})();
let tx = new Transaction().add(
// create nonce account
SystemProgram.createAccount({
fromPubkey: feePayer.publicKey,
newAccountPubkey: nonceAccount.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
NONCE_ACCOUNT_LENGTH
),
space: NONCE_ACCOUNT_LENGTH,
programId: SystemProgram.programId,
}),
// init nonce account
SystemProgram.nonceInitialize({
noncePubkey: nonceAccount.publicKey, // nonce account pubkey
authorizedPubkey: nonceAccountAuth.publicKey, // nonce account authority (for advance and close)
})
);
console.log(
`txhash: ${await connection.sendTransaction(tx, [feePayer, nonceAccount])}`
);
Truy vấn Nonce Account
Press </> button to view full source
import {
clusterApiUrl,
Connection,
PublicKey,
Keypair,
NonceAccount,
} from "@solana/web3.js";
(async () => {
// connection
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const nonceAccountPubkey = new PublicKey(
"7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4"
);
let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);
let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
console.log(`nonce: ${nonceAccount.nonce}`);
console.log(`authority: ${nonceAccount.authorizedPubkey.toBase58()}`);
console.log(`fee calculator: ${JSON.stringify(nonceAccount.feeCalculator)}`);
})();
let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);
let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
Sử dụng Nonce Account
Press </> button to view full source
import {
clusterApiUrl,
Connection,
PublicKey,
Keypair,
Transaction,
SystemProgram,
NonceAccount,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import * as bs58 from "bs58";
(async () => {
// Setup our connection and wallet
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const feePayer = Keypair.generate();
// Fund our wallet with 1 SOL
const airdropSignature = await connection.requestAirdrop(
feePayer.publicKey,
LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
// G2FAbFQPFa5qKXCetoFZQEvF9BVvCKbvUZvodpVidnoY
const nonceAccountAuth = Keypair.fromSecretKey(
bs58.decode(
"4NMwxzmYj2uvHuq8xoqhY8RXg63KSVJM1DXkpbmkUY7YQWuoyQgFnnzn6yo3CMnqZasnNPNuAT2TLwQsCaKkUddp"
)
);
const nonceAccountPubkey = new PublicKey(
"7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4"
);
let nonceAccountInfo = await connection.getAccountInfo(nonceAccountPubkey);
let nonceAccount = NonceAccount.fromAccountData(nonceAccountInfo.data);
let tx = new Transaction().add(
// nonce advance must be the first insturction
SystemProgram.nonceAdvance({
noncePubkey: nonceAccountPubkey,
authorizedPubkey: nonceAccountAuth.publicKey,
}),
// after that, you do what you really want to do, here we append a transfer instruction as an example.
SystemProgram.transfer({
fromPubkey: feePayer.publicKey,
toPubkey: nonceAccountAuth.publicKey,
lamports: 1,
})
);
// assign `nonce` as recentBlockhash
tx.recentBlockhash = nonceAccount.nonce;
tx.feePayer = feePayer.publicKey;
tx.sign(
feePayer,
nonceAccountAuth
); /* fee payer + nonce account authority + ... */
console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);
})();
let tx = new Transaction().add(
// nonce advance must be the first insturction
SystemProgram.nonceAdvance({
noncePubkey: nonceAccountPubkey,
authorizedPubkey: nonceAccountAuth.publicKey,
}),
// after that, you do what you really want to do, here we append a transfer instruction as an example.
SystemProgram.transfer({
fromPubkey: feePayer.publicKey,
toPubkey: nonceAccountAuth.publicKey,
lamports: 1,
})
);
// assign `nonce` as recentBlockhash
tx.recentBlockhash = nonceAccount.nonce;
tx.feePayer = feePayer.publicKey;
tx.sign(
feePayer,
nonceAccountAuth
); /* fee payer + nonce account authority + ... */
console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);