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.
@@ -80,6 +80,55 @@ module BSV
80
80
  data = @dstack.pop_bytes
81
81
  @dstack.push_bytes(ScriptNumber.minimally_encode(data))
82
82
  end
83
+
84
+ # OP_SUBSTR: extract a substring by offset and length
85
+ # Stack: [... data offset len] -> [... data[offset, len]]
86
+ def op_substr
87
+ len = @dstack.pop_int.to_i32
88
+ offset = @dstack.pop_int.to_i32
89
+ data = @dstack.pop_bytes
90
+
91
+ unless offset >= 0 && offset < data.bytesize && len >= 0 && len <= data.bytesize - offset
92
+ raise ScriptError.new(
93
+ ScriptErrorCode::INVALID_INPUT_LENGTH,
94
+ "invalid OP_SUBSTR range: offset=#{offset} len=#{len} for data of length #{data.bytesize}"
95
+ )
96
+ end
97
+
98
+ @dstack.push_bytes(data.byteslice(offset, len))
99
+ end
100
+
101
+ # OP_LEFT: extract the leftmost N bytes
102
+ # Stack: [... data len] -> [... data[0, len]]
103
+ def op_left
104
+ len = @dstack.pop_int.to_i32
105
+ data = @dstack.pop_bytes
106
+
107
+ unless len.between?(0, data.bytesize)
108
+ raise ScriptError.new(
109
+ ScriptErrorCode::INVALID_INPUT_LENGTH,
110
+ "invalid OP_LEFT length: #{len} for data of length #{data.bytesize}"
111
+ )
112
+ end
113
+
114
+ @dstack.push_bytes(data.byteslice(0, len))
115
+ end
116
+
117
+ # OP_RIGHT: extract the rightmost N bytes
118
+ # Stack: [... data len] -> [... data[size-len, len]]
119
+ def op_right
120
+ len = @dstack.pop_int.to_i32
121
+ data = @dstack.pop_bytes
122
+
123
+ unless len.between?(0, data.bytesize)
124
+ raise ScriptError.new(
125
+ ScriptErrorCode::INVALID_INPUT_LENGTH,
126
+ "invalid OP_RIGHT length: #{len} for data of length #{data.bytesize}"
127
+ )
128
+ end
129
+
130
+ @dstack.push_bytes(data.byteslice(data.bytesize - len, len))
131
+ end
83
132
  end
84
133
  end
85
134
  end
@@ -130,9 +130,8 @@ module BSV
130
130
  OP_CHECKLOCKTIMEVERIFY = 0xb1
131
131
  OP_CHECKSEQUENCEVERIFY = 0xb2
132
132
 
133
- # Chronicle slots (0xb3–0xb7): reserved for future string/shift opcodes.
134
- # Full semantics are deferred to SDK v0.10. Scripts using these opcodes
135
- # will raise ScriptErrorCode::UNIMPLEMENTED_OPCODE at execution time.
133
+ # Chronicle slots (0xb3–0xb7): string-splice and numeric-shift opcodes
134
+ # re-enabled in BSV Chronicle. Fully implemented in the interpreter.
136
135
  OP_SUBSTR = 0xb3
137
136
  OP_LEFT = 0xb4
138
137
  OP_RIGHT = 0xb5
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BSV
8
+ module Transaction
9
+ module ChainTrackers
10
+ # Chain tracker that verifies merkle roots using the Chaintracks API (Arcade/GorillaPool).
11
+ #
12
+ # Queries the Chaintracks v2 block header endpoint to retrieve the merkle root for a
13
+ # given block height and compares it with the provided root.
14
+ #
15
+ # @example
16
+ # tracker = BSV::Transaction::ChainTrackers::Chaintracks.new
17
+ # tracker.valid_root_for_height?('abcd...', 800_000)
18
+ #
19
+ # @example With API key
20
+ # tracker = BSV::Transaction::ChainTrackers::Chaintracks.new(api_key: 'my-key')
21
+ # tracker.current_height
22
+ class Chaintracks < ChainTracker
23
+ MAINNET_URL = 'https://arcade.gorillapool.io'
24
+ TESTNET_URL = 'https://testnet.arcade.gorillapool.io'
25
+
26
+ # @param url [String] base URL for the Chaintracks API
27
+ # @param api_key [String, nil] optional Bearer API key
28
+ # @param http_client [#request, nil] injectable HTTP client for testing
29
+ def initialize(url: MAINNET_URL, api_key: nil, http_client: nil)
30
+ super()
31
+ @url = url.chomp('/')
32
+ @api_key = api_key
33
+ @http_client = http_client
34
+ end
35
+
36
+ # Verify that a merkle root is valid for the given block height.
37
+ #
38
+ # @param root [String] merkle root as a hex string
39
+ # @param height [Integer] block height
40
+ # @return [Boolean]
41
+ def valid_root_for_height?(root, height)
42
+ response = get("/chaintracks/v2/header/height/#{height}")
43
+ return false if response.nil?
44
+
45
+ data = JSON.parse(response.body)
46
+ merkle_root = data['merkleRoot']
47
+ return false unless merkle_root
48
+
49
+ merkle_root.downcase == root.downcase
50
+ end
51
+
52
+ # Return the current blockchain height.
53
+ #
54
+ # @return [Integer]
55
+ def current_height
56
+ response = get('/chaintracks/v2/tip', not_found_returns_nil: false)
57
+ data = JSON.parse(response.body)
58
+ data['height']
59
+ end
60
+
61
+ private
62
+
63
+ # @param path [String] API path
64
+ # @param not_found_returns_nil [Boolean] if true, return nil on 404 instead of raising
65
+ # @return [Net::HTTPResponse, nil]
66
+ def get(path, not_found_returns_nil: true)
67
+ uri = URI("#{@url}#{path}")
68
+ request = Net::HTTP::Get.new(uri)
69
+ request['Authorization'] = "Bearer #{@api_key}" if @api_key
70
+
71
+ response = execute(uri, request)
72
+ code = response.code.to_i
73
+
74
+ return nil if not_found_returns_nil && code == 404
75
+ return response if (200..299).cover?(code)
76
+
77
+ raise BSV::Network::ChainProviderError.new(
78
+ response.body || "HTTP #{code}",
79
+ status_code: code
80
+ )
81
+ end
82
+
83
+ def execute(uri, request)
84
+ if @http_client
85
+ @http_client.request(uri, request)
86
+ else
87
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
88
+ http.request(request)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -5,6 +5,17 @@ module BSV
5
5
  # Namespace for chain tracker implementations.
6
6
  module ChainTrackers
7
7
  autoload :WhatsOnChain, 'bsv/transaction/chain_trackers/whats_on_chain'
8
+ autoload :Chaintracks, 'bsv/transaction/chain_trackers/chaintracks'
9
+
10
+ # Return a default chain tracker backed by the Arcade/GorillaPool Chaintracks API.
11
+ #
12
+ # @param testnet [Boolean] use the testnet endpoint when true
13
+ # @param api_key [String, nil] optional Bearer API key
14
+ # @return [Chaintracks]
15
+ def self.default(testnet: false, api_key: nil)
16
+ url = testnet ? Chaintracks::TESTNET_URL : Chaintracks::MAINNET_URL
17
+ Chaintracks.new(url: url, api_key: api_key)
18
+ end
8
19
  end
9
20
  end
10
21
  end
data/lib/bsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BSV
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/bsv-sdk.rb CHANGED
@@ -12,4 +12,5 @@ module BSV
12
12
  autoload :Overlay, 'bsv/overlay'
13
13
  autoload :Identity, 'bsv/identity'
14
14
  autoload :Registry, 'bsv/registry'
15
+ autoload :MCP, 'bsv/mcp'
15
16
  end
metadata CHANGED
@@ -1,23 +1,39 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsv-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Bettison
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-10 00:00:00.000000000 Z
11
- dependencies: []
10
+ date: 2026-04-11 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mcp
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.12'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.12'
12
26
  description: A Ruby library for interacting with the BSV Blockchain — keys, scripts,
13
27
  transactions, and more.
14
- executables: []
28
+ executables:
29
+ - bsv-mcp
15
30
  extensions: []
16
31
  extra_rdoc_files: []
17
32
  files:
18
- - CHANGELOG-sdk.md
33
+ - CHANGELOG.md
19
34
  - LICENSE
20
35
  - README.md
36
+ - bin/bsv-mcp
21
37
  - lib/bsv-sdk.rb
22
38
  - lib/bsv/auth.rb
23
39
  - lib/bsv/auth/auth_error.rb
@@ -31,6 +47,16 @@ files:
31
47
  - lib/bsv/identity/constants.rb
32
48
  - lib/bsv/identity/identity_parser.rb
33
49
  - lib/bsv/identity/types.rb
50
+ - lib/bsv/mcp.rb
51
+ - lib/bsv/mcp/config.rb
52
+ - lib/bsv/mcp/server.rb
53
+ - lib/bsv/mcp/tools/broadcast_p2pkh.rb
54
+ - lib/bsv/mcp/tools/check_balance.rb
55
+ - lib/bsv/mcp/tools/decode_tx.rb
56
+ - lib/bsv/mcp/tools/fetch_tx.rb
57
+ - lib/bsv/mcp/tools/fetch_utxos.rb
58
+ - lib/bsv/mcp/tools/generate_key.rb
59
+ - lib/bsv/mcp/tools/helpers.rb
34
60
  - lib/bsv/network.rb
35
61
  - lib/bsv/network/arc.rb
36
62
  - lib/bsv/network/broadcast_error.rb
@@ -97,6 +123,7 @@ files:
97
123
  - lib/bsv/transaction/beef.rb
98
124
  - lib/bsv/transaction/chain_tracker.rb
99
125
  - lib/bsv/transaction/chain_trackers.rb
126
+ - lib/bsv/transaction/chain_trackers/chaintracks.rb
100
127
  - lib/bsv/transaction/chain_trackers/whats_on_chain.rb
101
128
  - lib/bsv/transaction/fee_model.rb
102
129
  - lib/bsv/transaction/fee_models.rb
@@ -121,7 +148,7 @@ licenses:
121
148
  metadata:
122
149
  homepage_uri: https://github.com/sgbett/bsv-ruby-sdk
123
150
  source_code_uri: https://github.com/sgbett/bsv-ruby-sdk
124
- changelog_uri: https://github.com/sgbett/bsv-ruby-sdk/blob/master/CHANGELOG-sdk.md
151
+ changelog_uri: https://github.com/sgbett/bsv-ruby-sdk/blob/master/gem/bsv-sdk/CHANGELOG.md
125
152
  rubygems_mfa_required: 'true'
126
153
  rdoc_options: []
127
154
  require_paths: