Search documentation of Cartouche

# Cartouche.Solana.Transaction (Cartouche v0.1.2)

Copy Markdown View Source

Build, serialize, sign, and deserialize Solana transactions (legacy format).

A Solana transaction consists of signatures and a message. The message contains a header, ordered account keys, a recent blockhash, and compiled instructions. Each signer signs the raw serialized message bytes.

## Example: Build and sign a SOL transfer

    fee_payer = <<...>>  # 32-byte pubkey
    recipient = <<...>>  # 32-byte pubkey
    blockhash = <<...>>  # 32 bytes from getLatestBlockhash

    instruction = Cartouche.Solana.SystemProgram.transfer(fee_payer, recipient, 1_000_000_000)

    message = Cartouche.Solana.Transaction.build_message(fee_payer, [instruction], blockhash)
    transaction = Cartouche.Solana.Transaction.sign(message, [fee_payer_seed])

    # Serialize for RPC submission
    bytes = Cartouche.Solana.Transaction.serialize(transaction)

## Summary

### Types

t()

### Functions

add_signature(transaction, index, arg)

Add a signature to a transaction at a specific signer position.

build_message(arg1, instructions, arg2)

Build a compiled message from high-level instructions.

decode_compact_u16(binary)

Decode a compact-u16 from the beginning of a binary.

deserialize(binary)

Deserialize a legacy transaction from binary.

deserialize_message(arg1)

Deserialize a message from binary.

encode_compact_u16(value)

Encode a non-negative integer as a compact-u16 (variable-length).

serialize(transaction)

Serialize a full transaction (signatures + message) for RPC submission.

serialize_message(msg)

Serialize a message to the bytes that get signed.

sign(message, seeds)

Sign a message with one or more seeds and produce a full transaction.

sign_partial(message, signers)

Partially sign a message, filling only the specified signer positions.

## Types

# t()

    @type t() :: %Cartouche.Solana.Transaction{
      message: Cartouche.Solana.Transaction.Message.t(),
      signatures: [<<_::512>>]
    }

## Functions

# add_signature(transaction, index, arg)

    @spec add_signature(t(), non_neg_integer(), <<_::512>>) :: t()

Add a signature to a transaction at a specific signer position.

Used to fill in a missing signature on a partially-signed transaction, typically by a sponsor or co-signer who receives the transaction from another party. See `sign_partial/2` for the full sponsored transaction flow.

The `index` is the position in the signatures array (matching the account keys order in the message). The existing signature at that position is replaced.

## Examples

    # Sponsor receives a partially-signed transaction and adds their signature
    {:ok, partial} = Transaction.deserialize(bytes_from_user)
    msg_bytes = Transaction.serialize_message(partial.message)
    sponsor_sig = :crypto.sign(:eddsa, :none, msg_bytes, [sponsor_seed, :ed25519])
    full_trx = Transaction.add_signature(partial, 0, sponsor_sig)

# build_message(arg1, instructions, arg2)

    @spec build_message(
      <<_::256>>,
      [Cartouche.Solana.Transaction.Instruction.t()],
      <<_::256>>
    ) ::
      Cartouche.Solana.Transaction.Message.t()

Build a compiled message from high-level instructions.

Handles account deduplication, permission merging, ordering, and index compilation. The fee payer is always placed first as a writable signer.

# decode_compact_u16(binary)

    @spec decode_compact_u16(binary()) :: {non_neg_integer(), binary()}

Decode a compact-u16 from the beginning of a binary.

Returns `{value, rest}`. Raises `FunctionClauseError` on empty or truncated input — internal callers that need an error tuple use `safe_decode_compact_u16/1`.

## Examples

    iex> Cartouche.Solana.Transaction.decode_compact_u16(<<0, 99>>)
    {0, <<99>>}

    iex> Cartouche.Solana.Transaction.decode_compact_u16(<<128, 1, 99>>)
    {128, <<99>>}

# deserialize(binary)

    @spec deserialize(binary()) :: {:ok, t()} | {:error, term()}

Deserialize a legacy transaction from binary.

Returns `{:ok, t()}` on a complete, well-formed transaction; `{:error, atom()}` on malformed input. Possible error atoms:

- `:truncated_compact_u16` — compact-u16 prefix ends mid-byte
- `:insufficient_signature_data` — signature-count exceeds remaining bytes
- `:insufficient_pubkey_data` — pubkey-count exceeds remaining bytes
- `:insufficient_instruction_data` — instruction-count, account-list, or data-payload exceeds remaining bytes
- `:invalid_message_header` — fewer than 3 header bytes
- `:invalid_message_body` — blockhash truncated or other structural mismatch the inner clauses didn't tag
- `:invalid_transaction` — message parsed but trailing bytes remain

# deserialize_message(arg1)

    @spec deserialize_message(binary()) ::
      {:ok, Cartouche.Solana.Transaction.Message.t(), binary()} | {:error, term()}

Deserialize a message from binary.

Returns `{:ok, Message.t(), rest :: binary()}` on success — `rest` is whatever bytes follow the message (callers like `deserialize/1` enforce `rest == <<>>`).

Returns `{:error, :invalid_message_header}` when fewer than 3 header bytes are present. Specific atoms surface from inner parse clauses (`:truncated_compact_u16`, `:insufficient_pubkey_data`, `:insufficient_instruction_data`); `{:error, :invalid_message_body}` is the catch-all for structural mismatches the inner clauses didn't tag (notably a truncated blockhash).

# encode_compact_u16(value)

    @spec encode_compact_u16(non_neg_integer()) :: binary()

Encode a non-negative integer as a compact-u16 (variable-length).

## Examples

    iex> Cartouche.Solana.Transaction.encode_compact_u16(0)
    <<0>>

    iex> Cartouche.Solana.Transaction.encode_compact_u16(127)
    <<127>>

    iex> Cartouche.Solana.Transaction.encode_compact_u16(128)
    <<128, 1>>

    iex> Cartouche.Solana.Transaction.encode_compact_u16(16384)
    <<128, 128, 1>>

# serialize(transaction)

    @spec serialize(t()) :: binary()

Serialize a full transaction (signatures + message) for RPC submission.

# serialize_message(msg)

    @spec serialize_message(Cartouche.Solana.Transaction.Message.t()) :: binary()

Serialize a message to the bytes that get signed.

# sign(message, seeds)

    @spec sign(Cartouche.Solana.Transaction.Message.t(), [<<_::256>>]) :: t()

Sign a message with one or more seeds and produce a full transaction.

Seeds must be ordered to match the signer positions in the message's account keys (i.e., the first `num_required_signatures` accounts).

# sign_partial(message, signers)

    @spec sign_partial(Cartouche.Solana.Transaction.Message.t(), %{
      required(non_neg_integer()) => <<_::256>>
    }) :: t()

Partially sign a message, filling only the specified signer positions.

This is the core primitive for sponsored transactions (where one party pays fees on behalf of another). The typical flow is:

1. User builds a message with the sponsor's pubkey as the fee payer
2. User calls `sign_partial/2` with their own seed to sign their position
3. User serializes the partially-signed transaction and sends it to the sponsor
4. Sponsor deserializes and calls `add_signature/3` to fill in their position
5. Sponsor submits the fully-signed transaction via `Cartouche.Solana.RPC.send_transaction/2`

`signers` is a map of `%{account_index => seed}` where `account_index` is the position of the signer in the message's account keys list (0-based). Positions not present in the map get zero-filled placeholder signatures.

## Examples

    # User is account[1], sponsor is account[0] (fee payer)
    partial = Transaction.sign_partial(message, %{1 => user_seed})
    # => %Transaction{signatures: [<<0::512>>, <user_sig>], ...}

    # Serialize and send to sponsor
    bytes = Transaction.serialize(partial)