feat: thread new blockheight expiry strategy through `sendAndConfirmTransaction` (#25227)

* chore: extract expirable blockhash record into its own type

* fix: the local latest blockhash cache now fetches and stores lastValidBlockHeight

* fix: allow people to supply a confirmation strategy to sendAndConfirmRawTransaction

* test: upgrade RPC helpers to use blockheight confirmation method

* test: patch up tests to use blockheight based confirmation strategy

* fix: eliminate deprecated construction of Transaction inside simulateTransaction

* test: eliminate deprecated constructions of Transaction in tests
This commit is contained in:
Steven Luscher 2022-05-14 21:54:12 -07:00 committed by GitHub
parent abd4ef889e
commit 4ea39c1cd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 72 deletions

View File

@ -284,15 +284,18 @@ export type RpcResponseAndContext<T> = {
value: T; value: T;
}; };
export type BlockhashWithExpiryBlockHeight = Readonly<{
blockhash: Blockhash;
lastValidBlockHeight: number;
}>;
/** /**
* A strategy for confirming transactions that uses the last valid * A strategy for confirming transactions that uses the last valid
* block height for a given blockhash to check for transaction expiration. * block height for a given blockhash to check for transaction expiration.
*/ */
export type BlockheightBasedTransactionConfimationStrategy = { export type BlockheightBasedTransactionConfimationStrategy = {
signature: TransactionSignature; signature: TransactionSignature;
blockhash: Blockhash; } & BlockhashWithExpiryBlockHeight;
lastValidBlockHeight: number;
};
/** /**
* @internal * @internal
@ -2218,12 +2221,12 @@ export class Connection {
/** @internal */ _disableBlockhashCaching: boolean = false; /** @internal */ _disableBlockhashCaching: boolean = false;
/** @internal */ _pollingBlockhash: boolean = false; /** @internal */ _pollingBlockhash: boolean = false;
/** @internal */ _blockhashInfo: { /** @internal */ _blockhashInfo: {
recentBlockhash: Blockhash | null; latestBlockhash: BlockhashWithExpiryBlockHeight | null;
lastFetch: number; lastFetch: number;
simulatedSignatures: Array<string>; simulatedSignatures: Array<string>;
transactionSignatures: Array<string>; transactionSignatures: Array<string>;
} = { } = {
recentBlockhash: null, latestBlockhash: null,
lastFetch: 0, lastFetch: 0,
transactionSignatures: [], transactionSignatures: [],
simulatedSignatures: [], simulatedSignatures: [],
@ -3322,11 +3325,11 @@ export class Connection {
/** /**
* Fetch the latest blockhash from the cluster * Fetch the latest blockhash from the cluster
* @return {Promise<{blockhash: Blockhash, lastValidBlockHeight: number}>} * @return {Promise<BlockhashWithExpiryBlockHeight>}
*/ */
async getLatestBlockhash( async getLatestBlockhash(
commitment?: Commitment, commitment?: Commitment,
): Promise<{blockhash: Blockhash; lastValidBlockHeight: number}> { ): Promise<BlockhashWithExpiryBlockHeight> {
try { try {
const res = await this.getLatestBlockhashAndContext(commitment); const res = await this.getLatestBlockhashAndContext(commitment);
return res.value; return res.value;
@ -3337,13 +3340,11 @@ export class Connection {
/** /**
* Fetch the latest blockhash from the cluster * Fetch the latest blockhash from the cluster
* @return {Promise<{blockhash: Blockhash, lastValidBlockHeight: number}>} * @return {Promise<BlockhashWithExpiryBlockHeight>}
*/ */
async getLatestBlockhashAndContext( async getLatestBlockhashAndContext(
commitment?: Commitment, commitment?: Commitment,
): Promise< ): Promise<RpcResponseAndContext<BlockhashWithExpiryBlockHeight>> {
RpcResponseAndContext<{blockhash: Blockhash; lastValidBlockHeight: number}>
> {
const args = this._buildArgs([], commitment); const args = this._buildArgs([], commitment);
const unsafeRes = await this._rpcRequest('getLatestBlockhash', args); const unsafeRes = await this._rpcRequest('getLatestBlockhash', args);
const res = create(unsafeRes, GetLatestBlockhashRpcResult); const res = create(unsafeRes, GetLatestBlockhashRpcResult);
@ -3989,7 +3990,9 @@ export class Connection {
/** /**
* @internal * @internal
*/ */
async _recentBlockhash(disableCache: boolean): Promise<Blockhash> { async _blockhashWithExpiryBlockHeight(
disableCache: boolean,
): Promise<BlockhashWithExpiryBlockHeight> {
if (!disableCache) { if (!disableCache) {
// Wait for polling to finish // Wait for polling to finish
while (this._pollingBlockhash) { while (this._pollingBlockhash) {
@ -3997,8 +4000,8 @@ export class Connection {
} }
const timeSinceFetch = Date.now() - this._blockhashInfo.lastFetch; const timeSinceFetch = Date.now() - this._blockhashInfo.lastFetch;
const expired = timeSinceFetch >= BLOCKHASH_CACHE_TIMEOUT_MS; const expired = timeSinceFetch >= BLOCKHASH_CACHE_TIMEOUT_MS;
if (this._blockhashInfo.recentBlockhash !== null && !expired) { if (this._blockhashInfo.latestBlockhash !== null && !expired) {
return this._blockhashInfo.recentBlockhash; return this._blockhashInfo.latestBlockhash;
} }
} }
@ -4008,21 +4011,25 @@ export class Connection {
/** /**
* @internal * @internal
*/ */
async _pollNewBlockhash(): Promise<Blockhash> { async _pollNewBlockhash(): Promise<BlockhashWithExpiryBlockHeight> {
this._pollingBlockhash = true; this._pollingBlockhash = true;
try { try {
const startTime = Date.now(); const startTime = Date.now();
const cachedLatestBlockhash = this._blockhashInfo.latestBlockhash;
const cachedBlockhash = cachedLatestBlockhash
? cachedLatestBlockhash.blockhash
: null;
for (let i = 0; i < 50; i++) { for (let i = 0; i < 50; i++) {
const {blockhash} = await this.getRecentBlockhash('finalized'); const latestBlockhash = await this.getLatestBlockhash('finalized');
if (this._blockhashInfo.recentBlockhash != blockhash) { if (cachedBlockhash !== latestBlockhash.blockhash) {
this._blockhashInfo = { this._blockhashInfo = {
recentBlockhash: blockhash, latestBlockhash,
lastFetch: Date.now(), lastFetch: Date.now(),
transactionSignatures: [], transactionSignatures: [],
simulatedSignatures: [], simulatedSignatures: [],
}; };
return blockhash; return latestBlockhash;
} }
// Sleep for approximately half a slot // Sleep for approximately half a slot
@ -4048,13 +4055,11 @@ export class Connection {
let transaction; let transaction;
if (transactionOrMessage instanceof Transaction) { if (transactionOrMessage instanceof Transaction) {
let originalTx: Transaction = transactionOrMessage; let originalTx: Transaction = transactionOrMessage;
transaction = new Transaction({ transaction = new Transaction();
recentBlockhash: originalTx.recentBlockhash, transaction.feePayer = originalTx.feePayer;
nonceInfo: originalTx.nonceInfo,
feePayer: originalTx.feePayer,
signatures: [...originalTx.signatures],
});
transaction.instructions = transactionOrMessage.instructions; transaction.instructions = transactionOrMessage.instructions;
transaction.nonceInfo = originalTx.nonceInfo;
transaction.signatures = originalTx.signatures;
} else { } else {
transaction = Transaction.populate(transactionOrMessage); transaction = Transaction.populate(transactionOrMessage);
// HACK: this function relies on mutating the populated transaction // HACK: this function relies on mutating the populated transaction
@ -4066,7 +4071,11 @@ export class Connection {
} else { } else {
let disableCache = this._disableBlockhashCaching; let disableCache = this._disableBlockhashCaching;
for (;;) { for (;;) {
transaction.recentBlockhash = await this._recentBlockhash(disableCache); const latestBlockhash = await this._blockhashWithExpiryBlockHeight(
disableCache,
);
transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
transaction.recentBlockhash = latestBlockhash.blockhash;
if (!signers) break; if (!signers) break;
@ -4154,7 +4163,11 @@ export class Connection {
} else { } else {
let disableCache = this._disableBlockhashCaching; let disableCache = this._disableBlockhashCaching;
for (;;) { for (;;) {
transaction.recentBlockhash = await this._recentBlockhash(disableCache); const latestBlockhash = await this._blockhashWithExpiryBlockHeight(
disableCache,
);
transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.sign(...signers); transaction.sign(...signers);
if (!transaction.signature) { if (!transaction.signature) {
throw new Error('!signature'); // should never happen throw new Error('!signature'); // should never happen

View File

@ -1,6 +1,9 @@
import type {Buffer} from 'buffer'; import type {Buffer} from 'buffer';
import {Connection} from '../connection'; import {
BlockheightBasedTransactionConfimationStrategy,
Connection,
} from '../connection';
import type {TransactionSignature} from '../transaction'; import type {TransactionSignature} from '../transaction';
import type {ConfirmOptions} from '../connection'; import type {ConfirmOptions} from '../connection';
@ -11,14 +14,57 @@ import type {ConfirmOptions} from '../connection';
* *
* @param {Connection} connection * @param {Connection} connection
* @param {Buffer} rawTransaction * @param {Buffer} rawTransaction
* @param {BlockheightBasedTransactionConfimationStrategy} confirmationStrategy
* @param {ConfirmOptions} [options] * @param {ConfirmOptions} [options]
* @returns {Promise<TransactionSignature>} * @returns {Promise<TransactionSignature>}
*/ */
export async function sendAndConfirmRawTransaction( export async function sendAndConfirmRawTransaction(
connection: Connection, connection: Connection,
rawTransaction: Buffer, rawTransaction: Buffer,
confirmationStrategy: BlockheightBasedTransactionConfimationStrategy,
options?: ConfirmOptions, options?: ConfirmOptions,
): Promise<TransactionSignature>;
/**
* @deprecated Calling `sendAndConfirmRawTransaction()` without a `confirmationStrategy`
* is no longer supported and will be removed in a future version.
*/
// eslint-disable-next-line no-redeclare
export async function sendAndConfirmRawTransaction(
connection: Connection,
rawTransaction: Buffer,
options?: ConfirmOptions,
): Promise<TransactionSignature>;
// eslint-disable-next-line no-redeclare
export async function sendAndConfirmRawTransaction(
connection: Connection,
rawTransaction: Buffer,
confirmationStrategyOrConfirmOptions:
| BlockheightBasedTransactionConfimationStrategy
| ConfirmOptions
| undefined,
maybeConfirmOptions?: ConfirmOptions,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
let confirmationStrategy:
| BlockheightBasedTransactionConfimationStrategy
| undefined;
let options: ConfirmOptions | undefined;
if (
confirmationStrategyOrConfirmOptions &&
Object.prototype.hasOwnProperty.call(
confirmationStrategyOrConfirmOptions,
'lastValidBlockHeight',
)
) {
confirmationStrategy =
confirmationStrategyOrConfirmOptions as BlockheightBasedTransactionConfimationStrategy;
options = maybeConfirmOptions;
} else {
options = confirmationStrategyOrConfirmOptions as
| ConfirmOptions
| undefined;
}
const sendOptions = options && { const sendOptions = options && {
skipPreflight: options.skipPreflight, skipPreflight: options.skipPreflight,
preflightCommitment: options.preflightCommitment || options.commitment, preflightCommitment: options.preflightCommitment || options.commitment,
@ -29,12 +75,11 @@ export async function sendAndConfirmRawTransaction(
sendOptions, sendOptions,
); );
const status = ( const commitment = options && options.commitment;
await connection.confirmTransaction( const confirmationPromise = confirmationStrategy
signature, ? connection.confirmTransaction(confirmationStrategy, commitment)
options && options.commitment, : connection.confirmTransaction(signature, commitment);
) const status = (await confirmationPromise).value;
).value;
if (status.err) { if (status.err) {
throw new Error( throw new Error(

View File

@ -176,9 +176,9 @@ if (process.env.TEST_LIVE) {
}); });
it('simulate transaction without signature verification', async () => { it('simulate transaction without signature verification', async () => {
const simulatedTransaction = new Transaction({ const simulatedTransaction = new Transaction();
feePayer: payerAccount.publicKey, simulatedTransaction.feePayer = payerAccount.publicKey;
}).add({ simulatedTransaction.add({
keys: [ keys: [
{pubkey: payerAccount.publicKey, isSigner: true, isWritable: true}, {pubkey: payerAccount.publicKey, isSigner: true, isWritable: true},
], ],

View File

@ -1131,7 +1131,11 @@ describe('Connection', function () {
const badTransactionSignature = 'bad transaction signature'; const badTransactionSignature = 'bad transaction signature';
await expect( await expect(
connection.confirmTransaction(badTransactionSignature), connection.confirmTransaction({
blockhash: 'sampleBlockhash',
lastValidBlockHeight: 9999,
signature: badTransactionSignature,
}),
).to.be.rejectedWith('signature must be base58 encoded'); ).to.be.rejectedWith('signature must be base58 encoded');
await mockRpcResponse({ await mockRpcResponse({
@ -2899,11 +2903,11 @@ describe('Connection', function () {
const accountFrom = Keypair.generate(); const accountFrom = Keypair.generate();
const accountTo = Keypair.generate(); const accountTo = Keypair.generate();
const {blockhash} = await helpers.latestBlockhash({connection}); const latestBlockhash = await helpers.latestBlockhash({connection});
const transaction = new Transaction({ const transaction = new Transaction({
feePayer: accountFrom.publicKey, feePayer: accountFrom.publicKey,
recentBlockhash: blockhash, ...latestBlockhash,
}).add( }).add(
SystemProgram.transfer({ SystemProgram.transfer({
fromPubkey: accountFrom.publicKey, fromPubkey: accountFrom.publicKey,
@ -3825,7 +3829,11 @@ describe('Connection', function () {
{skipPreflight: true}, {skipPreflight: true},
); );
await connection.confirmTransaction(signature); await connection.confirmTransaction({
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
signature,
});
const response = (await connection.getSignatureStatus(signature)).value; const response = (await connection.getSignatureStatus(signature)).value;
if (response !== null) { if (response !== null) {

View File

@ -185,7 +185,8 @@ const processTransaction = async ({
commitment: Commitment; commitment: Commitment;
err?: any; err?: any;
}) => { }) => {
const blockhash = (await latestBlockhash({connection})).blockhash; const {blockhash, lastValidBlockHeight} = await latestBlockhash({connection});
transaction.lastValidBlockHeight = lastValidBlockHeight;
transaction.recentBlockhash = blockhash; transaction.recentBlockhash = blockhash;
transaction.sign(...signers); transaction.sign(...signers);
@ -222,7 +223,10 @@ const processTransaction = async ({
result: true, result: true,
}); });
return await connection.confirmTransaction(signature, commitment); return await connection.confirmTransaction(
{blockhash, lastValidBlockHeight, signature},
commitment,
);
}; };
const airdrop = async ({ const airdrop = async ({

View File

@ -339,9 +339,10 @@ describe('StakeProgram', () => {
lockup: new Lockup(0, 0, from.publicKey), lockup: new Lockup(0, 0, from.publicKey),
lamports: amount, lamports: amount,
}); });
const createWithSeedTransaction = new Transaction({recentBlockhash}).add( const createWithSeedTransaction = new Transaction({
createWithSeed, blockhash: recentBlockhash,
); lastValidBlockHeight: 9999,
}).add(createWithSeed);
expect(createWithSeedTransaction.instructions).to.have.length(2); expect(createWithSeedTransaction.instructions).to.have.length(2);
const systemInstructionType = SystemInstruction.decodeInstructionType( const systemInstructionType = SystemInstruction.decodeInstructionType(
@ -368,9 +369,10 @@ describe('StakeProgram', () => {
votePubkey: vote.publicKey, votePubkey: vote.publicKey,
}); });
const delegateTransaction = new Transaction({recentBlockhash}).add( const delegateTransaction = new Transaction({
delegate, blockhash: recentBlockhash,
); lastValidBlockHeight: 9999,
}).add(delegate);
const anotherStakeInstructionType = StakeInstruction.decodeInstructionType( const anotherStakeInstructionType = StakeInstruction.decodeInstructionType(
delegateTransaction.instructions[0], delegateTransaction.instructions[0],
); );

View File

@ -23,7 +23,10 @@ describe('Transaction', () => {
const account3 = Keypair.generate(); const account3 = Keypair.generate();
const recentBlockhash = Keypair.generate().publicKey.toBase58(); const recentBlockhash = Keypair.generate().publicKey.toBase58();
const programId = Keypair.generate().publicKey; const programId = Keypair.generate().publicKey;
const transaction = new Transaction({recentBlockhash}).add({ const transaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add({
keys: [ keys: [
{pubkey: account3.publicKey, isSigner: true, isWritable: false}, {pubkey: account3.publicKey, isSigner: true, isWritable: false},
{pubkey: payer.publicKey, isSigner: true, isWritable: true}, {pubkey: payer.publicKey, isSigner: true, isWritable: true},
@ -49,7 +52,10 @@ describe('Transaction', () => {
const other = Keypair.generate(); const other = Keypair.generate();
const recentBlockhash = Keypair.generate().publicKey.toBase58(); const recentBlockhash = Keypair.generate().publicKey.toBase58();
const programId = Keypair.generate().publicKey; const programId = Keypair.generate().publicKey;
const transaction = new Transaction({recentBlockhash}).add({ const transaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add({
keys: [ keys: [
{pubkey: other.publicKey, isSigner: true, isWritable: true}, {pubkey: other.publicKey, isSigner: true, isWritable: true},
{pubkey: payer.publicKey, isSigner: true, isWritable: true}, {pubkey: payer.publicKey, isSigner: true, isWritable: true},
@ -101,7 +107,10 @@ describe('Transaction', () => {
const payer = Keypair.generate(); const payer = Keypair.generate();
const recentBlockhash = Keypair.generate().publicKey.toBase58(); const recentBlockhash = Keypair.generate().publicKey.toBase58();
const programId = Keypair.generate().publicKey; const programId = Keypair.generate().publicKey;
const transaction = new Transaction({recentBlockhash}).add({ const transaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add({
keys: [{pubkey: payer.publicKey, isSigner: true, isWritable: false}], keys: [{pubkey: payer.publicKey, isSigner: true, isWritable: false}],
programId, programId,
}); });
@ -121,11 +130,11 @@ describe('Transaction', () => {
const accountFrom = Keypair.generate(); const accountFrom = Keypair.generate();
const accountTo = Keypair.generate(); const accountTo = Keypair.generate();
const {blockhash} = await helpers.latestBlockhash({connection}); const latestBlockhash = await helpers.latestBlockhash({connection});
const transaction = new Transaction({ const transaction = new Transaction({
feePayer: accountFrom.publicKey, feePayer: accountFrom.publicKey,
recentBlockhash: blockhash, ...latestBlockhash,
}).add( }).add(
SystemProgram.transfer({ SystemProgram.transfer({
fromPubkey: accountFrom.publicKey, fromPubkey: accountFrom.publicKey,
@ -149,10 +158,16 @@ describe('Transaction', () => {
lamports: 123, lamports: 123,
}); });
const transaction = new Transaction({recentBlockhash}).add(transfer); const transaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add(transfer);
transaction.sign(account1, account2); transaction.sign(account1, account2);
const partialTransaction = new Transaction({recentBlockhash}).add(transfer); const partialTransaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add(transfer);
partialTransaction.setSigners(account1.publicKey, account2.publicKey); partialTransaction.setSigners(account1.publicKey, account2.publicKey);
expect(partialTransaction.signatures[0].signature).to.be.null; expect(partialTransaction.signatures[0].signature).to.be.null;
expect(partialTransaction.signatures[1].signature).to.be.null; expect(partialTransaction.signatures[1].signature).to.be.null;
@ -196,7 +211,10 @@ describe('Transaction', () => {
const programId = Keypair.generate().publicKey; const programId = Keypair.generate().publicKey;
it('setSigners', () => { it('setSigners', () => {
const transaction = new Transaction({recentBlockhash}).add({ const transaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add({
keys: [ keys: [
{pubkey: duplicate1.publicKey, isSigner: true, isWritable: true}, {pubkey: duplicate1.publicKey, isSigner: true, isWritable: true},
{pubkey: payer.publicKey, isSigner: false, isWritable: true}, {pubkey: payer.publicKey, isSigner: false, isWritable: true},
@ -224,7 +242,10 @@ describe('Transaction', () => {
}); });
it('sign', () => { it('sign', () => {
const transaction = new Transaction({recentBlockhash}).add({ const transaction = new Transaction({
blockhash: recentBlockhash,
lastValidBlockHeight: 9999,
}).add({
keys: [ keys: [
{pubkey: duplicate1.publicKey, isSigner: true, isWritable: true}, {pubkey: duplicate1.publicKey, isSigner: true, isWritable: true},
{pubkey: payer.publicKey, isSigner: false, isWritable: true}, {pubkey: payer.publicKey, isSigner: false, isWritable: true},
@ -263,14 +284,18 @@ describe('Transaction', () => {
lamports: 123, lamports: 123,
}); });
const orgTransaction = new Transaction({recentBlockhash}).add( const latestBlockhash = {
transfer1, blockhash: recentBlockhash,
transfer2, lastValidBlockHeight: 9999,
); };
const orgTransaction = new Transaction({
...latestBlockhash,
}).add(transfer1, transfer2);
orgTransaction.sign(account1, account2); orgTransaction.sign(account1, account2);
const newTransaction = new Transaction({ const newTransaction = new Transaction({
recentBlockhash: orgTransaction.recentBlockhash, ...latestBlockhash,
signatures: orgTransaction.signatures, signatures: orgTransaction.signatures,
}).add(transfer1, transfer2); }).add(transfer1, transfer2);
@ -292,10 +317,10 @@ describe('Transaction', () => {
lamports: 123, lamports: 123,
}); });
const orgTransaction = new Transaction({recentBlockhash}).add( const orgTransaction = new Transaction({
transfer1, blockhash: recentBlockhash,
transfer2, lastValidBlockHeight: 9999,
); }).add(transfer1, transfer2);
orgTransaction.sign(account1); orgTransaction.sign(account1);
}); });
@ -363,8 +388,9 @@ describe('Transaction', () => {
lamports: 49, lamports: 49,
}); });
const expectedTransaction = new Transaction({ const expectedTransaction = new Transaction({
recentBlockhash, blockhash: recentBlockhash,
feePayer: sender.publicKey, feePayer: sender.publicKey,
lastValidBlockHeight: 9999,
}).add(transfer); }).add(transfer);
expectedTransaction.sign(sender); expectedTransaction.sign(sender);
@ -493,9 +519,10 @@ describe('Transaction', () => {
toPubkey: recipient, toPubkey: recipient,
lamports: 49, lamports: 49,
}); });
const expectedTransaction = new Transaction({recentBlockhash}).add( const expectedTransaction = new Transaction({
transfer, blockhash: recentBlockhash,
); lastValidBlockHeight: 9999,
}).add(transfer);
// Empty signature array fails. // Empty signature array fails.
expect(expectedTransaction.signatures).to.have.length(0); expect(expectedTransaction.signatures).to.have.length(0);
@ -607,8 +634,9 @@ describe('Transaction', () => {
const acc1Writable = Keypair.generate(); const acc1Writable = Keypair.generate();
const acc2Writable = Keypair.generate(); const acc2Writable = Keypair.generate();
const t0 = new Transaction({ const t0 = new Transaction({
recentBlockhash: 'HZaTsZuhN1aaz9WuuimCFMyH7wJ5xiyMUHFCnZSMyguH', blockhash: 'HZaTsZuhN1aaz9WuuimCFMyH7wJ5xiyMUHFCnZSMyguH',
feePayer: signer.publicKey, feePayer: signer.publicKey,
lastValidBlockHeight: 9999,
}); });
t0.add( t0.add(
new TransactionInstruction({ new TransactionInstruction({