From 1e6838472cad9d3c05d7051b44fd9e9fd702a7c8 Mon Sep 17 00:00:00 2001 From: broody Date: Thu, 2 Oct 2025 09:36:22 -1000 Subject: [PATCH 1/4] fix: properly serialize Solana transactions with empty signature placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When serializing unsigned Solana transactions for Phantom wallet, the previous implementation used `sol.createTxComplex()` which returned a hex string that didn't include proper signature slots. This caused Phantom to throw "Reader(signatures/0): readBytes: Unexpected end of buffer" when trying to deserialize the transaction. The fix creates a properly formatted transaction structure with: - A message containing feePayer, instructions, and recentBlockhash - Empty 64-byte signature placeholders for all required signers - Proper encoding using sol.Transaction.encode() This ensures Phantom can successfully deserialize and sign the transaction. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/controller/src/utils/solana/index.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/controller/src/utils/solana/index.ts b/packages/controller/src/utils/solana/index.ts index 70d5efe9a2..2cd519c572 100644 --- a/packages/controller/src/utils/solana/index.ts +++ b/packages/controller/src/utils/solana/index.ts @@ -98,13 +98,30 @@ export class Transaction { throw new Error("Transaction requires feePayer and recentBlockhash"); } - const txHex = sol.createTxComplex( - this.feePayer.toString(), - this._instructions, - this.recentBlockhash, - ); + // Build the transaction message + const message = { + feePayer: this.feePayer.toString(), + instructions: this._instructions, + recentBlockhash: this.recentBlockhash, + }; + + // Collect all unique signers (feePayer + instruction signers) + const signers = new Set(); + signers.add(this.feePayer.toString()); + for (const ix of this._instructions) { + for (const key of ix.keys) { + if (key.sign) { + signers.add(key.address); + } + } + } - return Buffer.from(txHex, "hex"); + // Create empty signatures (64 bytes each) for all signers + const signatures = Array.from(signers).map(() => new Uint8Array(64)); + + // Encode the transaction with message and empty signatures + const tx = { message, signatures }; + return Buffer.from(sol.Transaction.encode(tx)); } serializeMessage(): Buffer { From 05406adfc5f695b81c8ab0182ec5fecac1d238d8 Mon Sep 17 00:00:00 2001 From: broody Date: Thu, 2 Oct 2025 09:56:08 -1000 Subject: [PATCH 2/4] fix: use base64 decoding for createTxComplex output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix incorrectly assumed createTxComplex returned hex, but it actually returns a base64-encoded transaction string that already includes properly formatted empty signatures. micro-sol-signer's signature format is an object/map where keys are signer addresses and values are 64-byte signature buffers, not an array. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/controller/src/utils/solana/index.ts | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/controller/src/utils/solana/index.ts b/packages/controller/src/utils/solana/index.ts index 2cd519c572..9005793654 100644 --- a/packages/controller/src/utils/solana/index.ts +++ b/packages/controller/src/utils/solana/index.ts @@ -98,30 +98,15 @@ export class Transaction { throw new Error("Transaction requires feePayer and recentBlockhash"); } - // Build the transaction message - const message = { - feePayer: this.feePayer.toString(), - instructions: this._instructions, - recentBlockhash: this.recentBlockhash, - }; - - // Collect all unique signers (feePayer + instruction signers) - const signers = new Set(); - signers.add(this.feePayer.toString()); - for (const ix of this._instructions) { - for (const key of ix.keys) { - if (key.sign) { - signers.add(key.address); - } - } - } - - // Create empty signatures (64 bytes each) for all signers - const signatures = Array.from(signers).map(() => new Uint8Array(64)); + // Use createTxComplex which returns a base64-encoded transaction with empty signatures + const txBase64 = sol.createTxComplex( + this.feePayer.toString(), + this._instructions, + this.recentBlockhash, + ); - // Encode the transaction with message and empty signatures - const tx = { message, signatures }; - return Buffer.from(sol.Transaction.encode(tx)); + // Decode the base64 string to get the transaction bytes + return Buffer.from(txBase64, "base64"); } serializeMessage(): Buffer { From e3c77258f831e2b919512a99d3e325b5fdd7dcce Mon Sep 17 00:00:00 2001 From: broody Date: Thu, 2 Oct 2025 10:05:12 -1000 Subject: [PATCH 3/4] debug: add console logs to trace transaction serialization --- packages/controller/src/utils/solana/index.ts | 26 ++++++++++++++++++- .../controller/src/wallets/phantom/index.ts | 18 +++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/controller/src/utils/solana/index.ts b/packages/controller/src/utils/solana/index.ts index 9005793654..bb3b4f8edb 100644 --- a/packages/controller/src/utils/solana/index.ts +++ b/packages/controller/src/utils/solana/index.ts @@ -90,6 +90,7 @@ export class Transaction { // Build transaction using micro-sol-signer if (this._transaction) { // If we have a decoded transaction, re-serialize it + console.log("[Transaction.serialize] Re-encoding existing transaction"); return Buffer.from(sol.Transaction.encode(this._transaction)); } @@ -98,6 +99,17 @@ export class Transaction { throw new Error("Transaction requires feePayer and recentBlockhash"); } + console.log( + "[Transaction.serialize] Building new transaction with", + this._instructions.length, + "instructions", + ); + console.log("[Transaction.serialize] feePayer:", this.feePayer.toString()); + console.log( + "[Transaction.serialize] recentBlockhash:", + this.recentBlockhash, + ); + // Use createTxComplex which returns a base64-encoded transaction with empty signatures const txBase64 = sol.createTxComplex( this.feePayer.toString(), @@ -105,8 +117,20 @@ export class Transaction { this.recentBlockhash, ); + console.log( + "[Transaction.serialize] createTxComplex returned base64 length:", + txBase64.length, + ); + // Decode the base64 string to get the transaction bytes - return Buffer.from(txBase64, "base64"); + const buffer = Buffer.from(txBase64, "base64"); + console.log("[Transaction.serialize] Final buffer length:", buffer.length); + console.log( + "[Transaction.serialize] First 32 bytes:", + Array.from(buffer.slice(0, 32)), + ); + + return buffer; } serializeMessage(): Buffer { diff --git a/packages/controller/src/wallets/phantom/index.ts b/packages/controller/src/wallets/phantom/index.ts index ef9d3833c1..e4177cdffa 100644 --- a/packages/controller/src/wallets/phantom/index.ts +++ b/packages/controller/src/wallets/phantom/index.ts @@ -135,7 +135,25 @@ export class PhantomWallet implements WalletAdapter { } try { + console.log( + "[PhantomWallet.sendTransaction] Received serialized_txn length:", + serailized_txn.length, + ); + console.log( + "[PhantomWallet.sendTransaction] First 32 bytes:", + Array.from(serailized_txn.slice(0, 32)), + ); + console.log( + "[PhantomWallet.sendTransaction] Deserializing with Transaction.from...", + ); + const txn = Transaction.from(serailized_txn); + + console.log("[PhantomWallet.sendTransaction] Successfully deserialized"); + console.log( + "[PhantomWallet.sendTransaction] Sending to Phantom provider...", + ); + const provider = this.getProvider(); const result = await provider.signAndSendTransaction(txn); return { From faf2f19b6641086f774ee130cde946b24293f818 Mon Sep 17 00:00:00 2001 From: broody Date: Thu, 2 Oct 2025 10:14:18 -1000 Subject: [PATCH 4/4] fix: correct base64 decoding in Solana transaction serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed Transaction.serialize() in both controller and keychain packages to properly decode base64-encoded strings from micro-sol-signer's createTxComplex() function. Previously used hex decoding which resulted in truncated/corrupted transaction data (45 bytes instead of expected 200+), causing "Unexpected end of buffer" errors in Phantom wallet integration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/keychain/src/utils/solana/index.ts | 35 ++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/keychain/src/utils/solana/index.ts b/packages/keychain/src/utils/solana/index.ts index a77ee5fa5a..b1ebd87463 100644 --- a/packages/keychain/src/utils/solana/index.ts +++ b/packages/keychain/src/utils/solana/index.ts @@ -149,15 +149,42 @@ export class Transaction { throw new Error("Transaction requires feePayer and recentBlockhash"); } - // Create the transaction with micro-sol-signer - const txHex = sol.createTxComplex( + console.log( + "[keychain/Transaction.serialize] Building transaction with", + this.instructions.length, + "instructions", + ); + console.log( + "[keychain/Transaction.serialize] feePayer:", + this.feePayer.toString(), + ); + console.log( + "[keychain/Transaction.serialize] recentBlockhash:", + this.recentBlockhash, + ); + + // createTxComplex returns a base64-encoded string, not hex! + const txBase64 = sol.createTxComplex( this.feePayer.toString(), this.instructions, this.recentBlockhash, ); - // Convert hex string to Uint8Array - return new Uint8Array(Buffer.from(txHex, "hex")); + console.log( + "[keychain/Transaction.serialize] createTxComplex returned base64 length:", + txBase64.length, + ); + + // Decode the base64 string to get the transaction bytes + const buffer = new Uint8Array(Buffer.from(txBase64, "base64")); + + console.log("[keychain/Transaction.serialize] Final buffer length:", buffer.length); + console.log( + "[keychain/Transaction.serialize] First 32 bytes:", + Array.from(buffer.slice(0, 32)), + ); + + return buffer; } }