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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions