2

Problem

  1. Create instruction in backend server and partial sign it with a private key
  2. Serialize the transaction and send it to the Frontend
  3. Frontend deserialize transaction, and signs with the wallet adaptor
  4. Frontend sends and confirms transaction

This is where the problem occurs

signTransaction using the wallet adaptor removes the partialSigned. This is a problem because wallet adaptors can't partial sign, they can only sign, which removes any partial sign done in a backend server.

I had to do the opposite, sign using the wallet adapter, then serialize the transaction send it over to the backend, and then the backend partial signs using a private key and sends the transaction

I can't do it the other way around, ie. i always have to end with partial sign and not sign. But wallet adaptors are not able to partial sign, only sign - which removes any partial signed (from my backend)

Am am I getting something wrong? Or is this intended or a bug?

2
  • Are you using a legacy transaction (Transaction) or a VersionedTransaction (VersionedTransaction)? Reason I ask is because the answer to addressing is different based on this.
    – Ahri
    Commented Jan 25 at 11:53
  • Hi @Ahri I was using Transaction, i was reading the VersionedTransaction and see its a little different (does not replace the signatures) am currently trying to see if that works Commented Jan 26 at 2:32

3 Answers 3

3

For completeness, this is a common issue when using the Transaction class in web3js. It has 2 sign methods, sign and partialSign

If you call sign then it resets the signatures, field, ie removes any previous signers: https://github.com/solana-labs/solana-web3.js/blob/35de86bcc20a751ec4d30bbe0788b402b43f2eb9/packages/library-legacy/src/transaction/legacy.ts#L679-L682

If you call partialSign then it only adds new signatures and doesn't affect existing ones: https://github.com/solana-labs/solana-web3.js/blob/35de86bcc20a751ec4d30bbe0788b402b43f2eb9/packages/library-legacy/src/transaction/legacy.ts#L724-L727

This means that if you get a transaction from an API that has already been signed, you need to use partialSign to add your signature.

Most wallet adapters use partialSign, and wallet-adapter uses it in other places where it adds signers. This is mostly a legacy concern because most wallets now use wallet-standard which doesn't depend on these web3js types. But it does affect code that still uses the Transaction object from web3js.

This was fixed for VersionedTransaction which only has a sign method, which doesn't reset signatures: https://github.com/solana-labs/solana-web3.js/blob/35de86bcc20a751ec4d30bbe0788b402b43f2eb9/packages/library-legacy/src/transaction/versioned.ts#L107

1
  • Thanks for the explanation! Commented Jan 26 at 11:55
2

I was doing your described flow in this example program: https://github.com/solana-developers/one-milion-nfts

So in the backend you partialy sign the transactions with the flags:

verifySignatures: false,
requireAllSignatures: false,

Looking like this:

const transaction = await mintCompressedNft(
    CONNECTION,
    nftArgs,
    ownerWallet.publicKey,
    TreeAccount,
    CollectionMint,
    collectionMetadataAccount, 
    collectionMasterEditionAccount,
    feepayer
  );
  transaction.recentBlockhash = hash.blockhash;

  transaction.partialSign(ownerWallet);
  console.log("transaction " + transaction.instructions.length);
  
  const serializedTransaction = transaction.serialize({
    verifySignatures: false,
    requireAllSignatures: false,
  });

  const base64Transaction = serializedTransaction.toString('base64');

  res.status(200).json({
    transaction: base64Transaction,
    message: "OK",
  });

In the client you can then sign the transaction using the wallet adapter and send it like so:

const jsonResponse = await response.json(); //extract JSON from the http response

const decodedTx = Buffer.from(jsonResponse.transaction, "base64");
const transaction = Transaction.from(decodedTx);

const signature = await sendTransaction(transaction, CONNECTION, {
  skipPreflight: true,
});
2
  • Hey Jonas thanks for the reply, yea that was my flow as well. Only difference was i was not using wallet sendTransaction as was not using react. I was looking at the code from sendTransaction, apparently it uses partialSign if there is > 0 signers. partialSign requires a keypair as input though which is not available to FE wallet adaptor github.com/solana-labs/wallet-adapter/blob/… Line 67: signers?.length && transaction.partialSign(...signers); Commented Jan 26 at 2:42
  • 1
    Hmm my bad actually sendTransaction it only does partialSign if additional signers are passed in the options. Let me try with VersionedTransaction instead Commented Jan 26 at 2:50
1

Ahh silly me, i had a bug that was using the wrong wallet address, Verified that it works for both Transaction (partial sign on backend -> sign on frontend) and VersionTransaction (sign on backend -> sign on backend)

Thanks and will close it.

For others who might be interested, this was the use case. I was trying to pay for the account creation. There is 2 signers, the payer is the fee payer and the signer is the actual owner


#[derive(Accounts)]
pub struct CreateUser<'info> {
    #[account(
        init_if_needed,
        payer = payer, // the payer might not be the signer (pay on behalf of the signer)
        seeds = [b"user", signer.key().as_ref()],
        bump,
        space = User::len(),
    )]
    pub user: Account<'info, User>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

Then creating the instruction here

async createUserIx({
        geohash,
        region,
        wallet,
        payer,
    }: {
        geohash: number[];
        region: number[];
        wallet?: PublicKey | null;
        payer?: PublicKey | null;
    }): Promise<TransactionInstruction> {
        const [pda, _] = this.getUserPda(wallet);

        return await this.program.methods
            .createUser(region, geohash)
            .accounts({
                user: pda,
                signer: wallet,
                payer: payer,
                systemProgram: SystemProgram.programId,
            })
            .instruction();
    }

After than sign -> serializing -> deserialize -> sign -> send works

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.