Class: Runar::SDK::RPCProvider

Inherits:
Provider
  • Object
show all
Defined in:
lib/runar/sdk/rpc_provider.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host: 'localhost', port: 18_332, username: 'bitcoin', password: 'bitcoin', network: 'regtest') ⇒ RPCProvider

Returns a new instance of RPCProvider.

Parameters:

  • host (String) (defaults to: 'localhost')

    Bitcoin node hostname (default: ‘localhost’)

  • port (Integer) (defaults to: 18_332)

    RPC port (default: 18332 for regtest)

  • username (String) (defaults to: 'bitcoin')

    RPC username (default: ‘bitcoin’)

  • password (String) (defaults to: 'bitcoin')

    RPC password (default: ‘bitcoin’)

  • network (String) (defaults to: 'regtest')

    network name returned by #get_network (default: ‘regtest’)



31
32
33
34
35
36
# File 'lib/runar/sdk/rpc_provider.rb', line 31

def initialize(host: 'localhost', port: 18_332, username: 'bitcoin', password: 'bitcoin', network: 'regtest')
  @host     = host
  @port     = port
  @auth     = ["#{username}:#{password}"].pack('m0')
  @network  = network
end

Class Method Details

.regtest(host: 'localhost', port: 18_332, username: 'bitcoin', password: 'bitcoin') ⇒ RPCProvider

Factory method that returns an RPCProvider with default regtest settings.

Parameters:

  • host (String) (defaults to: 'localhost')

    hostname (default: ‘localhost’)

  • port (Integer) (defaults to: 18_332)

    port (default: 18332)

  • username (String) (defaults to: 'bitcoin')

    RPC username (default: ‘bitcoin’)

  • password (String) (defaults to: 'bitcoin')

    RPC password (default: ‘bitcoin’)

Returns:



45
46
47
# File 'lib/runar/sdk/rpc_provider.rb', line 45

def self.regtest(host: 'localhost', port: 18_332, username: 'bitcoin', password: 'bitcoin')
  new(host: host, port: port, username: username, password: password, network: 'regtest')
end

Instance Method Details

#broadcast(raw_tx) ⇒ String

Broadcast a signed raw transaction to the node.

Parameters:

  • raw_tx (String)

    hex-encoded raw transaction

Returns:

  • (String)

    txid



80
81
82
# File 'lib/runar/sdk/rpc_provider.rb', line 80

def broadcast(raw_tx)
  rpc_call('sendrawtransaction', raw_tx).to_s
end

#get_contract_utxo(script_hash) ⇒ Hash?

Script-hash UTXO lookup via scantxoutset (best-effort).

Uses the scantxoutset RPC to scan the UTXO set for outputs matching the given script hash. This is functional for regtest/testnet but slow on mainnet. For production use, consider an electrum-style indexer or track the UTXO manually with RunarContract#from_txid after deployment.

Parameters:

  • script_hash (String)

    hex-encoded script hash to search for

Returns:

  • (Hash, nil)

    UTXO hash with :txid, :output_index, :satoshis, :script keys, or nil



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/runar/sdk/rpc_provider.rb', line 109

def get_contract_utxo(script_hash)
  result = rpc_call('scantxoutset', 'start', ["raw(#{script_hash})"])
  unspents = Array(result['unspents'])
  return nil if unspents.empty?

  u = unspents.first
  {
    txid: u['txid'],
    output_index: u['vout'].to_i,
    satoshis: (u['amount'].to_f * 1e8).round,
    script: u['scriptPubKey']
  }
rescue StandardError
  raise NotImplementedError,
        'RPCProvider#get_contract_utxo: scantxoutset RPC failed. ' \
        'Your node may not support this command. Alternatives: ' \
        'use an electrum-style indexer, or track the UTXO manually with ' \
        'RunarContract#from_txid after deployment.'
end

#get_fee_rateInteger

Return fee rate in satoshis per kilobyte.

Returns 1 sat/KB unconditionally — appropriate for regtest. For production networks, consider wrapping estimatesmartfee.

Returns:

  • (Integer)


142
143
144
# File 'lib/runar/sdk/rpc_provider.rb', line 142

def get_fee_rate
  1
end

#get_networkString

Return the network name this provider is connected to.

Returns:

  • (String)

    e.g. ‘regtest’, ‘testnet’, ‘mainnet’



132
133
134
# File 'lib/runar/sdk/rpc_provider.rb', line 132

def get_network
  @network
end

#get_raw_transaction(txid) ⇒ String

Fetch the raw transaction hex by txid.

Parameters:

  • txid (String)

Returns:

  • (String)

    hex-encoded raw transaction



71
72
73
74
# File 'lib/runar/sdk/rpc_provider.rb', line 71

def get_raw_transaction(txid)
  result = rpc_call('getrawtransaction', txid, false)
  result.to_s
end

#get_transaction(txid) ⇒ Transaction

Fetch a Transaction by txid using getrawtransaction (verbose).

Parameters:

  • txid (String)

    transaction id

Returns:



53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/runar/sdk/rpc_provider.rb', line 53

def get_transaction(txid)
  raw = rpc_call('getrawtransaction', txid, true)
  raw_hex = raw.fetch('hex', '')

  outputs = Array(raw['vout']).map do |o|
    val_btc = o.fetch('value', 0.0)
    sats    = (val_btc * 1e8).round
    sp      = o.fetch('scriptPubKey', {})
    TxOutput.new(script: sp.fetch('hex', ''), satoshis: sats)
  end

  Transaction.new(txid: txid, version: 1, outputs: outputs, raw: raw_hex)
end

#get_utxos(address) ⇒ Array<Utxo>

Return all UTXOs for the given address using listunspent.

Parameters:

  • address (String)

    BSV address

Returns:



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/runar/sdk/rpc_provider.rb', line 88

def get_utxos(address)
  result = Array(rpc_call('listunspent', 0, 9_999_999, [address]))
  result.map do |u|
    Utxo.new(
      txid: u['txid'],
      output_index: u['vout'].to_i,
      satoshis: (u['amount'].to_f * 1e8).round,
      script: u.fetch('scriptPubKey', '')
    )
  end
end

#mine(n_blocks = 1) ⇒ Array<String>

Generate n_blocks blocks (regtest only).

Uses generatetoaddress with a dummy bech32 address. This will advance the chain and confirm any transactions in the mempool.

Parameters:

  • n_blocks (Integer) (defaults to: 1)

    number of blocks to mine (default: 1)

Returns:

  • (Array<String>)

    block hashes



153
154
155
156
157
# File 'lib/runar/sdk/rpc_provider.rb', line 153

def mine(n_blocks = 1)
  # A standard regtest coinbase address for the dummy recipient.
  dummy_address = 'bcrt1qjrdns4f5zwkv29ln86plqzs092yd5fg6nsz8re'
  rpc_call('generatetoaddress', n_blocks, dummy_address)
end

#rpc_call(method, *params) ⇒ Object

Low-level JSON-RPC call.

Parameters:

  • method (String)

    RPC method name

  • params (Array)

    RPC parameters (splat)

Returns:

  • (Object)

    the result field of the JSON-RPC response

Raises:

  • (RuntimeError)

    on RPC error or HTTP failure



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/runar/sdk/rpc_provider.rb', line 165

def rpc_call(method, *params)
  body = JSON.generate(
    jsonrpc: '1.0',
    id: 'runar',
    method: method,
    params: params
  )

  uri = URI::HTTP.build(host: @host, port: @port, path: '/')
  request = Net::HTTP::Post.new(uri)
  request['Content-Type']  = 'application/json'
  request['Authorization'] = "Basic #{@auth}"
  request.body = body

  response = Net::HTTP.start(@host, @port, read_timeout: 600) do |http|
    http.request(request)
  end

  parse_rpc_response(response, method)
end