bsv-sdk 0.23.1 → 0.25.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.md +79 -0
- data/README.md +1 -1
- data/lib/bsv/auth/auth_payload.rb +5 -0
- data/lib/bsv/identity/client.rb +9 -5
- data/lib/bsv/kv_store/entry.rb +15 -0
- data/lib/bsv/kv_store/global.rb +210 -0
- data/lib/bsv/kv_store/interpreter.rb +109 -0
- data/lib/bsv/kv_store/token.rb +10 -0
- data/lib/bsv/kv_store.rb +10 -0
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -2
- data/lib/bsv/mcp/tools/decode_tx.rb +1 -1
- data/lib/bsv/mcp/tools/fetch_tx.rb +1 -1
- data/lib/bsv/mcp/tools/helpers.rb +3 -3
- data/lib/bsv/network/protocol.rb +12 -1
- data/lib/bsv/network/util.rb +13 -5
- data/lib/bsv/overlay/admin_token_template.rb +2 -2
- data/lib/bsv/overlay/historian.rb +118 -0
- data/lib/bsv/overlay/topic_broadcaster.rb +1 -1
- data/lib/bsv/overlay/types.rb +37 -0
- data/lib/bsv/overlay.rb +1 -0
- data/lib/bsv/primitives/ecies.rb +12 -3
- data/lib/bsv/registry/client.rb +54 -7
- data/lib/bsv/script/bip276.rb +143 -0
- data/lib/bsv/script/interpreter/interpreter.rb +1 -1
- data/lib/bsv/script/push_drop_template.rb +2 -2
- data/lib/bsv/script.rb +1 -0
- data/lib/bsv/storage/downloader.rb +174 -0
- data/lib/bsv/storage/errors.rb +8 -0
- data/lib/bsv/storage/utils.rb +90 -0
- data/lib/bsv/storage.rb +16 -0
- data/lib/bsv/transaction/beef.rb +173 -19
- data/lib/bsv/transaction/beef_party.rb +119 -0
- data/lib/bsv/transaction/chain_tracker.rb +2 -2
- data/lib/bsv/transaction/fee_model.rb +1 -1
- data/lib/bsv/transaction/fee_models/live_policy.rb +1 -1
- data/lib/bsv/transaction/fee_models/satoshis_per_kilobyte.rb +1 -1
- data/lib/bsv/transaction/merkle_path.rb +2 -2
- data/lib/bsv/transaction/p2pkh.rb +1 -1
- data/lib/bsv/transaction/transaction_input.rb +1 -1
- data/lib/bsv/transaction/{transaction.rb → tx.rb} +18 -18
- data/lib/bsv/transaction/unlocking_script_template.rb +2 -2
- data/lib/bsv/transaction.rb +3 -2
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +13 -0
- data/lib/bsv/wallet/proto_wallet.rb +12 -2
- data/lib/bsv/wallet/serializer/create_signature.rb +7 -0
- data/lib/bsv/wallet/serializer/get_public_key.rb +5 -1
- data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +6 -3
- data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +6 -3
- data/lib/bsv/wallet/serializer/verify_signature.rb +7 -0
- data/lib/bsv-sdk.rb +2 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a0dbc7f524004e1db715058dd17d336e62f99687239ec4c7807512d730e6ef5
|
|
4
|
+
data.tar.gz: 45b1fc354cbfe718a38e01c0d4896d537600d188bba909f351da1bc17bac66c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e212bf3c077ab32a6d5187c0e3cbd93fd61f26844d40dce31b28c0574a1111db3a081dd0d843be90dc682779ede74ece03f0f7eccc0eec930a372306cdc3ca2
|
|
7
|
+
data.tar.gz: ec1e1b2e0c7b5ae142b52cb1863000fd262d0c1dc30346c2a7d412af2e1f66cbc3057c864bc9d6229737be59c30eae2eaf6d7fee7c296a9393c6e0f8b636b558
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,85 @@ 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.25.0 — 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `BSV::Storage::Utils` — UHRP URL helpers (`normalise_url`, `url_for_hash`,
|
|
12
|
+
`url_for_file`, `hash_from_url`, `valid_url?`) (#817).
|
|
13
|
+
- `BSV::Overlay::Historian` — generic output-history walker over `LookupResolver`,
|
|
14
|
+
enabling protocol-specific replay of an outpoint's lineage (#818).
|
|
15
|
+
- `BSV::Registry` — typed `resolve_basket`, `resolve_protocol`, and
|
|
16
|
+
`resolve_certificate` methods on the registry client (#819).
|
|
17
|
+
- `BSV::KVStore::Interpreter` — `Historian`-compatible interpreter for KV
|
|
18
|
+
protocol outputs (#820).
|
|
19
|
+
- `BSV::Storage::Downloader` — UHRP resolver and content fetcher with
|
|
20
|
+
hash verification (#821).
|
|
21
|
+
- `BSV::KVStore::Global` — overlay-backed read-only key-value reader (#822).
|
|
22
|
+
- `BSV::Script::BIP276` — text encoding for scripts and templates (`encode`,
|
|
23
|
+
`decode`) (#830).
|
|
24
|
+
- `BSV::Transaction::BeefParty` — `Beef` subclass tracking per-party knowledge
|
|
25
|
+
of transactions for multi-party BEEF exchange, plus supporting methods on
|
|
26
|
+
`Beef` (#831).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- `Beef` wire-inputs debug log now uses the existing `TransactionInput#dtxid_hex`
|
|
30
|
+
and `Tx#dtxid` accessors instead of inlined byte-order conversions (#828).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- ECIES bitcore variant — added `iv:` kwarg and TypeScript SDK conformance
|
|
34
|
+
vector (#829).
|
|
35
|
+
|
|
36
|
+
## 0.24.0 — 2026-06-11
|
|
37
|
+
|
|
38
|
+
### Breaking Changes
|
|
39
|
+
- **`BSV::Transaction::Transaction` renamed to `BSV::Transaction::Tx`** (#795). Class only;
|
|
40
|
+
module `BSV::Transaction` and peer classes (`TransactionInput`, `TransactionOutput`,
|
|
41
|
+
`ChainTracker`, `Beef`, etc.) unchanged. No compat alias — pre-1.0 clean break.
|
|
42
|
+
Downstream consumers update with `s/BSV::Transaction::Transaction/BSV::Transaction::Tx/g`.
|
|
43
|
+
- **`BSV::Wallet::WalletWireTransceiver` pubkey results are now 66-char hex** (#798),
|
|
44
|
+
matching the BRC-100 `PubKeyHex` contract and `ProtoWallet`'s direct-call shape.
|
|
45
|
+
Affects `get_public_key`, `reveal_counterparty_key_linkage`, `reveal_specific_key_linkage`.
|
|
46
|
+
Wire bytes remain 33-byte compressed binary; only the deserialised Ruby return shape
|
|
47
|
+
changed. See ADR-001 for the rationale.
|
|
48
|
+
- **`BSV::Identity::Client` and `BSV::Registry::Client` raise `BSV::Overlay::OverlayError`
|
|
49
|
+
subclasses on overlay broadcast failure** (#802) — `publicly_reveal_attributes`,
|
|
50
|
+
`revoke_certificate_revelation`, `register_definition`, `sign_and_broadcast` previously
|
|
51
|
+
returned a result hash and required callers to inspect `status` themselves.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- `BSV::Wallet::ProtoWallet::KeyDeriver#identity_key_bytes` — binary accessor for the
|
|
55
|
+
identity pubkey, alongside the hex `identity_key`. Mirrors the planned bsv-wallet
|
|
56
|
+
KeyDeriver shape.
|
|
57
|
+
- `BSV::Overlay::OverlayBroadcastResult#success?` and `#raise_if_error!` — let callers
|
|
58
|
+
treat broadcast failure as an exception with the appropriate `OverlayError` subclass.
|
|
59
|
+
- ADR-001 documenting the SDK's binary-internal rule and the public-key hex exception
|
|
60
|
+
(`docs/guides/wtxid-dtxid.md` extension + `.architecture/decisions/adrs/ADR-001-…`).
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- `BSV::Network::Util.resolve_tx_hex` rejects empty input rather than silently producing
|
|
64
|
+
an empty `rawTx` broadcast (#799).
|
|
65
|
+
- `BSV::Network::Protocol#default_call` converts transport-level exceptions
|
|
66
|
+
(`SocketError`, `Errno::ECONNREFUSED`, `Net::OpenTimeout`, `OpenSSL::SSL::SSLError`,
|
|
67
|
+
etc.) into structured `ProtocolResponse` with `http_success: false` and descriptive
|
|
68
|
+
`error_message`, rather than propagating raw exceptions to callers (#807).
|
|
69
|
+
- `BSV::Auth::AuthPayload.serialize_request` raises `ArgumentError` for `request_nonce`
|
|
70
|
+
that isn't exactly 32 bytes, preventing malformed BRC-103 wire frames (#800).
|
|
71
|
+
- `BSV::MCP::Tools::BroadcastP2pkh` rescues `StandardError` rather than just
|
|
72
|
+
`ArgumentError`, so non-`ArgumentError` exceptions from invalid WIF input (e.g.
|
|
73
|
+
`NoMethodError` on `nil`) return a structured MCP error instead of crashing the tool
|
|
74
|
+
handler (#801).
|
|
75
|
+
- `BSV::Wallet::ProtoWallet#create_signature` / `#verify_signature` and matching wire
|
|
76
|
+
serialisers validate that `hash_to_directly_sign` / `hash_to_directly_verify` are
|
|
77
|
+
exactly 32 bytes, preventing malformed signatures (#805).
|
|
78
|
+
- `BSV::Attest.verify` asserts that the provider's `fetch_transaction` returns a Tx-like
|
|
79
|
+
object before iterating, producing a clear `ArgumentError` instead of a low-signal
|
|
80
|
+
`NoMethodError` (#803).
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
- Bitcoin Core script-vectors spec now enforces a bidirectional regression gate:
|
|
84
|
+
previously-failing vectors that now pass surface as failures so `known_failures.json`
|
|
85
|
+
stays honest. Cleaned out 101 stale entries (#806).
|
|
86
|
+
|
|
8
87
|
## 0.23.1 — 2026-06-06
|
|
9
88
|
|
|
10
89
|
### Fixed
|
data/README.md
CHANGED
|
@@ -86,7 +86,7 @@ pubkey_hash = priv_key.public_key.hash160
|
|
|
86
86
|
locking_script = BSV::Script::Script.p2pkh_lock(pubkey_hash)
|
|
87
87
|
|
|
88
88
|
# Create a transaction spending a UTXO
|
|
89
|
-
tx = BSV::Transaction::
|
|
89
|
+
tx = BSV::Transaction::Tx.new
|
|
90
90
|
|
|
91
91
|
# Add an input referencing a previous transaction output
|
|
92
92
|
input = BSV::Transaction::TransactionInput.new(
|
|
@@ -34,6 +34,11 @@ module BSV
|
|
|
34
34
|
# @param body [String, nil] request body bytes; nil encodes as ABSENT
|
|
35
35
|
# @return [String] binary payload (ASCII-8BIT encoding)
|
|
36
36
|
def serialize_request(request_nonce:, method:, path:, query:, headers:, body:)
|
|
37
|
+
unless request_nonce.is_a?(String) && request_nonce.bytesize == 32
|
|
38
|
+
got = request_nonce.respond_to?(:bytesize) ? "#{request_nonce.class}/#{request_nonce.bytesize}" : request_nonce.class
|
|
39
|
+
raise ArgumentError, "request_nonce must be a 32-byte binary string, got #{got}"
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
buf = ''.b
|
|
38
43
|
|
|
39
44
|
# 32-byte request nonce — written raw, no length prefix
|
data/lib/bsv/identity/client.rb
CHANGED
|
@@ -107,6 +107,8 @@ module BSV
|
|
|
107
107
|
# @return [BSV::Overlay::OverlayBroadcastResult]
|
|
108
108
|
# @raise [ArgumentError] if the certificate has no fields or fields_to_reveal is empty
|
|
109
109
|
# @raise [RuntimeError] if certificate verification fails or create_action returns no tx
|
|
110
|
+
# @raise [BSV::Overlay::OverlayError] (or a subclass) if the overlay broadcast fails —
|
|
111
|
+
# see {BSV::Overlay::OverlayBroadcastResult#raise_if_error!} for the mapping
|
|
110
112
|
def publicly_reveal_attributes(certificate, fields_to_reveal:)
|
|
111
113
|
fields = certificate[:fields] || certificate['fields'] || {}
|
|
112
114
|
raise ArgumentError, 'Public reveal failed: Certificate has no fields to reveal!' if fields.empty?
|
|
@@ -169,8 +171,8 @@ module BSV
|
|
|
169
171
|
|
|
170
172
|
raise 'Public reveal failed: failed to create action!' if create_result[:tx].nil?
|
|
171
173
|
|
|
172
|
-
tx = BSV::Transaction::
|
|
173
|
-
broadcaster_for_action.broadcast(tx)
|
|
174
|
+
tx = BSV::Transaction::Tx.from_beef(create_result[:tx])
|
|
175
|
+
broadcaster_for_action.broadcast(tx).raise_if_error!
|
|
174
176
|
end
|
|
175
177
|
|
|
176
178
|
# Revokes a publicly revealed certificate by spending the identity token.
|
|
@@ -182,6 +184,8 @@ module BSV
|
|
|
182
184
|
# @param serial_number [String] Base64 serial number of the certificate revelation to revoke
|
|
183
185
|
# @return [void]
|
|
184
186
|
# @raise [RuntimeError] if the revelation cannot be found or the transaction cannot be created
|
|
187
|
+
# @raise [BSV::Overlay::OverlayError] (or a subclass) if the overlay broadcast fails —
|
|
188
|
+
# see {BSV::Overlay::OverlayBroadcastResult#raise_if_error!} for the mapping
|
|
185
189
|
def revoke_certificate_revelation(serial_number)
|
|
186
190
|
question = BSV::Overlay::LookupQuestion.new(
|
|
187
191
|
service: Constants::SERVICE,
|
|
@@ -232,7 +236,7 @@ module BSV
|
|
|
232
236
|
raise 'Revoke failed: failed to create signable transaction' if create_result[:signable_transaction].nil?
|
|
233
237
|
|
|
234
238
|
signable = create_result[:signable_transaction]
|
|
235
|
-
partial_tx = BSV::Transaction::
|
|
239
|
+
partial_tx = BSV::Transaction::Tx.from_beef(signable[:tx])
|
|
236
240
|
|
|
237
241
|
# Unlock via PushDrop
|
|
238
242
|
template = BSV::Script::PushDropTemplate.new(wallet: @wallet, originator: @originator)
|
|
@@ -255,8 +259,8 @@ module BSV
|
|
|
255
259
|
|
|
256
260
|
raise 'Revoke failed: failed to sign transaction' if sign_result[:tx].nil?
|
|
257
261
|
|
|
258
|
-
signed_tx = BSV::Transaction::
|
|
259
|
-
broadcaster_for_action.broadcast(signed_tx)
|
|
262
|
+
signed_tx = BSV::Transaction::Tx.from_beef(sign_result[:tx])
|
|
263
|
+
broadcaster_for_action.broadcast(signed_tx).raise_if_error!
|
|
260
264
|
end
|
|
261
265
|
|
|
262
266
|
private
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module KVStore
|
|
5
|
+
# An immutable KVStore entry returned from {Global#get}.
|
|
6
|
+
#
|
|
7
|
+
# +token+ is populated only when +include_token: true+ is passed to +#get+.
|
|
8
|
+
# +history+ is populated only when +history: true+ is passed to +#get+.
|
|
9
|
+
Entry = Data.define(:key, :value, :controller, :protocol_id, :tags, :token, :history) do
|
|
10
|
+
def initialize(key:, value:, controller:, protocol_id:, tags:, token: nil, history: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module KVStore
|
|
7
|
+
# Read-only client for the global KVStore overlay service.
|
|
8
|
+
#
|
|
9
|
+
# Queries the +ls_kvstore+ lookup service via a {BSV::Overlay::LookupResolver},
|
|
10
|
+
# decodes PushDrop tokens (5-field legacy and 6-field current formats), verifies
|
|
11
|
+
# the token signature, and returns typed {Entry} objects.
|
|
12
|
+
#
|
|
13
|
+
# Set/remove operations require wallet writes and are out of scope — those live
|
|
14
|
+
# in the +bsv-wallet+ gem.
|
|
15
|
+
#
|
|
16
|
+
# == Selector requirement
|
|
17
|
+
#
|
|
18
|
+
# {#get} raises +ArgumentError+ unless at least one of +key+, +controller+,
|
|
19
|
+
# +protocol_id+, or a non-empty +tags+ array is supplied in the query.
|
|
20
|
+
#
|
|
21
|
+
# == Always returns Array
|
|
22
|
+
#
|
|
23
|
+
# {#get} always returns +Array<Entry>+, even for single-result selectors such
|
|
24
|
+
# as +key + controller+. Callers that expect at most one result should use
|
|
25
|
+
# +.first+.
|
|
26
|
+
#
|
|
27
|
+
# == Silent skipping
|
|
28
|
+
#
|
|
29
|
+
# Outputs that fail BEEF parsing, PushDrop decoding, field-count validation, or
|
|
30
|
+
# signature verification are silently skipped (matching the TS SDK contract).
|
|
31
|
+
class Global
|
|
32
|
+
# @param network_preset [Symbol] :mainnet, :testnet, or :local
|
|
33
|
+
# @param lookup_resolver [BSV::Overlay::LookupResolver, nil] injectable resolver
|
|
34
|
+
# @param service_name [String] lookup service identifier
|
|
35
|
+
# @param proto_wallet [BSV::Wallet::ProtoWallet, nil] injectable wallet used for
|
|
36
|
+
# signature verification; defaults to +ProtoWallet.new('anyone')+. Tests can
|
|
37
|
+
# substitute a double to avoid +allow_any_instance_of+.
|
|
38
|
+
def initialize(network_preset: :mainnet, lookup_resolver: nil, service_name: 'ls_kvstore', proto_wallet: nil)
|
|
39
|
+
@lookup_resolver = lookup_resolver || BSV::Overlay::LookupResolver.new(network_preset: network_preset)
|
|
40
|
+
@service_name = service_name
|
|
41
|
+
@proto_wallet = proto_wallet || BSV::Wallet::ProtoWallet.new('anyone')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Query the overlay for KVStore entries.
|
|
45
|
+
#
|
|
46
|
+
# @param query [Hash] selector: +:key+, +:controller+, +:protocol_id+, +:tags+,
|
|
47
|
+
# +:tag_query_mode+. At least one of the first four must be present.
|
|
48
|
+
# @param include_token [Boolean] populate {Token} on each entry when true
|
|
49
|
+
# @param history [Boolean] populate history via {BSV::Overlay::Historian} when true
|
|
50
|
+
# @return [Array<Entry>] matching entries (empty array when nothing matches)
|
|
51
|
+
# @raise [ArgumentError] if no selector is provided
|
|
52
|
+
def get(query, include_token: false, history: false)
|
|
53
|
+
validate_selector!(query)
|
|
54
|
+
|
|
55
|
+
question = BSV::Overlay::LookupQuestion.new(
|
|
56
|
+
service: @service_name,
|
|
57
|
+
query: camelise(query)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
answer = @lookup_resolver.query(question)
|
|
61
|
+
return [] unless answer.type == 'output-list'
|
|
62
|
+
|
|
63
|
+
answer.outputs.filter_map do |output|
|
|
64
|
+
build_entry(output, include_token: include_token, history: history)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# ---- Selector validation ----
|
|
73
|
+
|
|
74
|
+
def validate_selector!(query)
|
|
75
|
+
raise ArgumentError, 'query must be a Hash' unless query.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
has_key = query[:key].is_a?(String) && !query[:key].empty?
|
|
78
|
+
has_controller = query[:controller].is_a?(String) && !query[:controller].empty?
|
|
79
|
+
has_protocol = query[:protocol_id].is_a?(Array) && query[:protocol_id].length == 2
|
|
80
|
+
has_tags = query[:tags].is_a?(Array) && !query[:tags].empty?
|
|
81
|
+
|
|
82
|
+
return if has_key || has_controller || has_protocol || has_tags
|
|
83
|
+
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
'At least one selector required (key, controller, protocol_id, or non-empty tags)'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ---- Entry construction ----
|
|
89
|
+
|
|
90
|
+
def build_entry(output, include_token:, history:)
|
|
91
|
+
beef_data = output['beef'] || output[:beef]
|
|
92
|
+
output_index = (output['outputIndex'] || output[:output_index] || 0).to_i
|
|
93
|
+
raise 'Negative outputIndex' if output_index.negative?
|
|
94
|
+
|
|
95
|
+
beef = parse_beef!(beef_data)
|
|
96
|
+
beef_tx = beef.transactions.last
|
|
97
|
+
raise 'No transaction in BEEF' if beef_tx.nil? || beef_tx.is_a?(BSV::Transaction::Beef::TxidOnlyEntry)
|
|
98
|
+
|
|
99
|
+
tx = beef_tx.transaction
|
|
100
|
+
txout = tx.outputs[output_index]
|
|
101
|
+
raise 'outputIndex out of range' if txout.nil?
|
|
102
|
+
|
|
103
|
+
locking_script = txout.locking_script
|
|
104
|
+
raise 'No locking script' unless locking_script&.pushdrop?
|
|
105
|
+
|
|
106
|
+
raw_fields = locking_script.pushdrop_fields
|
|
107
|
+
raise 'Not a PushDrop script' unless raw_fields
|
|
108
|
+
|
|
109
|
+
field_count = raw_fields.length
|
|
110
|
+
raise "Unexpected field count: #{field_count}" unless [5, 6].include?(field_count)
|
|
111
|
+
|
|
112
|
+
# Separate data fields from trailing signature field
|
|
113
|
+
data_fields = raw_fields[0...-1]
|
|
114
|
+
sig_bytes = raw_fields.last
|
|
115
|
+
|
|
116
|
+
# Decode common fields (indices match kvProtocol in TS SDK)
|
|
117
|
+
protocol_id = decode_protocol_id!(data_fields[0])
|
|
118
|
+
key = decode_utf8!(data_fields[1])
|
|
119
|
+
value = decode_utf8!(data_fields[2])
|
|
120
|
+
controller = data_fields[3].unpack1('H*')
|
|
121
|
+
|
|
122
|
+
# New format (6 fields) has tags at index 4; old format (5 fields) has no tags field
|
|
123
|
+
tags = if field_count == 6
|
|
124
|
+
begin
|
|
125
|
+
JSON.parse(decode_utf8!(data_fields[4]))
|
|
126
|
+
rescue StandardError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
verify_signature!(data_fields, sig_bytes, protocol_id, key, controller)
|
|
132
|
+
|
|
133
|
+
token = if include_token
|
|
134
|
+
Token.new(
|
|
135
|
+
dtxid: tx.dtxid_hex,
|
|
136
|
+
output_index: output_index,
|
|
137
|
+
beef: beef,
|
|
138
|
+
satoshis: txout.satoshis || 0
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
entry_history = if history
|
|
143
|
+
BSV::Overlay::Historian
|
|
144
|
+
.new(BSV::KVStore::Interpreter)
|
|
145
|
+
.build_history(tx, { key: key, protocol_id: protocol_id })
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Entry.new(
|
|
149
|
+
key: key,
|
|
150
|
+
value: value,
|
|
151
|
+
controller: controller,
|
|
152
|
+
protocol_id: protocol_id,
|
|
153
|
+
tags: tags,
|
|
154
|
+
token: token,
|
|
155
|
+
history: entry_history
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ---- Field decoding helpers ----
|
|
160
|
+
|
|
161
|
+
def parse_beef!(beef_data)
|
|
162
|
+
case beef_data
|
|
163
|
+
when String
|
|
164
|
+
BSV::Transaction::Beef.from_binary(beef_data)
|
|
165
|
+
when Array
|
|
166
|
+
BSV::Transaction::Beef.from_binary(beef_data.pack('C*'))
|
|
167
|
+
else
|
|
168
|
+
raise 'Missing or unsupported BEEF data'
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def decode_utf8!(bytes)
|
|
173
|
+
raise 'Nil field' if bytes.nil?
|
|
174
|
+
|
|
175
|
+
str = bytes.force_encoding(Encoding::UTF_8)
|
|
176
|
+
raise 'Invalid UTF-8' unless str.valid_encoding?
|
|
177
|
+
|
|
178
|
+
str
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def decode_protocol_id!(bytes)
|
|
182
|
+
JSON.parse(decode_utf8!(bytes))
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# ---- Signature verification ----
|
|
186
|
+
|
|
187
|
+
def verify_signature!(data_fields, sig_bytes, protocol_id, key, controller)
|
|
188
|
+
@proto_wallet.verify_signature(
|
|
189
|
+
data: data_fields.join.bytes,
|
|
190
|
+
signature: sig_bytes.bytes,
|
|
191
|
+
protocol_id: protocol_id,
|
|
192
|
+
key_id: key,
|
|
193
|
+
counterparty: controller
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# ---- Query key camelisation ----
|
|
198
|
+
|
|
199
|
+
def camelise(query)
|
|
200
|
+
query.transform_keys do |k|
|
|
201
|
+
case k
|
|
202
|
+
when :protocol_id then :protocolID
|
|
203
|
+
when :tag_query_mode then :tagQueryMode
|
|
204
|
+
else k
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module KVStore
|
|
7
|
+
# Historian interpreter for KVStore PushDrop tokens.
|
|
8
|
+
#
|
|
9
|
+
# Decodes a PushDrop locking script from the specified output, validates it
|
|
10
|
+
# has the expected field count (5 old format / 6 new format), and filters by
|
|
11
|
+
# +ctx[:key]+ and +ctx[:protocol_id]+.
|
|
12
|
+
#
|
|
13
|
+
# Returns the value as a UTF-8 string, or +nil+ for any non-match or error.
|
|
14
|
+
# Never raises.
|
|
15
|
+
#
|
|
16
|
+
# Field layout (old format — 5 fields):
|
|
17
|
+
# [0] protocolID (JSON-encoded array)
|
|
18
|
+
# [1] key
|
|
19
|
+
# [2] value
|
|
20
|
+
# [3] controller
|
|
21
|
+
# [4] signature
|
|
22
|
+
#
|
|
23
|
+
# Field layout (new format — 6 fields):
|
|
24
|
+
# [0] protocolID (JSON-encoded array)
|
|
25
|
+
# [1] key
|
|
26
|
+
# [2] value
|
|
27
|
+
# [3] controller
|
|
28
|
+
# [4] tags
|
|
29
|
+
# [5] signature
|
|
30
|
+
module Interpreter
|
|
31
|
+
PROTOCOL_ID = 0
|
|
32
|
+
KEY = 1
|
|
33
|
+
VALUE = 2
|
|
34
|
+
CONTROLLER = 3
|
|
35
|
+
TAGS_NEW = 4
|
|
36
|
+
SIGNATURE_OLD = 4
|
|
37
|
+
SIGNATURE_NEW = 5
|
|
38
|
+
|
|
39
|
+
KV_PROTOCOL_FIELDS_OLD = 5
|
|
40
|
+
KV_PROTOCOL_FIELDS_NEW = 6
|
|
41
|
+
|
|
42
|
+
# Decode the KVStore token at +output_index+ in +tx+.
|
|
43
|
+
#
|
|
44
|
+
# Implements the Historian interpreter contract:
|
|
45
|
+
# +interpreter.call(tx, output_index, ctx)+ → String or nil.
|
|
46
|
+
#
|
|
47
|
+
# @param tx [Transaction::Tx, nil] the transaction to inspect
|
|
48
|
+
# @param output_index [Integer] index into +tx.outputs+
|
|
49
|
+
# @param ctx [Hash, nil] must contain +:key+ (String) and +:protocol_id+ (Array)
|
|
50
|
+
# @return [String, nil] the decoded UTF-8 value, or nil on any mismatch/error
|
|
51
|
+
def self.call(tx, output_index, ctx)
|
|
52
|
+
return nil if tx.nil?
|
|
53
|
+
return nil if ctx.nil? || ctx[:key].nil?
|
|
54
|
+
|
|
55
|
+
output = tx.outputs[output_index]
|
|
56
|
+
return nil if output.nil?
|
|
57
|
+
|
|
58
|
+
locking_script = output.locking_script
|
|
59
|
+
return nil if locking_script.nil?
|
|
60
|
+
|
|
61
|
+
return nil unless locking_script.pushdrop?
|
|
62
|
+
|
|
63
|
+
fields = locking_script.pushdrop_fields
|
|
64
|
+
return nil unless fields
|
|
65
|
+
|
|
66
|
+
field_count = fields.length
|
|
67
|
+
return nil unless [KV_PROTOCOL_FIELDS_OLD, KV_PROTOCOL_FIELDS_NEW].include?(field_count)
|
|
68
|
+
|
|
69
|
+
protocol_id_bytes = fields[PROTOCOL_ID]
|
|
70
|
+
return nil if protocol_id_bytes.nil?
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
protocol_id_str = protocol_id_bytes.force_encoding(Encoding::UTF_8)
|
|
74
|
+
return nil unless protocol_id_str.valid_encoding?
|
|
75
|
+
|
|
76
|
+
parsed_protocol_id = JSON.parse(protocol_id_str)
|
|
77
|
+
return nil unless parsed_protocol_id == ctx[:protocol_id]
|
|
78
|
+
rescue JSON::ParserError
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
key_bytes = fields[KEY]
|
|
83
|
+
return nil if key_bytes.nil?
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
key_str = key_bytes.force_encoding(Encoding::UTF_8)
|
|
87
|
+
return nil unless key_str.valid_encoding?
|
|
88
|
+
return nil unless key_str == ctx[:key]
|
|
89
|
+
rescue StandardError
|
|
90
|
+
return nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
value_bytes = fields[VALUE]
|
|
95
|
+
return nil if value_bytes.nil?
|
|
96
|
+
|
|
97
|
+
value_str = value_bytes.force_encoding(Encoding::UTF_8)
|
|
98
|
+
return nil unless value_str.valid_encoding?
|
|
99
|
+
|
|
100
|
+
value_str
|
|
101
|
+
rescue StandardError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module KVStore
|
|
5
|
+
# Transaction output reference for a KVStore token.
|
|
6
|
+
#
|
|
7
|
+
# Populated when +include_token: true+ is passed to {Global#get}.
|
|
8
|
+
Token = Data.define(:dtxid, :output_index, :beef, :satoshis)
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/bsv/kv_store.rb
ADDED
|
@@ -127,7 +127,10 @@ module BSV
|
|
|
127
127
|
[::MCP::Content::Text.new(result.to_json)],
|
|
128
128
|
structured_content: result
|
|
129
129
|
)
|
|
130
|
-
rescue
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
# Catch broadly so MCP returns a structured error rather than
|
|
132
|
+
# crashing the tool handler on unexpected exception types
|
|
133
|
+
# (e.g. NoMethodError on nil WIF input, encoding errors, etc.).
|
|
131
134
|
Helpers.error_response(e.message)
|
|
132
135
|
end
|
|
133
136
|
|
|
@@ -150,7 +153,7 @@ module BSV
|
|
|
150
153
|
# Build, fee-compute, and sign the transaction.
|
|
151
154
|
# @api private
|
|
152
155
|
def self.build_transaction(selected_utxos, satoshis, to_address, sender_address, private_key)
|
|
153
|
-
tx = BSV::Transaction::
|
|
156
|
+
tx = BSV::Transaction::Tx.new
|
|
154
157
|
|
|
155
158
|
# Wire inputs with EF metadata (source_satoshis + source_locking_script)
|
|
156
159
|
selected_utxos.each do |utxo|
|
|
@@ -21,7 +21,7 @@ module BSV
|
|
|
21
21
|
|
|
22
22
|
# Convert a Transaction to a hash suitable for JSON responses.
|
|
23
23
|
#
|
|
24
|
-
# @param tx [
|
|
24
|
+
# @param tx [Transaction::Tx]
|
|
25
25
|
# @return [Hash]
|
|
26
26
|
def self.transaction_to_h(tx)
|
|
27
27
|
{
|
|
@@ -33,7 +33,7 @@ module BSV
|
|
|
33
33
|
}
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# Convert a TransactionInput to a hash.
|
|
36
|
+
# Convert a Transaction::TransactionInput to a hash.
|
|
37
37
|
# @api private
|
|
38
38
|
def self.input_to_h(input, _index)
|
|
39
39
|
unlock_script = input.unlocking_script
|
|
@@ -46,7 +46,7 @@ module BSV
|
|
|
46
46
|
}
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Convert a TransactionOutput to a hash.
|
|
49
|
+
# Convert a Transaction::TransactionOutput to a hash.
|
|
50
50
|
# @api private
|
|
51
51
|
def self.output_to_h(output, index)
|
|
52
52
|
script = output.locking_script
|
data/lib/bsv/network/protocol.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'openssl'
|
|
5
6
|
require 'uri'
|
|
6
7
|
|
|
7
8
|
module BSV
|
|
@@ -179,7 +180,17 @@ module BSV
|
|
|
179
180
|
|
|
180
181
|
BSV.logger&.debug { "[Protocol] #{defn[:method].upcase} #{uri}" }
|
|
181
182
|
|
|
182
|
-
|
|
183
|
+
begin
|
|
184
|
+
response = execute(uri, request)
|
|
185
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
|
|
186
|
+
Errno::EHOSTUNREACH, Errno::ENETUNREACH,
|
|
187
|
+
Net::OpenTimeout, Net::ReadTimeout,
|
|
188
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
|
189
|
+
OpenSSL::SSL::SSLError => e
|
|
190
|
+
BSV.logger&.debug { "[Protocol] transport error: #{e.class}: #{e.message}" }
|
|
191
|
+
return ProtocolResponse.new(nil, http_success: false,
|
|
192
|
+
error_message: "transport error: #{e.class}: #{e.message}")
|
|
193
|
+
end
|
|
183
194
|
|
|
184
195
|
build_response(response, defn[:response])
|
|
185
196
|
end
|
data/lib/bsv/network/util.rb
CHANGED
|
@@ -25,19 +25,27 @@ module BSV
|
|
|
25
25
|
# length and contains only hex characters. This handles hex strings
|
|
26
26
|
# tagged as ASCII-8BIT (e.g. read from IO in binary mode).
|
|
27
27
|
#
|
|
28
|
+
# Empty input is rejected — an empty transaction cannot meaningfully
|
|
29
|
+
# be broadcast and any downstream `rawTx: ''` body is a sign of a
|
|
30
|
+
# caller bug, not valid traffic.
|
|
31
|
+
#
|
|
28
32
|
# @param tx [String, #to_ef_hex, #to_hex] transaction in any supported form
|
|
29
33
|
# @return [String] hex-encoded transaction
|
|
34
|
+
# @raise [ArgumentError] if `tx` is an empty string
|
|
30
35
|
def self.resolve_tx_hex(tx)
|
|
31
36
|
if tx.is_a?(String)
|
|
32
|
-
|
|
37
|
+
raise ArgumentError, 'tx cannot be empty — refusing to construct an empty broadcast' if tx.empty?
|
|
38
|
+
return tx if tx.match?(/\A[0-9a-fA-F]+\z/) && tx.length.even?
|
|
33
39
|
|
|
34
40
|
return tx.unpack1('H*')
|
|
35
41
|
end
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
begin
|
|
44
|
+
tx.to_ef_hex
|
|
45
|
+
rescue ArgumentError => e
|
|
46
|
+
BSV.logger&.debug { "[Network::Util] EF serialisation failed: #{e.message} — falling back to raw hex" }
|
|
47
|
+
tx.to_hex
|
|
48
|
+
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -80,7 +80,7 @@ module BSV
|
|
|
80
80
|
# Computes the BIP-143 sighash (SIGHASH_ALL|FORK_ID) and signs it
|
|
81
81
|
# using the wallet's derived key for the protocol.
|
|
82
82
|
#
|
|
83
|
-
# @param tx [
|
|
83
|
+
# @param tx [Transaction::Tx] the spending transaction
|
|
84
84
|
# @param input_index [Integer] which input to sign
|
|
85
85
|
# @return [BSV::Script::Script] the unlocking script
|
|
86
86
|
def sign(tx, input_index)
|
|
@@ -101,7 +101,7 @@ module BSV
|
|
|
101
101
|
|
|
102
102
|
# Estimated byte length of the unlocking script.
|
|
103
103
|
#
|
|
104
|
-
# @param _tx [
|
|
104
|
+
# @param _tx [Transaction::Tx] unused
|
|
105
105
|
# @param _input_index [Integer] unused
|
|
106
106
|
# @return [Integer]
|
|
107
107
|
def estimated_length(_tx, _input_index)
|