Skip to content

order_and_execute() TypeError on transactions with multiple signatures #3

@moratb

Description

@moratb

The Bug

UltraApiClient.order_and_execute() fails on transactions with multiple signature slots (common with larger swap amounts).

Error:

TypeError: argument 'keypairs': failed to extract enum Signer
'Signature' object cannot be converted to 'Keypair'

Root Cause

Bug in JupiterClient._sign_versioned_transaction() (lines 87-99):

signers = list(versioned_transaction.signatures)  # List of Signature objects
signers[wallet_index] = wallet  # ❌ Puts Keypair into Signature list - TYPE MISMATCH

return VersionedTransaction(versioned_transaction.message, signers)  # Fails!

The issue: The code replaces a Signature with a Keypair object, creating a mixed-type list. This works for single-signature transactions but fails for multi-signature transactions.

Reproduce

from jup_python_sdk.clients.ultra_api_client import UltraApiClient
from jup_python_sdk.models.ultra_api.ultra_order_request_model import UltraOrderRequest

client = UltraApiClient(private_key_env_var="PRIVATE_KEY")

order_request = UltraOrderRequest(
    input_mint="85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ",
    output_mint="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    amount=100000000,  # Large amounts trigger multi-signature transactions
    taker=client._get_public_key()
)

client.order_and_execute(order_request)  # ❌ TypeError

Fix

Replace _sign_versioned_transaction() in jupiter_client.py:

def _sign_versioned_transaction(
    self, versioned_transaction: VersionedTransaction
) -> VersionedTransaction:
    wallet = Keypair.from_bytes(self._load_private_key_bytes())
    
    # Actually sign the message to create a Signature object
    my_signature = wallet.sign_message(bytes(versioned_transaction.message))
    
    account_keys = versioned_transaction.message.account_keys
    wallet_index = account_keys.index(wallet.pubkey())
    
    # Replace with Signature (not Keypair)
    signatures = list(versioned_transaction.signatures)
    signatures[wallet_index] = my_signature
    
    # Use populate() method with all Signature objects
    return VersionedTransaction.populate(
        versioned_transaction.message,
        signatures
    )

Key change: Sign the message to create a Signature object, then use .populate() instead of the constructor. This maintains type consistency: all elements remain Signature objects.

Workaround

Use separate order() and execute() calls with manual signing

    order_request = UltraOrderRequest(
        input_mint=in_address,
        output_mint=out_address,
        amount=in_amount,
        taker=self.address
    )
    
    order_response = self.client.order(order_request)
    request_id = order_response["requestId"]
    
    ## manual signing of the transaction
    tx_bytes = base64.b64decode(order_response["transaction"])
    raw_tx = VersionedTransaction.from_bytes(tx_bytes)
    wallet = Keypair.from_bytes(self.client._load_private_key_bytes())
    my_signature = wallet.sign_message(message.to_bytes_versioned(raw_tx.message))
    wallet_index = raw_tx.message.account_keys.index(wallet.pubkey())
    signatures = list(raw_tx.signatures)
    signatures[wallet_index] = my_signature
    signed_tx = VersionedTransaction.populate(raw_tx.message, signatures)
    signed_transaction = base64.b64encode(bytes(signed_tx)).decode("utf-8")
    
    execute_request = UltraExecuteRequest(
        request_id=request_id,
        signed_transaction=signed_transaction
    )
    
    client_response = self.client.execute(execute_request)
    tx_signature = str(client_response["signature"])

Environment

  • Python: 3.13.5
  • jup-python-sdk: 1.1.0
  • solders: 0.26.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions