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 +4 -4
- data/{CHANGELOG-sdk.md → CHANGELOG.md} +37 -0
- data/bin/bsv-mcp +10 -0
- data/lib/bsv/mcp/config.rb +44 -0
- data/lib/bsv/mcp/server.rb +68 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +211 -0
- data/lib/bsv/mcp/tools/check_balance.rb +96 -0
- data/lib/bsv/mcp/tools/decode_tx.rb +64 -0
- data/lib/bsv/mcp/tools/fetch_tx.rb +72 -0
- data/lib/bsv/mcp/tools/fetch_utxos.rb +72 -0
- data/lib/bsv/mcp/tools/generate_key.rb +62 -0
- data/lib/bsv/mcp/tools/helpers.rb +87 -0
- data/lib/bsv/mcp.rb +18 -0
- data/lib/bsv/network/arc.rb +107 -8
- data/lib/bsv/primitives/schnorr.rb +22 -0
- data/lib/bsv/script/interpreter/error.rb +1 -0
- data/lib/bsv/script/interpreter/interpreter.rb +16 -11
- data/lib/bsv/script/interpreter/operations/arithmetic.rb +43 -7
- data/lib/bsv/script/interpreter/operations/flow_control.rb +85 -10
- data/lib/bsv/script/interpreter/operations/splice.rb +49 -0
- data/lib/bsv/script/opcodes.rb +2 -3
- data/lib/bsv/transaction/chain_trackers/chaintracks.rb +95 -0
- data/lib/bsv/transaction/chain_trackers.rb +11 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv-sdk.rb +1 -0
- metadata +33 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 264bf278568bb64d32cd8445708f29436478c0b9a2bee01e74a71a00549e46f9
|
|
4
|
+
data.tar.gz: 0a29a5a53bac041332971c8a51628289ced3606423b4f02777780eac7171eeeb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|