Module: Solana::Ruby::Kit::TransactionIntrospection

Extended by:
T::Sig
Defined in:
lib/solana/ruby/kit/transaction_introspection/types.rb,
lib/solana/ruby/kit/transaction_introspection/get_instructions.rb,
lib/solana/ruby/kit/transaction_introspection/loaded_addresses.rb,
lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb,
lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb,
lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb,
lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb

Defined Under Namespace

Classes: CompiledInstruction, CompiledTransactionMessage, CompiledTransactionMessageHeader, DecodedRpcTransaction, LoadedAddresses, TracedInstruction

Constant Summary collapse

EMPTY_LOADED_ADDRESSES =
T.let(LoadedAddresses.new(readonly: [], writable: []), LoadedAddresses)

Class Method Summary collapse

Class Method Details

.decode_compiled_transaction_message(message_bytes) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/solana/ruby/kit/transaction_introspection/compiled_transaction_message.rb', line 56

def decode_compiled_transaction_message(message_bytes)
  bytes  = message_bytes.b
  offset = 0

  first_byte = bytes.getbyte(0).to_i
  if (first_byte & 0x80) != 0
    version = first_byte & 0x7f
    unless version == 0
      Kernel.raise SolanaError.new(
        SolanaError::TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
        { unsupported_version: version }
      )
    end
    offset += 1
  else
    version = :legacy
  end

  num_signer_accounts, offset              = read_byte(bytes, offset)
  num_readonly_signer_accounts, offset     = read_byte(bytes, offset)
  num_readonly_non_signer_accounts, offset = read_byte(bytes, offset)

  num_accounts, offset = decode_compact_u16(bytes, offset)
  static_accounts = T.let([], T::Array[Addresses::Address])
  num_accounts.times do
    static_accounts << Addresses.address(Addresses.encode_address(read_bytes(bytes, offset, 32)))
    offset += 32
  end

  lifetime_token = Addresses.encode_address(read_bytes(bytes, offset, 32))
  offset += 32

  num_instructions, offset = decode_compact_u16(bytes, offset)
  instructions = T.let([], T::Array[CompiledInstruction])
  num_instructions.times do
    program_address_index, offset = read_byte(bytes, offset)

    num_ix_accounts, offset = decode_compact_u16(bytes, offset)
     = T.let([], T::Array[Integer])
    num_ix_accounts.times do
      index, offset = read_byte(bytes, offset)
       << index
    end

    data_length, offset = decode_compact_u16(bytes, offset)
    data = read_bytes(bytes, offset, data_length)
    offset += data_length

    instructions << CompiledInstruction.new(
      program_address_index: program_address_index,
      account_indices:       ,
      data:                  data
    )
  end

  CompiledTransactionMessage.new(
    version:         version,
    header:          CompiledTransactionMessageHeader.new(
      num_signer_accounts:              num_signer_accounts,
      num_readonly_signer_accounts:     num_readonly_signer_accounts,
      num_readonly_non_signer_accounts: num_readonly_non_signer_accounts
    ),
    static_accounts: static_accounts,
    instructions:    instructions,
    lifetime_token:  lifetime_token
  )
end

.decode_transaction_from_rpc_response(rpc_tx) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/solana/ruby/kit/transaction_introspection/decode_rpc_transaction.rb', line 59

def decode_transaction_from_rpc_response(rpc_tx)
  tx_field = rpc_tx['transaction']

  if tx_field.is_a?(Array)
    encoding = tx_field[1]
    return decode_from_base64(rpc_tx) if encoding == 'base64'
    return decode_from_base58(rpc_tx) if encoding == 'base58'
  elsif tx_field.is_a?(Hash) && tx_field['message'].is_a?(Hash)
    # An `encoding: 'json'` message carries the compiled-message `header`
    # (signer/readonly counts). A `jsonParsed` message has no `header` —
    # the server has already resolved account roles onto `accountKeys` —
    # so checking for it distinguishes the two encodings.
    return decode_from_json(rpc_tx) if tx_field['message']['header'].is_a?(Hash)

    Kernel.raise SolanaError.new(SolanaError::TRANSACTION_INTROSPECTION__CANNOT_DECODE_JSON_PARSED_TRANSACTION)
  end

  Kernel.raise SolanaError.new(SolanaError::TRANSACTION_INTROSPECTION__UNRECOGNIZED_GET_TRANSACTION_RESPONSE)
end

.get_account_metas_from_compiled_transaction_message(compiled_message, loaded_addresses = nil) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/solana/ruby/kit/transaction_introspection/get_instructions.rb', line 36

def (compiled_message, loaded_addresses = nil)
  header          = compiled_message.header
  static_accounts = compiled_message.static_accounts

  num_writable_signer_accounts =
    header.num_signer_accounts - header.num_readonly_signer_accounts
  num_writable_non_signer_accounts =
    static_accounts.length - header.num_signer_accounts - header.num_readonly_non_signer_accounts

  metas = T.let([], T::Array[Instructions::AccountMeta])
  i = 0
  num_writable_signer_accounts.times do
    metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::WRITABLE_SIGNER)
    i += 1
  end
  header.num_readonly_signer_accounts.times do
    metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::READONLY_SIGNER)
    i += 1
  end
  num_writable_non_signer_accounts.times do
    metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::WRITABLE)
    i += 1
  end
  header.num_readonly_non_signer_accounts.times do
    metas << Instructions::AccountMeta.new(address: T.must(static_accounts[i]), role: Instructions::AccountRole::READONLY)
    i += 1
  end

  if loaded_addresses
    loaded_addresses.writable.each { |addr| metas << Instructions::AccountMeta.new(address: addr, role: Instructions::AccountRole::WRITABLE) }
    loaded_addresses.readonly.each  { |addr| metas << Instructions::AccountMeta.new(address: addr, role: Instructions::AccountRole::READONLY) }
  end

  metas
end

.get_inner_instructions_from_meta(meta, account_metas) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/solana/ruby/kit/transaction_introspection/get_inner_instructions.rb', line 37

def get_inner_instructions_from_meta(meta, )
  groups = meta && meta['innerInstructions']
  return [] unless groups

  result = T.let([], T::Array[TracedInstruction])

  groups.each do |group|
    outer_index = group['index']

    group['instructions'].each_with_index do |ix, inner_index|
      program_meta = [ix['programIdIndex']]
      if program_meta.nil?
        Kernel.raise SolanaError.new(
          SolanaError::TRANSACTIONS__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND,
          { index: ix['programIdIndex'] }
        )
      end

      accounts = (ix['accounts'] || []).map do |i|
         = [i]
        if .nil?
          Kernel.raise SolanaError.new(
            SolanaError::TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE,
            { index: i }
          )
        end
        
      end

      data_b58 = ix['data']
      data     = data_b58.nil? || data_b58.empty? ? nil : Encoding::Base58.decode(data_b58)

      trace = { kind: :inner, outer_index: outer_index, inner_index: inner_index }
      trace[:stack_height] = ix['stackHeight'] unless ix['stackHeight'].nil?

      result << TracedInstruction.new(
        instruction: Instructions::Instruction.new(
          program_address: program_meta.address,
          accounts:        accounts.empty? ? nil : accounts,
          data:            data
        ),
        trace: trace
      )
    end
  end

  result
end

.get_instructions_from_compiled_transaction_message(compiled_message, loaded_addresses = nil) ⇒ Object



84
85
86
87
# File 'lib/solana/ruby/kit/transaction_introspection/get_instructions.rb', line 84

def get_instructions_from_compiled_transaction_message(compiled_message, loaded_addresses = nil)
  metas = (compiled_message, loaded_addresses)
  get_instructions_from_compiled_transaction_message_with_metas(compiled_message, metas)
end

.get_instructions_from_compiled_transaction_message_with_metas(compiled_message, account_metas) ⇒ Object



99
100
101
# File 'lib/solana/ruby/kit/transaction_introspection/get_instructions.rb', line 99

def get_instructions_from_compiled_transaction_message_with_metas(compiled_message, )
  compiled_message.instructions.map { |ix| resolve_instruction(ix, ) }
end

.walk_instructions(compiled_message:, loaded_addresses: nil, meta: nil) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/solana/ruby/kit/transaction_introspection/walk_instructions.rb', line 34

def walk_instructions(compiled_message:, loaded_addresses: nil, meta: nil)
        = (compiled_message, loaded_addresses)
  outer_instructions = get_instructions_from_compiled_transaction_message_with_metas(compiled_message, )

  inner_by_outer_index = T.let({}, T::Hash[Integer, T::Array[TracedInstruction]])
  if meta
    get_inner_instructions_from_meta(meta, ).each do |inner|
      outer_index = inner.trace.fetch(:outer_index)
      (inner_by_outer_index[outer_index] ||= []) << inner
    end
  end

  result = T.let([], T::Array[TracedInstruction])
  outer_instructions.each_with_index do |instruction, index|
    result << TracedInstruction.new(instruction: instruction, trace: { kind: :outer, index: index })
    group = inner_by_outer_index.delete(index)
    result.concat(group) if group
  end
  # Inner groups whose index matches no outer instruction can only come
  # from malformed input (e.g. `meta` paired with the wrong message).
  # Append them rather than dropping them so no instruction is ever lost.
  inner_by_outer_index.each_value { |group| result.concat(group) }

  result
end