Module: Runar::SDK
- Extended by:
- State
- Defined in:
- lib/runar/sdk/state.rb,
lib/runar/sdk/types.rb,
lib/runar/sdk/signer.rb,
lib/runar/sdk/calling.rb,
lib/runar/sdk/codegen.rb,
lib/runar/sdk/contract.rb,
lib/runar/sdk/oppushtx.rb,
lib/runar/sdk/provider.rb,
lib/runar/sdk/deployment.rb,
lib/runar/sdk/local_signer.rb,
lib/runar/sdk/rpc_provider.rb,
lib/runar/sdk/anf_interpreter.rb
Defined Under Namespace
Modules: ANFInterpreter, Codegen, State Classes: ABI, ABIMethod, ABIParam, CallOptions, ConstructorSlot, DeployOptions, ExternalSigner, LocalSigner, MockProvider, MockSigner, OutputSpec, PreparedCall, Provider, RPCProvider, RunarArtifact, RunarContract, Signer, StateField, TerminalOutput, TransactionData, TxInput, TxOutput, Utxo
Constant Summary collapse
- Transaction =
Backward-compatibility alias — existing code using Transaction continues to work.
TransactionData- SIGHASH_ALL_FORKID =
0x41
Constants included from State
Class Method Summary collapse
-
.address_to_pubkey_hash(address) ⇒ String
Extract the 20-byte pubkey hash from a Base58Check P2PKH address.
- .base58_decode(encoded) ⇒ Object
-
.build_call_transaction(current_utxo, unlocking_script, new_locking_script, new_satoshis, change_address, change_script = '', additional_utxos = nil, fee_rate: 100, options: nil) ⇒ Array(String, Integer, Integer)
Build a raw (partially-signed) transaction that spends a contract UTXO.
-
.build_deploy_transaction(locking_script, utxos, satoshis, change_address, change_script = '', fee_rate: 100) ⇒ Array(String, Integer)
Build an unsigned deployment transaction.
-
.build_p2pkh_script(address) ⇒ String
Build a standard P2PKH locking script.
-
.compute_op_push_tx(tx_hex, input_index, subscript_hex, satoshis, code_separator_index = -1)) ⇒ Array(String, String)
Compute the OP_PUSH_TX DER signature and BIP-143 preimage for a transaction input.
-
.compute_preimage(tx_hex, input_index, subscript_hex, satoshis, sighash_type = SIGHASH_ALL_FORKID) ⇒ String
Compute the BIP-143 sighash preimage for a transaction input.
-
.double_sha256(hex) ⇒ String
Double-SHA256 hash (SHA256(SHA256(data))).
-
.encode_varint(n) ⇒ String
Encode an integer as a Bitcoin-style varint (hex string).
-
.estimate_deploy_fee(num_inputs, locking_script_byte_len, fee_rate = 100) ⇒ Integer
Estimate the fee for a deploy transaction.
-
.get_subscript(script_hex, code_separator_index) ⇒ String
Extract the subscript for BIP-143 sighash computation.
-
.hex_string?(str) ⇒ Boolean
Return true if
stris a valid hexadecimal string. -
.insert_unlocking_script(tx_hex, input_index, unlock_script) ⇒ String
Replace the scriptSig of a specific input within a raw transaction.
-
.read_varint_hex(hex_str, pos) ⇒ Array(Integer, Integer)
Read a Bitcoin-style varint from a hex string at position
pos. -
.reverse_hex(hex_str) ⇒ Object
Reverse the byte order of a hex string (converts txid to wire format).
-
.select_utxos(utxos, target_satoshis, locking_script_byte_len, fee_rate: 100) ⇒ Array<Utxo>
Select the minimum set of UTXOs needed to fund a deployment using a largest-first strategy.
-
.sign_preimage_k1(preimage_hex) ⇒ String
Compute the OP_PUSH_TX DER signature for a preimage.
-
.to_le32(n) ⇒ Object
Encode a 32-bit unsigned integer as 4 little-endian bytes (hex string).
-
.to_le64(n) ⇒ Object
Encode a 64-bit unsigned integer as 8 little-endian bytes (hex string).
-
.varint_byte_size(n) ⇒ Integer
Return the byte size of a varint encoding for
n.
Methods included from State
deserialize_state, encode_push_data, encode_script_int, extract_state_from_script, find_last_op_return, serialize_state
Class Method Details
.address_to_pubkey_hash(address) ⇒ String
Extract the 20-byte pubkey hash from a Base58Check P2PKH address.
214 215 216 217 218 219 |
# File 'lib/runar/sdk/deployment.rb', line 214 def address_to_pubkey_hash(address) decoded = base58_decode(address) raise ArgumentError, "invalid address length: #{decoded.bytesize}" unless decoded.bytesize == 25 decoded[1, 20].unpack1('H*') end |
.base58_decode(encoded) ⇒ Object
197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/runar/sdk/deployment.rb', line 197 def base58_decode(encoded) num = 0 encoded.each_char { |c| num = (num * 58) + BASE58_ALPHABET.index(c) } result = [] while num > 0 num, rem = num.divmod(256) result.unshift(rem) end # Leading '1' characters map to zero bytes. pad = encoded.chars.take_while { |c| c == '1' }.length ([0] * pad + result).pack('C*') end |
.build_call_transaction(current_utxo, unlocking_script, new_locking_script, new_satoshis, change_address, change_script = '', additional_utxos = nil, fee_rate: 100, options: nil) ⇒ Array(String, Integer, Integer)
Build a raw (partially-signed) transaction that spends a contract UTXO.
Input 0 is the contract’s current UTXO, carrying the provided unlocking_script (which may be empty when building before signing). Additional contract inputs from options[:additional_contract_inputs] are appended next, each with their own unlocking script. P2PKH funding inputs from additional_utxos follow with empty scriptSigs.
Output 0 is the new contract continuation output. When options[:contract_outputs] is present it replaces the single continuation output with multiple outputs (token-split pattern). A P2PKH change output is appended when the remaining balance is positive.
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 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 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/runar/sdk/calling.rb', line 37 def build_call_transaction( current_utxo, unlocking_script, new_locking_script, new_satoshis, change_address, change_script = '', additional_utxos = nil, fee_rate: 100, options: nil ) opts = || {} extra_contract_inputs = opts[:additional_contract_inputs] || [] additional = additional_utxos || [] all_utxos = [current_utxo] + extra_contract_inputs.map { |ci| ci[:utxo] } + additional total_input = all_utxos.sum(&:satoshis) # Resolve contract outputs — multi-output takes priority over single. resolved_outputs = if opts[:contract_outputs] && !opts[:contract_outputs].empty? opts[:contract_outputs] elsif !new_locking_script.empty? sats = new_satoshis.positive? ? new_satoshis : current_utxo.satoshis [{ script: new_locking_script, satoshis: sats }] else [] end contract_output_sats = resolved_outputs.sum { |co| co[:satoshis] } # Estimate transaction size for fee calculation. # # Input 0: prevTxid(32) + prevIndex(4) + scriptSig varint + scriptSig + sequence(4) unlock_byte_len = unlocking_script.length / 2 input0_size = 32 + 4 + varint_byte_size(unlock_byte_len) + unlock_byte_len + 4 extra_inputs_size = extra_contract_inputs.sum do |ci| ci_len = ci[:unlocking_script].length / 2 32 + 4 + varint_byte_size(ci_len) + ci_len + 4 end # P2PKH funding inputs are unsigned at construction time (~148 bytes each). additional_inputs_size = additional.length * P2PKH_INPUT_SIZE inputs_size = input0_size + extra_inputs_size + additional_inputs_size outputs_size = resolved_outputs.sum do |co| s_len = co[:script].length / 2 8 + varint_byte_size(s_len) + s_len end # Include change output in size estimate only when a recipient is specified. has_change_recipient = !change_address.to_s.empty? || !change_script.to_s.empty? outputs_size += P2PKH_OUTPUT_SIZE if has_change_recipient estimated_size = TX_OVERHEAD + inputs_size + outputs_size rate = [1, fee_rate].max fee = (estimated_size * rate + 999) / 1000 change = total_input - contract_output_sats - fee # Build raw transaction bytes as a hex string. tx = +'' # Version tx << to_le32(1) # Input count tx << encode_varint(all_utxos.length) # Input 0: contract UTXO with (possibly empty) unlocking script. tx << reverse_hex(current_utxo.txid) tx << to_le32(current_utxo.output_index) tx << encode_varint(unlock_byte_len) tx << unlocking_script tx << 'ffffffff' # Additional contract inputs with their own unlocking scripts. extra_contract_inputs.each do |ci| ci_utxo = ci[:utxo] ci_script = ci[:unlocking_script] tx << reverse_hex(ci_utxo.txid) tx << to_le32(ci_utxo.output_index) tx << encode_varint(ci_script.length / 2) tx << ci_script tx << 'ffffffff' end # P2PKH funding inputs — unsigned, empty scriptSig. additional.each do |utxo| tx << reverse_hex(utxo.txid) tx << to_le32(utxo.output_index) tx << '00' tx << 'ffffffff' end # Output count has_change = change.positive? && has_change_recipient output_count = resolved_outputs.length + (has_change ? 1 : 0) tx << encode_varint(output_count) # Contract continuation outputs. resolved_outputs.each do |co| s = co[:script] tx << to_le64(co[:satoshis]) tx << encode_varint(s.length / 2) tx << s end # Change output. if has_change actual_change_script = change_script.to_s.empty? ? build_p2pkh_script(change_address) : change_script tx << to_le64(change) tx << encode_varint(actual_change_script.length / 2) tx << actual_change_script end # Locktime tx << to_le32(0) [tx, all_utxos.length, has_change ? change : 0] end |
.build_deploy_transaction(locking_script, utxos, satoshis, change_address, change_script = '', fee_rate: 100) ⇒ Array(String, Integer)
Build an unsigned deployment transaction.
26 27 28 29 30 31 32 33 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/runar/sdk/deployment.rb', line 26 def build_deploy_transaction(locking_script, utxos, satoshis, change_address, change_script = '', fee_rate: 100) raise ArgumentError, 'build_deploy_transaction: no UTXOs provided' if utxos.empty? total_input = utxos.sum(&:satoshis) fee = estimate_deploy_fee(utxos.length, locking_script.length / 2, fee_rate) change = total_input - satoshis - fee if change < 0 raise ArgumentError, "build_deploy_transaction: insufficient funds. " \ "Need #{satoshis + fee} sats, have #{total_input}" end tx = +'' # Version (4 bytes, little-endian) tx << to_le32(1) # Input count (varint) tx << encode_varint(utxos.length) # Inputs (unsigned — empty scriptSig) utxos.each do |utxo| tx << reverse_hex(utxo.txid) tx << to_le32(utxo.output_index) tx << '00' # empty scriptSig (varint 0) tx << 'ffffffff' # sequence end # Output count (varint) has_change = change > 0 output_count = has_change ? 2 : 1 tx << encode_varint(output_count) # Output 0: contract locking script tx << to_le64(satoshis) tx << encode_varint(locking_script.length / 2) tx << locking_script # Output 1: change (omitted when change is zero) if has_change actual_change_script = change_script.empty? ? build_p2pkh_script(change_address) : change_script tx << to_le64(change) tx << encode_varint(actual_change_script.length / 2) tx << actual_change_script end # Locktime (4 bytes, little-endian) tx << to_le32(0) [tx, utxos.length] end |
.build_p2pkh_script(address) ⇒ String
Build a standard P2PKH locking script.
Accepts either a 40-character hex pubkey hash or a Base58Check P2PKH address. Returns the script as hex: 76a914{20-byte-hash}88ac.
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/runar/sdk/deployment.rb', line 131 def build_p2pkh_script(address) pub_key_hash = if address.length == 40 && hex_string?(address) address else address_to_pubkey_hash(address) end "76a914#{pub_key_hash}88ac" end |
.compute_op_push_tx(tx_hex, input_index, subscript_hex, satoshis, code_separator_index = -1)) ⇒ Array(String, String)
Compute the OP_PUSH_TX DER signature and BIP-143 preimage for a transaction input.
Convenience wrapper combining compute_preimage and sign_preimage_k1. When code_separator_index is provided and non-negative, only the portion of the subscript after the OP_CODESEPARATOR byte is used as the scriptCode.
82 83 84 85 86 87 |
# File 'lib/runar/sdk/oppushtx.rb', line 82 def compute_op_push_tx(tx_hex, input_index, subscript_hex, satoshis, code_separator_index = -1) effective_subscript = get_subscript(subscript_hex, code_separator_index) preimage_hex = compute_preimage(tx_hex, input_index, effective_subscript, satoshis) sig_hex = sign_preimage_k1(preimage_hex) [sig_hex, preimage_hex] end |
.compute_preimage(tx_hex, input_index, subscript_hex, satoshis, sighash_type = SIGHASH_ALL_FORKID) ⇒ String
Compute the BIP-143 sighash preimage for a transaction input.
Parses the raw transaction hex, then assembles the ten-field BIP-143 preimage for the given input. Returns the preimage as a lowercase hex string (208 bytes = 416 hex chars for typical transactions).
41 42 43 44 45 46 |
# File 'lib/runar/sdk/oppushtx.rb', line 41 def compute_preimage(tx_hex, input_index, subscript_hex, satoshis, sighash_type = SIGHASH_ALL_FORKID) tx = parse_raw_tx([tx_hex].pack('H*')) subscript = [subscript_hex].pack('H*') bip143_preimage(tx, input_index, subscript, satoshis, sighash_type).unpack1('H*') end |
.double_sha256(hex) ⇒ String
Double-SHA256 hash (SHA256(SHA256(data))).
114 115 116 117 |
# File 'lib/runar/sdk/oppushtx.rb', line 114 def double_sha256(hex) bytes = [hex].pack('H*') Digest::SHA256.hexdigest(Digest::SHA256.digest(bytes)) end |
.encode_varint(n) ⇒ String
Encode an integer as a Bitcoin-style varint (hex string).
151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/runar/sdk/deployment.rb', line 151 def encode_varint(n) if n < 0xFD format('%02x', n) elsif n <= 0xFFFF 'fd' + [n].pack('v').unpack1('H*') elsif n <= 0xFFFFFFFF 'fe' + [n].pack('V').unpack1('H*') else 'ff' + [n].pack('Q<').unpack1('H*') end end |
.estimate_deploy_fee(num_inputs, locking_script_byte_len, fee_rate = 100) ⇒ Integer
Estimate the fee for a deploy transaction.
Accounts for: overhead, P2PKH inputs, the contract output (variable-size script), and one P2PKH change output.
115 116 117 118 119 120 121 122 |
# File 'lib/runar/sdk/deployment.rb', line 115 def estimate_deploy_fee(num_inputs, locking_script_byte_len, fee_rate = 100) rate = [1, fee_rate].max inputs_size = num_inputs * P2PKH_INPUT_SIZE contract_out_size = 8 + varint_byte_size(locking_script_byte_len) + locking_script_byte_len change_output_size = P2PKH_OUTPUT_SIZE tx_size = TX_OVERHEAD + inputs_size + contract_out_size + change_output_size (tx_size * rate + 999) / 1000 end |
.get_subscript(script_hex, code_separator_index) ⇒ String
Extract the subscript for BIP-143 sighash computation.
When code_separator_index is nil or -1, the full script is returned. Otherwise everything after the OP_CODESEPARATOR at the given byte offset is returned (i.e. bytes from code_separator_index 1+ onward).
99 100 101 102 103 104 105 106 107 108 |
# File 'lib/runar/sdk/oppushtx.rb', line 99 def get_subscript(script_hex, code_separator_index) return script_hex if code_separator_index.nil? || code_separator_index.negative? # code_separator_index is the byte offset of the OP_CODESEPARATOR opcode # itself; the subscript begins at the next byte. trim_pos = (code_separator_index + 1) * 2 return script_hex if trim_pos > script_hex.length script_hex[trim_pos..] end |
.hex_string?(str) ⇒ Boolean
Return true if str is a valid hexadecimal string.
190 191 192 |
# File 'lib/runar/sdk/deployment.rb', line 190 def hex_string?(str) str.match?(/\A[0-9a-fA-F]+\z/) end |
.insert_unlocking_script(tx_hex, input_index, unlock_script) ⇒ String
Replace the scriptSig of a specific input within a raw transaction.
Parses the hex-encoded transaction, locates the scriptSig at input_index, substitutes it with unlock_script, and returns the modified transaction as hex. All other fields remain unchanged.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/runar/sdk/calling.rb', line 171 def insert_unlocking_script(tx_hex, input_index, unlock_script) pos = 0 # Skip version (4 bytes = 8 hex chars). pos += 8 # Read input count varint. input_count, ic_hex_len = read_varint_hex(tx_hex, pos) pos += ic_hex_len if input_index >= input_count raise ArgumentError, "insert_unlocking_script: input index #{input_index} out of range " \ "(#{input_count} inputs)" end input_count.times do |i| # prevTxid (32 bytes = 64 hex chars) + prevOutputIndex (4 bytes = 8 hex chars) pos += 64 + 8 # Read scriptSig length varint. script_len, sl_hex_len = read_varint_hex(tx_hex, pos) if i == input_index new_byte_len = unlock_script.length / 2 new_varint = encode_varint(new_byte_len) before = tx_hex[0, pos] after = tx_hex[pos + sl_hex_len + script_len * 2..] return "#{before}#{new_varint}#{unlock_script}#{after}" end # Skip scriptSig bytes + sequence (4 bytes = 8 hex chars). pos += sl_hex_len + script_len * 2 + 8 end raise ArgumentError, "insert_unlocking_script: input index #{input_index} out of range" end |
.read_varint_hex(hex_str, pos) ⇒ Array(Integer, Integer)
Read a Bitcoin-style varint from a hex string at position pos.
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/runar/sdk/calling.rb', line 219 def read_varint_hex(hex_str, pos) first = hex_str[pos, 2].to_i(16) case first when 0...0xFD [first, 2] when 0xFD lo = hex_str[pos + 2, 2].to_i(16) hi = hex_str[pos + 4, 2].to_i(16) [lo | (hi << 8), 6] when 0xFE value = [hex_str[pos + 2, 8]].pack('H*').unpack1('V') [value, 10] else # 0xFF value = [hex_str[pos + 2, 16]].pack('H*').unpack1('Q<') [value, 18] end end |
.reverse_hex(hex_str) ⇒ Object
Reverse the byte order of a hex string (converts txid to wire format).
185 186 187 |
# File 'lib/runar/sdk/deployment.rb', line 185 def reverse_hex(hex_str) [hex_str].pack('H*').reverse.unpack1('H*') end |
.select_utxos(utxos, target_satoshis, locking_script_byte_len, fee_rate: 100) ⇒ Array<Utxo>
Select the minimum set of UTXOs needed to fund a deployment using a largest-first strategy. Returns the selected subset; raises ArgumentError when funds are insufficient.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/runar/sdk/deployment.rb', line 89 def select_utxos(utxos, target_satoshis, locking_script_byte_len, fee_rate: 100) sorted = utxos.sort_by { |u| -u.satoshis } selected = [] total = 0 sorted.each do |utxo| selected << utxo total += utxo.satoshis fee = estimate_deploy_fee(selected.length, locking_script_byte_len, fee_rate) return selected if total >= target_satoshis + fee end raise ArgumentError, "select_utxos: insufficient funds. " \ "Need #{target_satoshis} sats plus fee, have #{total}" end |
.sign_preimage_k1(preimage_hex) ⇒ String
Compute the OP_PUSH_TX DER signature for a preimage.
Hashes the preimage with double-SHA256, then signs with private key d=1 and nonce k=1. Returns the DER-encoded signature with the sighash byte appended, hex-encoded.
56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/runar/sdk/oppushtx.rb', line 56 def sign_preimage_k1(preimage_hex) hash = double_sha256(preimage_hex) hash_bytes = [hash].pack('H*') r, s = ecdsa_sign_k1(hash_bytes) # Enforce low-S normalisation. half_n = ECPrimitives::SECP256K1_N >> 1 s = ECPrimitives::SECP256K1_N - s if s > half_n der_encode(r, s).unpack1('H*') + format('%02x', SIGHASH_ALL_FORKID) end |
.to_le32(n) ⇒ Object
Encode a 32-bit unsigned integer as 4 little-endian bytes (hex string).
175 176 177 |
# File 'lib/runar/sdk/deployment.rb', line 175 def to_le32(n) [n].pack('V').unpack1('H*') end |
.to_le64(n) ⇒ Object
Encode a 64-bit unsigned integer as 8 little-endian bytes (hex string).
180 181 182 |
# File 'lib/runar/sdk/deployment.rb', line 180 def to_le64(n) [n].pack('Q<').unpack1('H*') end |
.varint_byte_size(n) ⇒ Integer
Return the byte size of a varint encoding for n.
166 167 168 169 170 171 172 |
# File 'lib/runar/sdk/deployment.rb', line 166 def varint_byte_size(n) return 1 if n < 0xFD return 3 if n <= 0xFFFF return 5 if n <= 0xFFFFFFFF 9 end |