bsv-sdk 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4947a18f171b512f9504b7f43b9e53867f72e221f3408db8f7c5d93f4cd7878c
4
- data.tar.gz: a282778766358b1b0fe8c8184ecab5132548ec7a68d48989712ffd906b00c2a6
3
+ metadata.gz: 264bf278568bb64d32cd8445708f29436478c0b9a2bee01e74a71a00549e46f9
4
+ data.tar.gz: 0a29a5a53bac041332971c8a51628289ced3606423b4f02777780eac7171eeeb
5
5
  SHA512:
6
- metadata.gz: c70124fbf7d8044af528f1e927da94fe1c0661d35e09053e10ae934d83d5f808ec7fc612891d95e2faae2ee76c15e5355c2ae27b15cacbf29bec7ae911ff6244
7
- data.tar.gz: de507490b32cd0bc3dc494410a3a0637af2c32620bcd704ffcd285bc479b4c28f15e8304af59f4d907deb01234f8ffc6b6cea1d87baec4f8c358f0214595c3bb
6
+ metadata.gz: fa512a05a6afb7099c94c55a54524623ab0706fddc4b7836315f4750e6ad69abc438581aea4707e1ae6e93b23437868960a7c2bbfca8b86848d676cd587bbd7b
7
+ data.tar.gz: 35d2b3175fa5c5c65a200756e7c9abb64166b44fc370cac38ecb29a8faefdba641de60a07cdb2d9f3d12a695784b09eea8ba12b502865222073fdfe171251b91
@@ -5,6 +5,43 @@ All notable changes to the `bsv-sdk` gem are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 0.10.0 — 2026-04-11
9
+
10
+ ### Added
11
+
12
+ - **Chronicle opcode support** (HLR #328). All 10 Chronicle-restored opcodes
13
+ now execute with correct semantics, replacing the 0.9.0 "raise first"
14
+ fail-safes:
15
+ - **Splice:** OP_SUBSTR, OP_LEFT, OP_RIGHT
16
+ - **Arithmetic:** OP_LSHIFTNUM, OP_RSHIFTNUM (sign-preserving right shift)
17
+ - **Arithmetic:** OP_2MUL, OP_2DIV (restored from disabled)
18
+ - **Flow control:** OP_VER (push 4-byte LE tx version), OP_VERIF/OP_VERNOTIF
19
+ (version-conditional branching, added to CONDITIONAL_OPCODES)
20
+ - `MISSING_TX_CONTEXT` error code for OP_VER without transaction context
21
+
22
+ - **Arcade integration** (HLR #329):
23
+ - `BSV::Transaction::ChainTrackers::Chaintracks` — chain tracker using
24
+ Arcade's Chaintracks v2 API for SPV verification
25
+ - `ChainTrackers.default(testnet:)` — factory returning a Chaintracks
26
+ instance pointed at Arcade
27
+ - `ARC.default(testnet:)` — factory returning an ARC broadcaster pointed
28
+ at GorillaPool, matching TS/Go/Py SDK defaults
29
+ - `ARC#broadcast_many(txs)` — batch broadcast via POST `/v1/txs`, returns
30
+ mixed array of `BroadcastResponse`/`BroadcastError`
31
+ - Skip-validation headers (`skip_fee_validation:`, `skip_script_validation:`)
32
+ on `broadcast` and `broadcast_many`
33
+
34
+ ### Changed
35
+
36
+ - **Directory restructure** (PR #327). Source files moved to per-gem
37
+ directories: `gem/bsv-sdk/`, `gem/bsv-wallet/`, `gem/bsv-attest/`.
38
+ No change to gem names or require paths.
39
+
40
+ ### Removed
41
+
42
+ - **`op_disabled` handler** — OP_2MUL/OP_2DIV are now restored; the handler
43
+ is no longer needed.
44
+
8
45
  ## 0.9.0 — 2026-04-10
9
46
 
10
47
  ### Changed
data/bin/bsv-mcp ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # MCP stdio protocol: stdout is the JSON-RPC channel — do not write to it.
5
+ # All diagnostic output must use $stderr (warn, $stderr.puts, etc.).
6
+
7
+ require_relative '../lib/bsv-sdk'
8
+
9
+ config = BSV::MCP::Config.new
10
+ BSV::MCP::Server.start(config)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module MCP
5
+ # Configuration parsed from environment variables for the MCP server.
6
+ #
7
+ # Environment variables:
8
+ # BSV_NETWORK — 'mainnet' or 'testnet' (default: 'mainnet')
9
+ # BSV_ARC_URL — optional ARC endpoint override
10
+ # BSV_ARC_API_KEY — optional ARC bearer token
11
+ class Config
12
+ MAINNET = 'mainnet'
13
+ TESTNET = 'testnet'
14
+ VALID_NETWORKS = [MAINNET, TESTNET].freeze
15
+
16
+ attr_reader :network, :arc_url, :arc_api_key
17
+
18
+ def initialize(network: nil, arc_url: nil, arc_api_key: nil)
19
+ raw_network = network || ENV.fetch('BSV_NETWORK', MAINNET)
20
+ @network = normalise_network(raw_network)
21
+ @arc_url = arc_url || ENV.fetch('BSV_ARC_URL', nil)
22
+ @arc_api_key = arc_api_key || ENV.fetch('BSV_ARC_API_KEY', nil)
23
+ end
24
+
25
+ def mainnet?
26
+ @network == MAINNET
27
+ end
28
+
29
+ def testnet?
30
+ @network == TESTNET
31
+ end
32
+
33
+ private
34
+
35
+ def normalise_network(value)
36
+ normalised = value.to_s.strip.downcase
37
+ return normalised if VALID_NETWORKS.include?(normalised)
38
+
39
+ warn "BSV::MCP::Config: unknown network '#{value}', defaulting to '#{MAINNET}'"
40
+ MAINNET
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+
5
+ module BSV
6
+ module MCP
7
+ # Builds and returns an MCP::Server instance configured for the BSV SDK.
8
+ #
9
+ # Tools are registered here as the feature set grows. Currently returns an
10
+ # empty tool list — Tasks 2 and 3 will add concrete tools.
11
+ #
12
+ # Logging goes to $stderr only; $stdout is the MCP protocol channel.
13
+ module Server
14
+ # Builds the MCP::Server instance with the given config.
15
+ #
16
+ # @param config [BSV::MCP::Config] network and ARC configuration
17
+ # @return [::MCP::Server]
18
+ def self.build(config = nil)
19
+ config ||= Config.new
20
+
21
+ ::MCP::Server.new(
22
+ name: 'bsv-sdk',
23
+ version: BSV::VERSION,
24
+ instructions: server_instructions(config),
25
+ tools: registered_tools
26
+ )
27
+ end
28
+
29
+ # Returns the list of tool classes registered with this server.
30
+ #
31
+ # @return [Array<Class>] tool classes inheriting from MCP::Tool
32
+ def self.registered_tools
33
+ [
34
+ Tools::GenerateKey,
35
+ Tools::DecodeTx,
36
+ Tools::FetchUtxos,
37
+ Tools::FetchTx,
38
+ Tools::CheckBalance,
39
+ Tools::BroadcastP2pkh
40
+ ]
41
+ end
42
+
43
+ # Starts the MCP server over stdio using the standard StdioTransport.
44
+ # Blocks until the client disconnects or the process is interrupted.
45
+ #
46
+ # @param config [BSV::MCP::Config] network and ARC configuration
47
+ def self.start(config = nil)
48
+ config ||= Config.new
49
+
50
+ warn "BSV MCP server starting (network: #{config.network}, version: #{BSV::VERSION})"
51
+
52
+ server = build(config)
53
+ transport = ::MCP::Server::Transports::StdioTransport.new(server)
54
+ transport.open
55
+ end
56
+
57
+ # @api private
58
+ def self.server_instructions(config)
59
+ <<~INSTRUCTIONS.strip
60
+ BSV blockchain SDK tools. Network: #{config.network}.
61
+ All operations target the #{config.network} network unless otherwise noted.
62
+ Keys are passed as WIF strings and are never persisted by the server.
63
+ INSTRUCTIONS
64
+ end
65
+ private_class_method :server_instructions
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module MCP
5
+ module Tools
6
+ # Builds, signs, and broadcasts a simple P2PKH payment transaction.
7
+ #
8
+ # Fetches UTXOs for the sender's address, constructs a transaction
9
+ # with one payment output and one change output, signs all inputs
10
+ # with the provided private key, and submits to ARC in Extended
11
+ # Format (EF/BRC-30).
12
+ #
13
+ # This is the most complex MCP tool: it orchestrates WIF → PrivateKey
14
+ # → PublicKey → address → UTXO fetch → tx build → sign → broadcast.
15
+ class BroadcastP2pkh < ::MCP::Tool
16
+ tool_name 'broadcast_p2pkh'
17
+
18
+ description <<~DESC.strip
19
+ Build, sign, and broadcast a P2PKH payment transaction.
20
+
21
+ WARNING: This tool broadcasts a REAL transaction to the BSV network
22
+ and SPENDS REAL SATOSHIS. Use testnet for testing.
23
+
24
+ The tool:
25
+ 1. Derives the sender address from the WIF private key
26
+ 2. Fetches UTXOs for the sender address via WhatsOnChain
27
+ 3. Selects sufficient UTXOs (greedy: largest first)
28
+ 4. Builds a transaction with a payment output and a change output
29
+ 5. Signs all inputs with the WIF key (P2PKH, SIGHASH_ALL|FORKID)
30
+ 6. Submits to ARC in Extended Format (EF/BRC-30)
31
+ 7. Returns txid, status, and the raw hex
32
+
33
+ Parameters:
34
+ - wif: sender's private key in WIF format. The key is used only
35
+ for this call and is never stored by the server.
36
+ - to_address: recipient P2PKH address
37
+ - satoshis: amount to send in satoshis (must be > 0)
38
+ - network: 'mainnet' or 'testnet' — overrides the server default
39
+
40
+ Returns:
41
+ - txid: the broadcast transaction ID
42
+ - tx_status: ARC status string (e.g. 'SEEN_ON_NETWORK')
43
+ - hex: the raw transaction hex (plain format, not EF)
44
+
45
+ Error conditions (returned as error responses, not raised):
46
+ - Invalid WIF key
47
+ - No UTXOs found for sender
48
+ - Insufficient funds (balance < satoshis + estimated fee)
49
+ - Change below dust (546 sat): change is absorbed into the fee
50
+ - ARC broadcast rejection
51
+
52
+ Technical notes:
53
+ - All inputs have source_satoshis and source_locking_script set,
54
+ enabling Extended Format (EF/BRC-30) submission to ARC.
55
+ - Fee model: SatoshisPerKilobyte at 100 sat/kB (SDK default).
56
+ - Change returns to the sender address.
57
+ - UTXO selection: greedy descending by value.
58
+ DESC
59
+
60
+ input_schema(
61
+ type: 'object',
62
+ properties: {
63
+ wif: {
64
+ type: 'string',
65
+ description: 'Sender private key in WIF format.'
66
+ },
67
+ to_address: {
68
+ type: 'string',
69
+ description: 'Recipient P2PKH address.'
70
+ },
71
+ satoshis: {
72
+ type: 'integer',
73
+ description: 'Amount to send in satoshis.',
74
+ minimum: 1
75
+ },
76
+ network: {
77
+ type: 'string',
78
+ enum: %w[mainnet testnet],
79
+ description: 'Network to broadcast on. Overrides the server default.'
80
+ }
81
+ },
82
+ required: %w[wif to_address satoshis]
83
+ )
84
+
85
+ # Minimum output value to avoid dust rejection.
86
+ DUST_THRESHOLD = 546
87
+
88
+ def self.call(wif:, to_address:, satoshis:, network: nil, server_context: nil)
89
+ net_sym = Helpers.resolve_network_sym(network, server_context)
90
+
91
+ private_key = BSV::Primitives::PrivateKey.from_wif(wif)
92
+ sender_address = private_key.public_key.address(network: net_sym)
93
+
94
+ woc = BSV::Network::WhatsOnChain.new(network: net_sym)
95
+ all_utxos = woc.fetch_utxos(sender_address)
96
+
97
+ return Helpers.error_response('No UTXOs found for sender address — the address may have no funds') if all_utxos.empty?
98
+
99
+ # Greedy UTXO selection: sort descending, take until sufficient
100
+ sorted = all_utxos.sort_by { |u| -u.satoshis }
101
+ selected = select_utxos(sorted, satoshis)
102
+ return Helpers.error_response("Insufficient funds — available: #{all_utxos.sum(&:satoshis)} satoshis") if selected.nil?
103
+
104
+ tx = build_transaction(selected, satoshis, to_address, sender_address, private_key)
105
+
106
+ arc = build_arc(net_sym, server_context)
107
+ broadcast_result = arc.broadcast(tx)
108
+
109
+ result = {
110
+ txid: broadcast_result.txid,
111
+ tx_status: broadcast_result.tx_status,
112
+ hex: tx.to_hex
113
+ }
114
+
115
+ ::MCP::Tool::Response.new(
116
+ [::MCP::Content::Text.new(result.to_json)],
117
+ structured_content: result
118
+ )
119
+ rescue ArgumentError => e
120
+ Helpers.error_response(e.message)
121
+ rescue BSV::Network::ChainProviderError => e
122
+ Helpers.error_response("UTXO fetch failed: #{e.message}")
123
+ rescue BSV::Network::BroadcastError => e
124
+ Helpers.error_response("Broadcast failed: #{e.message}")
125
+ end
126
+
127
+ # Select UTXOs greedily until the total meets or exceeds the target.
128
+ # Returns nil when total funds are insufficient.
129
+ # @api private
130
+ def self.select_utxos(sorted_utxos, target_satoshis)
131
+ selected = []
132
+ total = 0
133
+ sorted_utxos.each do |utxo|
134
+ selected << utxo
135
+ total += utxo.satoshis
136
+ break if total >= target_satoshis
137
+ end
138
+ # We may still not have enough — caller checks nil return
139
+ total >= target_satoshis ? selected : nil
140
+ end
141
+ private_class_method :select_utxos
142
+
143
+ # Build, fee-compute, and sign the transaction.
144
+ # @api private
145
+ def self.build_transaction(selected_utxos, satoshis, to_address, sender_address, private_key)
146
+ tx = BSV::Transaction::Transaction.new
147
+
148
+ # Wire inputs with EF metadata (source_satoshis + source_locking_script)
149
+ selected_utxos.each do |utxo|
150
+ locking_script = p2pkh_lock_for(sender_address)
151
+ input = BSV::Transaction::TransactionInput.new(
152
+ prev_tx_id: BSV::Transaction::TransactionInput.txid_from_hex(utxo.tx_hash),
153
+ prev_tx_out_index: utxo.tx_pos
154
+ )
155
+ input.source_satoshis = utxo.satoshis
156
+ input.source_locking_script = locking_script
157
+ input.unlocking_script_template = BSV::Transaction::P2PKH.new(private_key)
158
+ tx.add_input(input)
159
+ end
160
+
161
+ # Payment output
162
+ tx.add_output(BSV::Transaction::TransactionOutput.new(
163
+ satoshis: satoshis,
164
+ locking_script: p2pkh_lock_for(to_address)
165
+ ))
166
+
167
+ # Change output (marked as change so tx.fee distributes into it)
168
+ tx.add_output(BSV::Transaction::TransactionOutput.new(
169
+ satoshis: 0,
170
+ locking_script: p2pkh_lock_for(sender_address),
171
+ change: true
172
+ ))
173
+
174
+ # Compute fee and distribute change; removes change output when dust
175
+ tx.fee
176
+
177
+ # Guard: if total outputs exceed inputs the transaction is invalid
178
+ if tx.total_output_satoshis > tx.total_input_satoshis
179
+ raise ArgumentError,
180
+ "Insufficient funds: inputs #{tx.total_input_satoshis} sat, " \
181
+ "outputs #{tx.total_output_satoshis} sat (including fee)"
182
+ end
183
+
184
+ # Sign all inputs via their P2PKH templates
185
+ tx.sign_all
186
+
187
+ tx
188
+ end
189
+ private_class_method :build_transaction
190
+
191
+ # Build a P2PKH locking script for the given address.
192
+ # @api private
193
+ def self.p2pkh_lock_for(address)
194
+ hash160 = BSV::Primitives::Base58.check_decode(address)[1..]
195
+ BSV::Script::Script.p2pkh_lock(hash160)
196
+ end
197
+ private_class_method :p2pkh_lock_for
198
+
199
+ # Build an ARC broadcaster using server config when available.
200
+ # @api private
201
+ def self.build_arc(net_sym, server_context)
202
+ testnet = net_sym == :testnet
203
+ opts = {}
204
+ opts[:api_key] = server_context[:arc_api_key] if server_context.is_a?(Hash) && server_context[:arc_api_key]
205
+ BSV::Network::ARC.default(testnet: testnet, **opts)
206
+ end
207
+ private_class_method :build_arc
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module MCP
5
+ module Tools
6
+ # Returns the total confirmed and unconfirmed balance for a BSV address
7
+ # or WIF private key, along with the individual UTXOs.
8
+ #
9
+ # Accepts either a WIF-encoded private key (from which the address is
10
+ # derived) or a plain P2PKH address string. The tool auto-detects the
11
+ # input type via a try/rescue on +PrivateKey.from_wif+.
12
+ class CheckBalance < ::MCP::Tool
13
+ tool_name 'check_balance'
14
+
15
+ description <<~DESC.strip
16
+ Check the BSV balance for an address or WIF private key.
17
+
18
+ Accepts either a WIF-encoded private key (auto-derives the address)
19
+ or a P2PKH address string directly. The tool detects which was
20
+ supplied automatically.
21
+
22
+ Parameters:
23
+ - address_or_wif: a BSV P2PKH address (mainnet starts with '1';
24
+ testnet starts with 'm' or 'n') OR a WIF private key (mainnet
25
+ starts with '5', 'K', or 'L'; testnet starts with '9' or 'c')
26
+ - network: 'mainnet' or 'testnet' — overrides the server default
27
+
28
+ Returns:
29
+ - address: the resolved P2PKH address
30
+ - balance_satoshis: total balance across all UTXOs
31
+ - utxo_count: number of unspent outputs
32
+ - utxos: array of { tx_hash, tx_pos, satoshis, height } objects
33
+
34
+ Note: balance is the sum of all UTXOs; it does not distinguish
35
+ confirmed from unconfirmed. A height of 0 indicates an unconfirmed
36
+ (mempool) UTXO.
37
+
38
+ Note: addresses are network-specific. A mainnet address queried
39
+ against testnet (or vice versa) will return no results or an error.
40
+ DESC
41
+
42
+ input_schema(
43
+ type: 'object',
44
+ properties: {
45
+ address_or_wif: {
46
+ type: 'string',
47
+ description: 'BSV P2PKH address or WIF private key to check.'
48
+ },
49
+ network: {
50
+ type: 'string',
51
+ enum: %w[mainnet testnet],
52
+ description: 'Network to query. Overrides the server default.'
53
+ }
54
+ },
55
+ required: ['address_or_wif']
56
+ )
57
+
58
+ def self.call(address_or_wif:, network: nil, server_context: nil)
59
+ net_sym = Helpers.resolve_network_sym(network, server_context)
60
+ address = resolve_address(address_or_wif, net_sym)
61
+
62
+ woc = BSV::Network::WhatsOnChain.new(network: net_sym)
63
+ utxos = woc.fetch_utxos(address)
64
+
65
+ balance = utxos.sum(&:satoshis)
66
+ result = {
67
+ address: address,
68
+ network: net_sym.to_s,
69
+ balance_satoshis: balance,
70
+ utxo_count: utxos.length,
71
+ utxos: utxos.map { |u| Helpers.utxo_to_h(u) }
72
+ }
73
+
74
+ ::MCP::Tool::Response.new(
75
+ [::MCP::Content::Text.new(result.to_json)],
76
+ structured_content: result
77
+ )
78
+ rescue ArgumentError => e
79
+ Helpers.error_response(e.message)
80
+ rescue BSV::Network::ChainProviderError => e
81
+ Helpers.error_response("#{e.message} (HTTP #{e.status_code})")
82
+ end
83
+
84
+ # Resolve a WIF key or address string to a P2PKH address.
85
+ # @api private
86
+ def self.resolve_address(address_or_wif, net_sym)
87
+ key = BSV::Primitives::PrivateKey.from_wif(address_or_wif)
88
+ key.public_key.address(network: net_sym)
89
+ rescue ArgumentError
90
+ address_or_wif
91
+ end
92
+ private_class_method :resolve_address
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module MCP
5
+ module Tools
6
+ # Decodes a raw BSV transaction from hex and returns a structured
7
+ # human-readable representation.
8
+ #
9
+ # Supports standard raw hex only (not BEEF format). The txid is
10
+ # returned in display byte order (reversed from the wire hash),
11
+ # matching block explorer conventions.
12
+ class DecodeTx < ::MCP::Tool
13
+ tool_name 'decode_tx'
14
+
15
+ description <<~DESC.strip
16
+ Decode a raw BSV transaction hex string into a structured JSON object.
17
+
18
+ Accepts standard raw transaction hex (not BEEF format). Returns:
19
+ - txid: transaction ID in display byte order (as seen in block explorers)
20
+ - version: transaction version number (typically 1)
21
+ - lock_time: transaction locktime (0 means no locktime)
22
+ - inputs: array of inputs, each with:
23
+ - prev_txid: the txid of the output being spent
24
+ - vout: output index in the previous transaction
25
+ - script_hex: raw unlocking script as hex
26
+ - script_asm: unlocking script in ASM notation
27
+ - sequence: input sequence number
28
+ - outputs: array of outputs, each with:
29
+ - index: output index in this transaction
30
+ - satoshis: value in satoshis
31
+ - script_hex: raw locking script as hex
32
+ - script_asm: locking script in ASM notation
33
+ - script_type: detected script type (pubkeyhash, nulldata, pubkey, etc.)
34
+
35
+ Note: input prev_txids are shown in display byte order. The raw wire
36
+ format stores them reversed; this tool normalises them for readability.
37
+ DESC
38
+
39
+ input_schema(
40
+ type: 'object',
41
+ properties: {
42
+ hex: {
43
+ type: 'string',
44
+ description: 'Raw transaction hex string (not BEEF format).'
45
+ }
46
+ },
47
+ required: ['hex']
48
+ )
49
+
50
+ def self.call(hex:, **)
51
+ tx = BSV::Transaction::Transaction.from_hex(hex)
52
+ result = Helpers.transaction_to_h(tx)
53
+
54
+ ::MCP::Tool::Response.new(
55
+ [::MCP::Content::Text.new(result.to_json)],
56
+ structured_content: result
57
+ )
58
+ rescue ArgumentError => e
59
+ Helpers.error_response(e.message)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module MCP
5
+ module Tools
6
+ # Fetches a transaction from the BSV network by its txid using the
7
+ # WhatsOnChain API, returning both the raw hex and decoded structure.
8
+ class FetchTx < ::MCP::Tool
9
+ tool_name 'fetch_tx'
10
+
11
+ description <<~DESC.strip
12
+ Fetch a transaction from the BSV network by its transaction ID.
13
+
14
+ Queries the WhatsOnChain API and returns the raw transaction hex
15
+ plus a decoded representation of its structure. Requires network access.
16
+
17
+ Parameters:
18
+ - txid: 64-character hex transaction ID (as shown in block explorers)
19
+ - network: 'mainnet' or 'testnet' — overrides the server default
20
+
21
+ Returns:
22
+ - txid: the transaction ID (confirmed by parsing)
23
+ - hex: raw transaction hex
24
+ - version: transaction version number
25
+ - lock_time: transaction locktime (0 = no locktime)
26
+ - inputs: array of inputs (prev_txid, vout, script_hex, script_asm, sequence)
27
+ - outputs: array of outputs (index, satoshis, script_hex, script_asm, script_type)
28
+
29
+ Note: WhatsOnChain returns raw transaction hex (not BEEF or EF format).
30
+ The txid in the response is derived by double-SHA-256 of the parsed tx,
31
+ and is shown in display byte order (matching block explorers).
32
+ DESC
33
+
34
+ input_schema(
35
+ type: 'object',
36
+ properties: {
37
+ txid: {
38
+ type: 'string',
39
+ description: '64-character hex transaction ID.'
40
+ },
41
+ network: {
42
+ type: 'string',
43
+ enum: %w[mainnet testnet],
44
+ description: 'Network to query. Overrides the server default.'
45
+ }
46
+ },
47
+ required: ['txid']
48
+ )
49
+
50
+ def self.call(txid:, network: nil, server_context: nil)
51
+ net_sym = Helpers.resolve_network_sym(network, server_context)
52
+ woc = BSV::Network::WhatsOnChain.new(network: net_sym)
53
+ tx = woc.fetch_transaction(txid)
54
+
55
+ result = {
56
+ hex: tx.to_hex,
57
+ network: net_sym.to_s
58
+ }.merge(Helpers.transaction_to_h(tx))
59
+
60
+ ::MCP::Tool::Response.new(
61
+ [::MCP::Content::Text.new(result.to_json)],
62
+ structured_content: result
63
+ )
64
+ rescue BSV::Network::ChainProviderError => e
65
+ Helpers.error_response("#{e.message} (HTTP #{e.status_code})")
66
+ rescue ArgumentError => e
67
+ Helpers.error_response(e.message)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end