X402.Solana.Transaction (X402 v0.6.0)

Copy Markdown View Source

Solana v0 transaction message building, serialization, and decoding.

Implements exactly what the exact SVM scheme needs, with no RPC and no new dependencies:

  • Instruction constructors for the reference client's instruction set — Compute Budget SetComputeUnitLimit/SetComputeUnitPrice, SPL Token / Token-2022 TransferChecked, and SPL Memo
  • compile/3 — compiles instructions into a serialized version 0 message (the version the reference TypeScript and Python clients produce), including compact-u16 (shortvec) encoding, account deduplication, and the account ordering used by @solana/kit, so the output is byte-identical to the reference client
  • serialize/2 — the wire transaction: compact-u16 signature count followed by 64-byte signature slots (missing signatures are all-zero placeholders, which is how a partially signed transaction represents the fee payer's pending signature) and the message bytes
  • decode/1 — parses a wire transaction (v0 or legacy) back into its parts for structural validation

Account ordering

Static accounts are ordered: fee payer first, then writable signers, read-only signers, writable non-signers, read-only non-signers. Within a group, addresses sort with @solana/kit's comparator (case-insensitive primary pass, lowercase-first tiebreak) so compiled messages are byte-identical to the reference client's.

Signing

Ed25519 signatures cover the entire serialized message returned by compile/3, including the leading 0x80 version byte.

Summary

Types

An account referenced by an instruction.

A compiled v0 message ready to sign.

A decoded wire transaction.

An instruction to compile into a message.

Functions

Splices a 64-byte Ed25519 signature into a decoded transaction's slot.

Compiles instructions into a serialized v0 message.

Decodes a wire transaction (v0 or legacy) into its parts.

Decodes a compact-u16 prefix, returning the value and the rest.

Encodes a non-negative integer as compact-u16 (shortvec).

The Solana network's maximum serialized transaction size in bytes.

An SPL Memo instruction carrying UTF-8 data (no accounts).

Serializes a wire transaction from message bytes and signatures.

The Compute Budget SetComputeUnitLimit instruction (discriminator 2, u32 little-endian units).

The Compute Budget SetComputeUnitPrice instruction (discriminator 3, u64 little-endian microlamports).

The SPL Token / Token-2022 TransferChecked instruction.

Types

account_meta()

@type account_meta() :: %{
  address: X402.Solana.address(),
  signer?: boolean(),
  writable?: boolean()
}

An account referenced by an instruction.

compiled()

@type compiled() :: %{bytes: binary(), signers: [X402.Solana.address()]}

A compiled v0 message ready to sign.

decoded()

@type decoded() :: %{
  version: 0 | :legacy,
  num_required_signatures: non_neg_integer(),
  num_readonly_signed: non_neg_integer(),
  num_readonly_unsigned: non_neg_integer(),
  signatures: [binary()],
  static_accounts: [X402.Solana.pubkey()],
  recent_blockhash: X402.Solana.pubkey(),
  instructions: [
    %{program_index: byte(), account_indices: [byte()], data: binary()}
  ],
  address_table_lookups: non_neg_integer(),
  message_bytes: binary()
}

A decoded wire transaction.

instruction()

@type instruction() :: %{
  program: X402.Solana.address(),
  accounts: [account_meta()],
  data: binary()
}

An instruction to compile into a message.

Functions

attach_signature(map, index, signature)

(since 0.6.0)
@spec attach_signature(decoded(), non_neg_integer(), binary()) ::
  {:ok, binary()} | {:error, :invalid_signature | :invalid_slot}

Splices a 64-byte Ed25519 signature into a decoded transaction's slot.

Rebuilds the wire transaction from a decode/1 result — compact-u16 signature count, the signature slots with signature at index, then the message bytes — preserving every other existing signature. This is how a facilitator fills the fee payer's empty slot 0 at settlement without disturbing the payer's signature.

Unlike serialize/2 (which zero-fills missing signatures by design, the partially-signed representation), a malformed signature here returns {:error, :invalid_signature}: silently broadcasting a zeroed fee-payer slot would only fail later on chain. An out-of-range index returns {:error, :invalid_slot}.

Examples

iex> decoded = %{
...>   num_required_signatures: 2,
...>   signatures: [<<0::512>>, <<1::512>>],
...>   message_bytes: <<0x80, 2, 1, 4>>
...> }
iex> {:ok, wire} = X402.Solana.Transaction.attach_signature(decoded, 0, <<9::512>>)
iex> wire == <<2>> <> <<9::512>> <> <<1::512>> <> <<0x80, 2, 1, 4>>
true

iex> X402.Solana.Transaction.attach_signature(
...>   %{num_required_signatures: 1, signatures: [<<0::512>>], message_bytes: <<0x80>>},
...>   0,
...>   <<1, 2, 3>>
...> )
{:error, :invalid_signature}

compile(fee_payer, instructions, recent_blockhash)

(since 0.6.0)
@spec compile(X402.Solana.address(), [instruction()], X402.Solana.address()) ::
  {:ok, compiled()} | {:error, :invalid_address}

Compiles instructions into a serialized v0 message.

Returns the message bytes (starting with the 0x80 version prefix — these are the bytes Ed25519 signatures cover) and the required signer addresses in signature-slot order (the fee payer is always first).

Returns {:error, :invalid_address} when any address fails Base58 decoding.

decode(wire)

(since 0.6.0)
@spec decode(binary()) :: {:ok, decoded()} | {:error, :invalid_transaction}

Decodes a wire transaction (v0 or legacy) into its parts.

Used by the server-side structural checks in X402.Scheme.ExactSVM. Rejects trailing bytes and truncated sections with {:error, :invalid_transaction}.

decode_compact_u16(binary)

(since 0.6.0)
@spec decode_compact_u16(binary()) :: {:ok, non_neg_integer(), binary()} | :error

Decodes a compact-u16 prefix, returning the value and the rest.

Examples

iex> X402.Solana.Transaction.decode_compact_u16(<<0x80, 0x01, "rest">>)
{:ok, 128, "rest"}

iex> X402.Solana.Transaction.decode_compact_u16(<<0xFF, 0x7F>>)
{:ok, 16_383, ""}

iex> X402.Solana.Transaction.decode_compact_u16(<<0x80>>)
:error

encode_compact_u16(value)

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

Encodes a non-negative integer as compact-u16 (shortvec).

Little-endian 7-bit groups with a continuation bit, as used for all counts in Solana's wire format.

Examples

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

iex> X402.Solana.Transaction.encode_compact_u16(127)
<<0x7F>>

iex> X402.Solana.Transaction.encode_compact_u16(128)
<<0x80, 0x01>>

iex> X402.Solana.Transaction.encode_compact_u16(16_383)
<<0xFF, 0x7F>>

iex> X402.Solana.Transaction.encode_compact_u16(16_384)
<<0x80, 0x80, 0x01>>

max_transaction_size()

(since 0.6.0)
@spec max_transaction_size() :: pos_integer()

The Solana network's maximum serialized transaction size in bytes.

Examples

iex> X402.Solana.Transaction.max_transaction_size()
1232

memo(data)

(since 0.6.0)
@spec memo(binary()) :: instruction()

An SPL Memo instruction carrying UTF-8 data (no accounts).

Examples

iex> ix = X402.Solana.Transaction.memo("pi_3abc123def456")
iex> {ix.program, ix.data}
{"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", "pi_3abc123def456"}

serialize(map, signatures)

(since 0.6.0)
@spec serialize(compiled(), %{required(X402.Solana.address()) => binary()}) ::
  binary()

Serializes a wire transaction from message bytes and signatures.

signatures maps signer addresses to 64-byte Ed25519 signatures; signers without an entry get a 64-byte zero placeholder — how a partially signed transaction leaves the fee payer's slot empty for the facilitator to fill at settlement.

set_compute_unit_limit(units)

(since 0.6.0)
@spec set_compute_unit_limit(non_neg_integer()) :: instruction()

The Compute Budget SetComputeUnitLimit instruction (discriminator 2, u32 little-endian units).

Examples

iex> ix = X402.Solana.Transaction.set_compute_unit_limit(20_000)
iex> {ix.program, ix.accounts, ix.data}
{"ComputeBudget111111111111111111111111111111", [], <<2, 32, 78, 0, 0>>}

set_compute_unit_price(micro_lamports)

(since 0.6.0)
@spec set_compute_unit_price(non_neg_integer()) :: instruction()

The Compute Budget SetComputeUnitPrice instruction (discriminator 3, u64 little-endian microlamports).

Examples

iex> ix = X402.Solana.Transaction.set_compute_unit_price(1)
iex> ix.data
<<3, 1, 0, 0, 0, 0, 0, 0, 0>>

transfer_checked(map)

(since 0.6.0)
@spec transfer_checked(%{
  source: X402.Solana.address(),
  mint: X402.Solana.address(),
  destination: X402.Solana.address(),
  authority: X402.Solana.address(),
  amount: non_neg_integer(),
  decimals: byte(),
  token_program: X402.Solana.address()
}) :: instruction()

The SPL Token / Token-2022 TransferChecked instruction.

Discriminator 12, u64 little-endian amount, u8 decimals; accounts [source (writable), mint, destination (writable), authority (signer)] — the layout the facilitator's static verification path parses.