diff --git a/sdk/scripts/tx-sender-test.ts b/sdk/scripts/tx-sender-test.ts new file mode 100644 index 0000000000..d5b25331e5 --- /dev/null +++ b/sdk/scripts/tx-sender-test.ts @@ -0,0 +1,324 @@ +import { + Connection, + Transaction, + TransactionInstruction, + PublicKey, + VersionedTransaction, + TransactionMessage, +} from '@solana/web3.js'; +import { RetryTxSender, WhileValidTxSender, Wallet, loadKeypair } from '../src'; + +// ============================================================================ +// Configuration +// ============================================================================ + +const RPC_ENDPOINT = 'https://api.devnet.solana.com'; +const MEMO_PROGRAM_ID = new PublicKey( + 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr' +); + +// ============================================================================ +// Helper Functions +// ============================================================================ + +async function verifyTransactionFeePayer( + connection: Connection, + txSig: string, + expectedFeePayer: PublicKey +): Promise { + console.log('šŸ” Verifying transaction details...'); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const tx = await connection.getParsedTransaction(txSig, { + commitment: 'confirmed', + maxSupportedTransactionVersion: 0, + }); + + if (!tx) { + throw new Error(`Transaction ${txSig} not found`); + } + + const actualFeePayer = tx.transaction.message.accountKeys[0].pubkey; + + console.log(' Expected Fee Payer:', expectedFeePayer.toBase58()); + console.log(' Actual Fee Payer: ', actualFeePayer.toBase58()); + console.log(' Transaction Fee: ', tx.meta?.fee || 0, 'lamports'); + + const signers = tx.transaction.message.accountKeys + .filter((key) => key.signer) + .map((key) => key.pubkey.toBase58()); + console.log(' Signers:'); + signers.forEach((signer, idx) => { + console.log(` ${idx + 1}. ${signer}`); + }); + + if (!actualFeePayer.equals(expectedFeePayer)) { + throw new Error( + `Fee payer mismatch! Expected: ${expectedFeePayer.toBase58()}, Got: ${actualFeePayer.toBase58()}` + ); + } + + console.log(' āœ… Fee payer verification passed!\n'); +} + +// ============================================================================ +// Test Functions +// ============================================================================ + +async function testRetryTxSender(wallet: Wallet, connection: Connection) { + console.log('\n========================================'); + console.log('Testing RetryTxSender'); + console.log('========================================\n'); + + const retryTxSender = new RetryTxSender({ + connection, + wallet, + }); + + const expectedFeePayer = wallet.payer!.publicKey; + + // Test Legacy Transaction + console.log('šŸ“¤ Sending legacy transaction...'); + const legacyTx = new Transaction({ + feePayer: expectedFeePayer, + }).add( + new TransactionInstruction({ + keys: [ + { + pubkey: wallet.authority.publicKey, + isSigner: true, + isWritable: true, + }, + { + pubkey: expectedFeePayer, + isSigner: false, + isWritable: true, + }, + ], + data: Buffer.from('RetryTxSender - Legacy Transaction Test', 'utf-8'), + programId: MEMO_PROGRAM_ID, + }) + ); + const { txSig: legacyTxSig } = await retryTxSender.send(legacyTx); + console.log(`\nāœ… RetryTxSender - Legacy Transaction sent successfully`); + console.log(` Signature: ${legacyTxSig}`); + console.log( + ` Explorer: https://solscan.io/tx/${legacyTxSig}?cluster=devnet\n` + ); + await verifyTransactionFeePayer(connection, legacyTxSig, expectedFeePayer); + + // Test Versioned Transaction + console.log('šŸ“¤ Sending versioned transaction...'); + const { blockhash } = await connection.getLatestBlockhash(); + const versionedTxMessage = new TransactionMessage({ + payerKey: expectedFeePayer, + instructions: [ + new TransactionInstruction({ + keys: [ + { + pubkey: wallet.authority.publicKey, + isSigner: true, + isWritable: true, + }, + { + pubkey: expectedFeePayer, + isSigner: false, + isWritable: true, + }, + ], + data: Buffer.from( + 'RetryTxSender - Versioned Transaction Test', + 'utf-8' + ), + programId: MEMO_PROGRAM_ID, + }), + ], + recentBlockhash: blockhash, + }); + const versionedTx = new VersionedTransaction( + versionedTxMessage.compileToV0Message([]) + ); + const { txSig: versionedTxSig } = + await retryTxSender.sendVersionedTransaction(versionedTx); + console.log(`\nāœ… RetryTxSender - Versioned Transaction sent successfully`); + console.log(` Signature: ${versionedTxSig}`); + console.log( + ` Explorer: https://solscan.io/tx/${versionedTxSig}?cluster=devnet\n` + ); + await verifyTransactionFeePayer(connection, versionedTxSig, expectedFeePayer); +} + +async function testWhileValidTxSender(wallet: Wallet, connection: Connection) { + console.log('\n========================================'); + console.log('Testing WhileValidTxSender'); + console.log('========================================\n'); + + const whileValidTxSender = new WhileValidTxSender({ + connection, + wallet, + timeout: 60000, + retrySleep: 1000, + }); + + const expectedFeePayer = wallet.payer!.publicKey; + + // Test Legacy Transaction + console.log('šŸ“¤ Sending legacy transaction...'); + const legacyTx = new Transaction({ + feePayer: expectedFeePayer, + }).add( + new TransactionInstruction({ + keys: [ + { + pubkey: wallet.authority.publicKey, + isSigner: true, + isWritable: true, + }, + { + pubkey: expectedFeePayer, + isSigner: false, + isWritable: true, + }, + ], + data: Buffer.from( + 'WhileValidTxSender - Legacy Transaction Test', + 'utf-8' + ), + programId: MEMO_PROGRAM_ID, + }) + ); + const { txSig: legacyTxSig } = await whileValidTxSender.send(legacyTx); + console.log(`\nāœ… WhileValidTxSender - Legacy Transaction sent successfully`); + console.log(` Signature: ${legacyTxSig}`); + console.log( + ` Explorer: https://solscan.io/tx/${legacyTxSig}?cluster=devnet\n` + ); + await verifyTransactionFeePayer(connection, legacyTxSig, expectedFeePayer); + + // Test Versioned Transaction + console.log('šŸ“¤ Sending versioned transaction...'); + const { blockhash } = await connection.getLatestBlockhash(); + const versionedTxMessage = new TransactionMessage({ + payerKey: expectedFeePayer, + instructions: [ + new TransactionInstruction({ + keys: [ + { + pubkey: wallet.authority.publicKey, + isSigner: true, + isWritable: true, + }, + { + pubkey: expectedFeePayer, + isSigner: false, + isWritable: true, + }, + ], + data: Buffer.from( + 'WhileValidTxSender - Versioned Transaction Test', + 'utf-8' + ), + programId: MEMO_PROGRAM_ID, + }), + ], + recentBlockhash: blockhash, + }); + const versionedTx = new VersionedTransaction( + versionedTxMessage.compileToV0Message([]) + ); + const { txSig: versionedTxSig } = + await whileValidTxSender.sendVersionedTransaction(versionedTx); + console.log( + `\nāœ… WhileValidTxSender - Versioned Transaction sent successfully` + ); + console.log(` Signature: ${versionedTxSig}`); + console.log( + ` Explorer: https://solscan.io/tx/${versionedTxSig}?cluster=devnet\n` + ); + await verifyTransactionFeePayer(connection, versionedTxSig, expectedFeePayer); +} + +// ============================================================================ +// Main Execution +// ============================================================================ + +async function main() { + try { + const connection = new Connection(RPC_ENDPOINT); + + // Case 1: Wallet with separate fee payer + console.log('\n========================================'); + console.log('Case 1: Testing with SEPARATE Fee Payer'); + console.log('========================================\n'); + + const privateKey = process.env.PRIVATE_KEY!; + const feePayerPrivateKey = process.env.FEE_PAYER_PRIVATE_KEY!; + const keypair = loadKeypair(privateKey); + const feePayerKeypair = loadKeypair(feePayerPrivateKey); + const walletWithFeePayer = new Wallet(keypair, feePayerKeypair); + + console.log( + 'Authority Public Key:', + walletWithFeePayer.publicKey.toBase58() + ); + console.log( + 'Fee Payer Public Key:', + walletWithFeePayer.payer?.publicKey?.toBase58() + ); + + if (!walletWithFeePayer.payer) { + console.warn( + 'āš ļø Warning: FEE_PAYER_PRIVATE_KEY not set. Skipping separate fee payer tests.' + ); + } else { + console.log( + '\nāœ… Fee payer is DIFFERENT from authority - this should be reflected in transaction logs\n' + ); + + await testRetryTxSender(walletWithFeePayer, connection); + await testWhileValidTxSender(walletWithFeePayer, connection); + } + + // Case 2: Wallet without separate fee payer (authority pays fees) + console.log('\n========================================'); + console.log('Case 2: Testing with SAME Authority and Fee Payer'); + console.log('========================================\n'); + + const walletWithoutFeePayer = new Wallet(keypair); + + console.log( + 'Authority Public Key:', + walletWithoutFeePayer.publicKey.toBase58() + ); + console.log( + 'Fee Payer Public Key:', + walletWithoutFeePayer.payer?.publicKey?.toBase58() + ); + console.log( + '\nāœ… Fee payer is SAME as authority - this should be reflected in transaction logs\n' + ); + + await testRetryTxSender(walletWithoutFeePayer, connection); + await testWhileValidTxSender(walletWithoutFeePayer, connection); + + console.log('\n========================================'); + console.log('All Tests Completed Successfully! šŸŽ‰'); + console.log('========================================\n'); + console.log('Summary:'); + console.log( + 'āœ… Verified fee payer is correctly set when using separate fee payer' + ); + console.log( + 'āœ… Verified fee payer is correctly set when authority pays fees' + ); + console.log( + 'āœ… All transaction logs confirmed correct fee payer assignment\n' + ); + } catch (error) { + console.error('\nāŒ Error occurred:', error); + process.exit(1); + } +} + +main(); diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index 2d1ecb13ae..97820b682e 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1118,7 +1118,7 @@ export class DriftClient { this.wallet.publicKey // only allow payer to initialize own user stats account ), authority: this.wallet.publicKey, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, state: await this.getStatePublicKey(), @@ -1158,7 +1158,7 @@ export class DriftClient { accounts: { signedMsgUserOrders: signedMsgUserAccountPublicKey, authority, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, @@ -1199,7 +1199,7 @@ export class DriftClient { accounts: { signedMsgUserOrders: signedMsgUserAccountPublicKey, authority, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, user: await getUserAccountPublicKey( this.program.programId, @@ -1272,7 +1272,10 @@ export class DriftClient { accounts: { revenueShare, authority, - payer: overrides?.payer ?? this.wallet.publicKey, + payer: + overrides?.payer ?? + this.wallet.payer?.publicKey ?? + this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, @@ -1308,7 +1311,10 @@ export class DriftClient { accounts: { escrow, authority, - payer: overrides?.payer ?? this.wallet.publicKey, + payer: + overrides?.payer ?? + this.wallet.payer?.publicKey ?? + this.wallet.publicKey, userStats: getUserStatsAccountPublicKey( this.program.programId, authority @@ -1346,7 +1352,7 @@ export class DriftClient { authority ), state: await this.getStatePublicKey(), - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, }, }); } @@ -1377,7 +1383,7 @@ export class DriftClient { accounts: { escrow, authority, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }, }); @@ -1428,7 +1434,8 @@ export class DriftClient { } ): Promise { const authority = overrides?.authority ?? this.wallet.publicKey; - const payer = overrides?.payer ?? this.wallet.publicKey; + const payer = + overrides?.payer ?? this.wallet.payer?.publicKey ?? this.wallet.publicKey; const escrow = getRevenueShareEscrowAccountPublicKey( this.program.programId, authority @@ -1537,7 +1544,7 @@ export class DriftClient { authority ?? this.wallet.publicKey ), authority: authority ?? this.wallet.publicKey, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, @@ -1623,7 +1630,7 @@ export class DriftClient { user: userAccountPublicKey, userStats: this.getUserStatsAccountPublicKey(), authority: this.wallet.publicKey, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, state: await this.getStatePublicKey(), @@ -1673,7 +1680,7 @@ export class DriftClient { user: userAccountPublicKey, authority: this.wallet.publicKey, userStats: this.getUserStatsAccountPublicKey(), - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, @@ -9458,7 +9465,7 @@ export class DriftClient { this.wallet.publicKey // only allow payer to initialize own insurance fund stake account ), authority: this.wallet.publicKey, - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, state: await this.getStatePublicKey(), @@ -10545,7 +10552,7 @@ export class DriftClient { const tx = await asV0Tx({ connection: this.connection, ixs: [pullIx], - payer: this.wallet.publicKey, + payer: this.wallet.payer?.publicKey ?? this.wallet.publicKey, computeUnitLimitMultiple: 1.3, lookupTables: await this.fetchAllLookupTableAccounts(), }); diff --git a/sdk/src/tx/baseTxSender.ts b/sdk/src/tx/baseTxSender.ts index 23db04be91..904d1182f2 100644 --- a/sdk/src/tx/baseTxSender.ts +++ b/sdk/src/tx/baseTxSender.ts @@ -173,11 +173,6 @@ export abstract class BaseTxSender implements TxSender { if (preSigned) { signedTx = tx; - // @ts-ignore - } else if (this.wallet.payer) { - // @ts-ignore - tx.sign((additionalSigners ?? []).concat(this.wallet.payer)); - signedTx = tx; } else { signedTx = await this.txHandler.signVersionedTx( tx, diff --git a/sdk/src/tx/txHandler.ts b/sdk/src/tx/txHandler.ts index bd9b2b702e..c6412d175a 100644 --- a/sdk/src/tx/txHandler.ts +++ b/sdk/src/tx/txHandler.ts @@ -191,7 +191,7 @@ export class TxHandler { [wallet, confirmationOpts] = this.getProps(wallet, confirmationOpts); - tx.feePayer = wallet.publicKey; + tx.feePayer = wallet.payer?.publicKey ?? wallet.publicKey; recentBlockhash = recentBlockhash ? recentBlockhash : await this.getLatestBlockhashForTransaction(); @@ -296,7 +296,9 @@ export class TxHandler { this.preSignedCb?.(); //@ts-ignore - const signedTx = (await wallet.signTransaction(tx)) as VersionedTransaction; + const signedTx = (await wallet.signVersionedTransaction( + tx + )) as VersionedTransaction; // Turn txSig Buffer into base58 string const txSig = this.getTxSigFromSignedTx(signedTx); @@ -398,7 +400,7 @@ export class TxHandler { [wallet] = this.getProps(wallet); const message = new TransactionMessage({ - payerKey: wallet.publicKey, + payerKey: wallet.payer?.publicKey ?? wallet.publicKey, recentBlockhash: recentBlockhash.blockhash, instructions: ixs, }).compileToLegacyMessage(); @@ -420,7 +422,7 @@ export class TxHandler { [wallet] = this.getProps(wallet); const message = new TransactionMessage({ - payerKey: wallet.publicKey, + payerKey: wallet.payer?.publicKey ?? wallet.publicKey, recentBlockhash: recentBlockhash.blockhash, instructions: ixs, }).compileToV0Message(lookupTableAccounts); @@ -646,10 +648,12 @@ export class TxHandler { this.addHashAndExpiryToLookup(recentBlockhash); + [wallet] = this.getProps(wallet); + for (const tx of Object.values(txsMap)) { if (!tx) continue; tx.recentBlockhash = recentBlockhash.blockhash; - tx.feePayer = wallet?.publicKey ?? this.wallet?.publicKey; + tx.feePayer = wallet.payer?.publicKey ?? wallet.publicKey; // @ts-ignore tx.SIGNATURE_BLOCK_AND_EXPIRY = recentBlockhash; @@ -689,7 +693,8 @@ export class TxHandler { // Extra handling for legacy transactions for (const [_key, tx] of filteredTxEntries) { if (this.isLegacyTransaction(tx)) { - (tx as Transaction).feePayer = wallet.publicKey; + (tx as Transaction).feePayer = + wallet.payer?.publicKey ?? wallet.publicKey; } } diff --git a/sdk/src/tx/whileValidTxSender.ts b/sdk/src/tx/whileValidTxSender.ts index 28b4ea0f23..3508963d4d 100644 --- a/sdk/src/tx/whileValidTxSender.ts +++ b/sdk/src/tx/whileValidTxSender.ts @@ -60,6 +60,7 @@ export class WhileValidTxSender extends BaseTxSender { connection, wallet, opts = { ...DEFAULT_CONFIRMATION_OPTS, maxRetries: 0 }, + timeout, retrySleep = DEFAULT_RETRY, additionalConnections = new Array(), confirmationStrategy = ConfirmationStrategy.Combo, @@ -74,6 +75,7 @@ export class WhileValidTxSender extends BaseTxSender { connection: Connection; wallet: IWallet; opts?: ConfirmOptions; + timeout?: number; retrySleep?: number; additionalConnections?; additionalTxSenderCallbacks?: ((base58EncodedTx: string) => void)[]; @@ -89,6 +91,7 @@ export class WhileValidTxSender extends BaseTxSender { connection, wallet, opts, + timeout, additionalConnections, additionalTxSenderCallbacks, txHandler, @@ -99,7 +102,12 @@ export class WhileValidTxSender extends BaseTxSender { throwOnTimeoutError, throwOnTransactionError, }); + this.connection = connection; + this.wallet = wallet; + this.opts = opts; + this.timeout = timeout; this.retrySleep = retrySleep; + this.additionalConnections = additionalConnections; this.checkAndSetUseBlockHeightOffset(); } @@ -168,20 +176,8 @@ export class WhileValidTxSender extends BaseTxSender { // @ts-ignore latestBlockhash = tx.SIGNATURE_BLOCK_AND_EXPIRY; } - - // @ts-ignore - } else if (this.wallet.payer) { - tx.message.recentBlockhash = latestBlockhash.blockhash; - // @ts-ignore - tx.sign((additionalSigners ?? []).concat(this.wallet.payer)); - signedTx = tx; } else { tx.message.recentBlockhash = latestBlockhash.blockhash; - additionalSigners - ?.filter((s): s is Signer => s !== undefined) - .forEach((kp) => { - tx.sign([kp]); - }); signedTx = await this.txHandler.signVersionedTx( tx, additionalSigners, diff --git a/sdk/src/wallet.ts b/sdk/src/wallet.ts index 05c5759a17..334f5974db 100644 --- a/sdk/src/wallet.ts +++ b/sdk/src/wallet.ts @@ -8,23 +8,51 @@ import { IWallet, IVersionedWallet } from './types'; import nacl from 'tweetnacl'; export class Wallet implements IWallet, IVersionedWallet { - constructor(readonly payer: Keypair) {} + readonly payer?: Keypair; + + constructor( + readonly authority: Keypair, + payer?: Keypair + ) { + this.payer = payer ?? authority; + } async signTransaction(tx: Transaction): Promise { - tx.partialSign(this.payer); + if ( + this.payer && + this.payer.publicKey.toBase58() !== this.authority.publicKey.toBase58() + ) { + tx.partialSign(this.payer, this.authority); + } else { + tx.partialSign(this.authority); + } return tx; } async signVersionedTransaction( tx: VersionedTransaction ): Promise { - tx.sign([this.payer]); + if ( + this.payer && + this.payer.publicKey.toBase58() !== this.authority.publicKey.toBase58() + ) { + tx.sign([this.payer, this.authority]); + } else { + tx.sign([this.authority]); + } return tx; } async signAllTransactions(txs: Transaction[]): Promise { return txs.map((t) => { - t.partialSign(this.payer); + if ( + this.payer && + this.payer.publicKey.toBase58() !== this.authority.publicKey.toBase58() + ) { + t.partialSign(this.payer, this.authority); + } else { + t.partialSign(this.authority); + } return t; }); } @@ -33,22 +61,29 @@ export class Wallet implements IWallet, IVersionedWallet { txs: VersionedTransaction[] ): Promise { return txs.map((t) => { - t.sign([this.payer]); + if ( + this.payer && + this.payer.publicKey.toBase58() !== this.authority.publicKey.toBase58() + ) { + t.sign([this.payer, this.authority]); + } else { + t.sign([this.authority]); + } return t; }); } get publicKey(): PublicKey { - return this.payer.publicKey; + return this.authority.publicKey; } } export class WalletV2 extends Wallet { - constructor(readonly payer: Keypair) { - super(payer); + constructor(readonly authority: Keypair) { + super(authority); } async signMessage(message: Uint8Array): Promise { - return Buffer.from(nacl.sign.detached(message, this.payer.secretKey)); + return Buffer.from(nacl.sign.detached(message, this.authority.secretKey)); } } diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 287c77d725..44ecf08c30 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -672,9 +672,9 @@ export class MockUserMap implements UserMapInterface { }); } - public async subscribe(): Promise { } + public async subscribe(): Promise {} - public async unsubscribe(): Promise { } + public async unsubscribe(): Promise {} public async addPubkey(userAccountPublicKey: PublicKey): Promise { const user = new User({ @@ -733,7 +733,7 @@ export class MockUserMap implements UserMapInterface { ); } - public async updateWithOrderRecord(_record: OrderRecord): Promise { } + public async updateWithOrderRecord(_record: OrderRecord): Promise {} public values(): IterableIterator { return this.userMap.values();