bsv-sdk 0.19.1 → 0.22.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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/README.md +2 -2
  4. data/lib/bsv/auth/transport.rb +1 -1
  5. data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +5 -3
  6. data/lib/bsv/network/protocol.rb +4 -5
  7. data/lib/bsv/network/protocols/arc.rb +4 -30
  8. data/lib/bsv/network/protocols/arcade.rb +163 -0
  9. data/lib/bsv/network/protocols/chaintracks.rb +6 -3
  10. data/lib/bsv/network/protocols/jungle_bus.rb +6 -0
  11. data/lib/bsv/network/protocols.rb +1 -0
  12. data/lib/bsv/network/provider.rb +7 -9
  13. data/lib/bsv/network/providers/gorilla_pool.rb +18 -18
  14. data/lib/bsv/network/util.rb +44 -0
  15. data/lib/bsv/network.rb +1 -0
  16. data/lib/bsv/overlay/lookup_resolver.rb +0 -1
  17. data/lib/bsv/overlay/topic_broadcaster.rb +0 -1
  18. data/lib/bsv/primitives/curve.rb +1 -11
  19. data/lib/bsv/primitives/ecies.rb +1 -8
  20. data/lib/bsv/primitives/hex.rb +1 -1
  21. data/lib/bsv/script/script.rb +1 -1
  22. data/lib/bsv/transaction/beef.rb +0 -2
  23. data/lib/bsv/transaction/chain_tracker.rb +74 -13
  24. data/lib/bsv/transaction/chain_trackers/whats_on_chain.rb +3 -3
  25. data/lib/bsv/transaction/chain_trackers.rb +0 -10
  26. data/lib/bsv/transaction/fee_models/live_policy.rb +10 -8
  27. data/lib/bsv/transaction/merkle_path.rb +0 -2
  28. data/lib/bsv/version.rb +1 -1
  29. data/lib/bsv/wallet/errors.rb +65 -21
  30. data/lib/bsv/wallet/proto_wallet/validators.rb +7 -49
  31. data/lib/bsv/wallet/proto_wallet.rb +15 -8
  32. data/lib/bsv/wallet/serializer/abort_action.rb +38 -0
  33. data/lib/bsv/wallet/serializer/acquire_certificate.rb +171 -0
  34. data/lib/bsv/wallet/serializer/certificate.rb +184 -0
  35. data/lib/bsv/wallet/serializer/common.rb +207 -0
  36. data/lib/bsv/wallet/serializer/create_action_args.rb +259 -0
  37. data/lib/bsv/wallet/serializer/create_action_result.rb +85 -0
  38. data/lib/bsv/wallet/serializer/create_hmac.rb +67 -0
  39. data/lib/bsv/wallet/serializer/create_signature.rb +90 -0
  40. data/lib/bsv/wallet/serializer/decrypt.rb +60 -0
  41. data/lib/bsv/wallet/serializer/discover_by_attributes.rb +61 -0
  42. data/lib/bsv/wallet/serializer/discover_by_identity_key.rb +49 -0
  43. data/lib/bsv/wallet/serializer/discover_certificates_result.rb +39 -0
  44. data/lib/bsv/wallet/serializer/encrypt.rb +60 -0
  45. data/lib/bsv/wallet/serializer/get_header_for_height.rb +71 -0
  46. data/lib/bsv/wallet/serializer/get_height.rb +46 -0
  47. data/lib/bsv/wallet/serializer/get_network.rb +65 -0
  48. data/lib/bsv/wallet/serializer/get_public_key.rb +86 -0
  49. data/lib/bsv/wallet/serializer/get_version.rb +44 -0
  50. data/lib/bsv/wallet/serializer/internalize_action.rb +151 -0
  51. data/lib/bsv/wallet/serializer/list_actions.rb +348 -0
  52. data/lib/bsv/wallet/serializer/list_certificates.rb +124 -0
  53. data/lib/bsv/wallet/serializer/list_outputs.rb +167 -0
  54. data/lib/bsv/wallet/serializer/prove_certificate.rb +146 -0
  55. data/lib/bsv/wallet/serializer/relinquish_certificate.rb +56 -0
  56. data/lib/bsv/wallet/serializer/relinquish_output.rb +44 -0
  57. data/lib/bsv/wallet/serializer/reveal_counterparty_key_linkage.rb +108 -0
  58. data/lib/bsv/wallet/serializer/reveal_specific_key_linkage.rb +116 -0
  59. data/lib/bsv/wallet/serializer/sign_action_args.rb +94 -0
  60. data/lib/bsv/wallet/serializer/sign_action_result.rb +49 -0
  61. data/lib/bsv/wallet/serializer/status.rb +85 -0
  62. data/lib/bsv/wallet/serializer/verify_hmac.rb +67 -0
  63. data/lib/bsv/wallet/serializer/verify_signature.rb +101 -0
  64. data/lib/bsv/wallet/serializer.rb +180 -0
  65. data/lib/bsv/wallet/substrates/http_wallet_json.rb +129 -0
  66. data/lib/bsv/wallet/substrates/http_wallet_wire.rb +99 -0
  67. data/lib/bsv/wallet/wallet_wire.rb +20 -0
  68. data/lib/bsv/wallet/wallet_wire_processor.rb +61 -0
  69. data/lib/bsv/wallet/wallet_wire_transceiver.rb +61 -0
  70. data/lib/bsv/wallet/wire/calls.rb +79 -0
  71. data/lib/bsv/wallet/wire/frame.rb +181 -0
  72. data/lib/bsv/wallet/wire/reader_writer.rb +402 -0
  73. data/lib/bsv/wallet/wire/validation.rb +213 -0
  74. data/lib/bsv/wallet/wire.rb +13 -0
  75. data/lib/bsv/wallet.rb +17 -0
  76. metadata +47 -3
  77. data/lib/bsv/transaction/chain_trackers/chaintracks.rb +0 -83
@@ -98,21 +98,11 @@ module BSV
98
98
 
99
99
  # Add two curve points together.
100
100
  #
101
- # Uses +Point#add+ where available (Ruby 3.0+ / OpenSSL 3), falling
102
- # back to multi-scalar multiplication for Ruby 2.7 compatibility.
103
- #
104
101
  # @param point_a [OpenSSL::PKey::EC::Point] first point
105
102
  # @param point_b [OpenSSL::PKey::EC::Point] second point
106
103
  # @return [OpenSSL::PKey::EC::Point] the sum of the two points
107
104
  def add_points(point_a, point_b)
108
- if point_a.respond_to?(:add)
109
- point_a.add(point_b)
110
- else
111
- # Ruby 2.7 / OpenSSL < 3: use multi-scalar mul
112
- # point_a.mul(bns, points) = bns[0]*point_a + bns[1]*points[0] + ...
113
- one = OpenSSL::BN.new('1')
114
- point_a.mul([one, one], [point_b])
115
- end
105
+ point_a.add(point_b)
116
106
  end
117
107
 
118
108
  # Extract the x-coordinate from a curve point as a big number.
@@ -210,14 +210,7 @@ module BSV
210
210
  def secure_compare(mac, expected)
211
211
  return false unless mac.bytesize == expected.bytesize
212
212
 
213
- if OpenSSL.respond_to?(:fixed_length_secure_compare)
214
- OpenSSL.fixed_length_secure_compare(mac, expected)
215
- else
216
- # Constant-time comparison for Ruby < 3.2
217
- result = 0
218
- mac.bytes.zip(expected.bytes) { |x, y| result |= x ^ y }
219
- result.zero?
220
- end
213
+ OpenSSL.fixed_length_secure_compare(mac, expected)
221
214
  end
222
215
 
223
216
  def derive_keys(private_key, public_key)
@@ -22,7 +22,7 @@ module BSV
22
22
  module Hex
23
23
  # Matches an even number of hex characters (case-insensitive).
24
24
  # Empty string is valid (decodes to empty bytes).
25
- HEX_RE = /\A(?:[0-9a-fA-F]{2})*\z/n.freeze
25
+ HEX_RE = /\A(?:[0-9a-fA-F]{2})*\z/n
26
26
  private_constant :HEX_RE
27
27
 
28
28
  # Test whether +str+ is valid hex (even-length, hex-only).
@@ -334,7 +334,7 @@ module BSV
334
334
  }.freeze
335
335
 
336
336
  # Reverse lookup: opcode → hash type symbol (excludes :raw).
337
- RPUZZLE_OP_TO_TYPE = RPUZZLE_HASH_OPS.reject { |k, _| k == :raw }.invert.freeze
337
+ RPUZZLE_OP_TO_TYPE = RPUZZLE_HASH_OPS.except(:raw).invert.freeze
338
338
 
339
339
  # The fixed opcode prefix shared by all RPuzzle locking scripts.
340
340
  # OP_OVER OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module BSV
6
4
  module Transaction
7
5
  # Background Evaluation Extended Format (BEEF) for SPV-ready transaction
@@ -10,18 +10,21 @@ module BSV
10
10
  # chain tracker is the data source: an object the consumer provides
11
11
  # that can answer "is this merkle root valid for this block height?"
12
12
  #
13
- # The SDK is deliberately unopinionated about where that answer comes
14
- # from. A chain tracker backed by an in-memory hash is declarative.
15
- # One that fetches from the network on cache miss and writes to a
16
- # database is imperative. The verify methods don't care — they just
17
- # ask the question. All imperative behaviour (fetching, caching,
18
- # persisting) lives in the consumer's chain tracker implementation,
19
- # not in the SDK.
13
+ # This class is a working default implementation that wraps a
14
+ # {BSV::Network::Provider} and dispatches via {Provider#call}. The
15
+ # provider must serve the +:get_block_header+ and +:current_height+
16
+ # commands (e.g. a provider configured with {Protocols::JungleBus}).
17
+ #
18
+ # Subclasses may override either or both methods to supply their own
19
+ # data source (in-memory hash, database cache, etc.) without touching
20
+ # the provider at all. The +provider+ argument is optional precisely to
21
+ # preserve this pattern — a subclass that overrides both methods never
22
+ # reaches the provider-dispatch path.
20
23
  #
21
24
  # Any object responding to +valid_root_for_height?+ and
22
25
  # +current_height+ satisfies this interface. Inheriting from this
23
26
  # class is optional — it exists to document the contract and provide
24
- # clear error messages when methods are missing.
27
+ # a ready-to-use provider-backed implementation.
25
28
  #
26
29
  # @example In-memory chain tracker (test / declarative)
27
30
  # class HashTracker < BSV::Transaction::ChainTracker
@@ -38,23 +41,81 @@ module BSV
38
41
  # header.merkle_root == root
39
42
  # end
40
43
  # end
44
+ #
45
+ # @example Provider-backed (default impl)
46
+ # tracker = BSV::Transaction::ChainTracker.default
47
+ # tracker.valid_root_for_height?('abcd...', 800_000)
48
+ #
49
+ # @example Testnet
50
+ # tracker = BSV::Transaction::ChainTracker.default(testnet: true)
51
+ # tracker.current_height
41
52
  class ChainTracker
53
+ # @return [BSV::Network::Provider, nil] the underlying provider, if any
54
+ attr_reader :provider
55
+
56
+ # Return a default ChainTracker backed by the GorillaPool provider.
57
+ #
58
+ # @param testnet [Boolean] when true, uses the testnet provider
59
+ # @return [ChainTracker]
60
+ def self.default(testnet: false)
61
+ new(BSV::Network::Providers::GorillaPool.default(testnet: testnet))
62
+ end
63
+
64
+ # @param provider [BSV::Network::Provider, nil] provider serving +:get_block_header+
65
+ # and +:current_height+. Optional when the subclass overrides both methods.
66
+ def initialize(provider = nil)
67
+ @provider = provider
68
+ end
69
+
42
70
  # Verify that a merkle root is valid for the given block height.
43
71
  #
72
+ # Dispatches +:get_block_header+ to the configured provider. Returns +false+
73
+ # on 404 (block not found). Normalises the merkle root field name from any
74
+ # of +merkleroot+, +merkleRoot+, or +merkle_root+.
75
+ #
44
76
  # @param root [String] merkle root as a hex string
45
77
  # @param height [Integer] block height
46
78
  # @return [Boolean] true if the root matches the block at the given height
47
- # @raise [NotImplementedError] if not overridden by a subclass
48
- def valid_root_for_height?(_root, _height)
49
- raise NotImplementedError, "#{self.class}#valid_root_for_height? must be implemented"
79
+ # @raise [RuntimeError] when no provider is configured
80
+ # @raise [RuntimeError] on network or API error
81
+ def valid_root_for_height?(root, height)
82
+ raise 'ChainTracker requires a provider when used directly' if @provider.nil?
83
+
84
+ result = @provider.call(:get_block_header, height)
85
+ return false if result.http_not_found?
86
+ raise result.error_message.to_s unless result.http_success?
87
+
88
+ actual = normalise_merkle_root(result.data)
89
+ return false unless actual
90
+
91
+ actual.casecmp(root).zero?
50
92
  end
51
93
 
52
94
  # Return the current blockchain height.
53
95
  #
96
+ # Dispatches +:current_height+ to the configured provider.
97
+ #
54
98
  # @return [Integer] the height of the chain tip
55
- # @raise [NotImplementedError] if not overridden by a subclass
99
+ # @raise [RuntimeError] when no provider is configured
100
+ # @raise [RuntimeError] on network or API error
56
101
  def current_height
57
- raise NotImplementedError, "#{self.class}#current_height must be implemented"
102
+ raise 'ChainTracker requires a provider when used directly' if @provider.nil?
103
+
104
+ result = @provider.call(:current_height)
105
+ return result.data if result.http_success?
106
+
107
+ raise result.error_message.to_s
108
+ end
109
+
110
+ private
111
+
112
+ # Field-name diversity belongs at the Protocols::* layer; this is a
113
+ # transitional shim until the wire protocols return canonical shapes.
114
+ # See #791.
115
+ def normalise_merkle_root(body)
116
+ return nil unless body.is_a?(Hash)
117
+
118
+ body['merkleroot'] || body['merkleRoot'] || body['merkle_root']
58
119
  end
59
120
  end
60
121
  end
@@ -20,10 +20,10 @@ module BSV
20
20
  # Returns a WhatsOnChain chain tracker using the provider default.
21
21
  #
22
22
  # @param testnet [Boolean] when true, uses the testnet endpoint
23
- # @param opts [Hash] forwarded to the underlying protocol (e.g. +api_key:+, +http_client:+)
23
+ # @param ** [Hash] forwarded to the underlying protocol (e.g. +api_key:+, +http_client:+)
24
24
  # @return [WhatsOnChain]
25
- def self.default(testnet: false, **opts)
26
- provider = BSV::Network::Providers::WhatsOnChain.default(testnet: testnet, **opts)
25
+ def self.default(testnet: false, **)
26
+ provider = BSV::Network::Providers::WhatsOnChain.default(testnet: testnet, **)
27
27
  new(protocol: provider.protocol_for(:valid_root))
28
28
  end
29
29
 
@@ -5,16 +5,6 @@ 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 opts [Hash] forwarded to the underlying tracker (e.g. +api_key:+)
14
- # @return [Chaintracks]
15
- def self.default(testnet: false, **opts)
16
- Chaintracks.default(testnet: testnet, **opts)
17
- end
18
8
  end
19
9
  end
20
10
  end
@@ -16,7 +16,7 @@ module BSV
16
16
  #
17
17
  # @example
18
18
  # model = BSV::Transaction::FeeModels::LivePolicy.new(
19
- # arc_url: 'https://arcade.gorillapool.io',
19
+ # arc_url: 'https://arc.taal.com',
20
20
  # fallback_rate: 50
21
21
  # )
22
22
  # fee = model.compute_fee(transaction)
@@ -34,18 +34,20 @@ module BSV
34
34
 
35
35
  DEFAULT_FALLBACK_RATE = 100
36
36
 
37
- # Returns a LivePolicy with sensible defaults (GorillaPool ARC,
38
- # 100 sat/kB fallback, 5-minute cache).
37
+ # TAAL still runs a public ARC instance that serves /v1/policy.
38
+ # Full policy access may require a TAAL API key.
39
+ TAAL_ARC_URL = 'https://arc.taal.com'
40
+
41
+ # Returns a LivePolicy using TAAL ARC for fee policy, 100 sat/kB fallback,
42
+ # and a 5-minute cache.
39
43
  #
40
- # @param api_key [String, nil] optional ARC API key
44
+ # @param api_key [String, nil] optional TAAL API key for authenticated policy access
41
45
  # @return [LivePolicy]
42
46
  def self.default(api_key: nil)
43
- provider = BSV::Network::Providers::GorillaPool.mainnet
44
- arc_protocol = provider.protocol_for(:broadcast)
45
- new(arc_url: arc_protocol.base_url, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
47
+ new(arc_url: TAAL_ARC_URL, fallback_rate: DEFAULT_FALLBACK_RATE, api_key: api_key)
46
48
  end
47
49
 
48
- # @param arc_url [String] ARC base URL (e.g. 'https://arcade.gorillapool.io')
50
+ # @param arc_url [String] ARC base URL (e.g. 'https://arc.taal.com')
49
51
  # @param fallback_rate [Integer] sat/kB to use when fetch fails (default: 100)
50
52
  # @param cache_ttl [Integer] seconds to cache a fetched rate (default: 300)
51
53
  # @param api_key [String, nil] optional Bearer token for ARC authentication
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  module BSV
6
4
  module Transaction
7
5
  # A BRC-74 merkle path (BUMP — Bitcoin Unified Merkle Path).
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.19.1'
4
+ VERSION = '0.22.0'
5
5
  end
@@ -3,44 +3,88 @@
3
3
  module BSV
4
4
  module Wallet
5
5
  # Base error for all wallet operations. Carries a machine-readable code
6
- # per the BRC-100 error structure.
6
+ # per the BRC-100 error structure and a wire-protocol stack string.
7
7
  class Error < StandardError
8
- attr_reader :code
8
+ attr_reader :code, :wallet_stack
9
9
 
10
- def initialize(message, code = 1)
10
+ def initialize(message = nil, code: 1, stack: '')
11
11
  @code = code
12
- super(message)
12
+ @wallet_stack = stack
13
+ super(message || '')
13
14
  end
14
- end
15
15
 
16
- # Raised when a required parameter is missing or invalid.
17
- class InvalidParameterError < Error
18
- attr_reader :parameter
16
+ # Serialise to a hash suitable for embedding in a wire result frame.
17
+ # Never leaks Ruby's internal backtrace — only the wallet_stack string.
18
+ def to_wire
19
+ { code: code, message: message.to_s, stack: wallet_stack.to_s }
20
+ end
21
+ end
19
22
 
20
- def initialize(parameter, must_be = 'valid')
21
- @parameter = parameter
22
- super("the #{parameter} parameter must be #{must_be}", 6)
23
+ # Code 2 operation not supported by this wallet implementation.
24
+ class UnsupportedActionError < Error
25
+ def initialize(message = 'this method is not supported by this wallet implementation', stack: '')
26
+ super(message, code: 2, stack: stack)
23
27
  end
24
28
  end
25
29
 
26
- # Raised when an HMAC fails to verify.
30
+ # Code 3 HMAC verification failed.
27
31
  class InvalidHmacError < Error
28
- def initialize(message = 'the provided HMAC is invalid')
29
- super(message, 3)
32
+ def initialize(message = 'the provided HMAC is invalid', stack: '')
33
+ super(message, code: 3, stack: stack)
30
34
  end
31
35
  end
32
36
 
33
- # Raised when a signature fails to verify.
37
+ # Code 4 signature verification failed.
34
38
  class InvalidSignatureError < Error
35
- def initialize(message = 'the provided signature is invalid')
36
- super(message, 4)
39
+ def initialize(message = 'the provided signature is invalid', stack: '')
40
+ super(message, code: 4, stack: stack)
37
41
  end
38
42
  end
39
43
 
40
- # Raised when an operation is not supported by this wallet implementation.
41
- class UnsupportedActionError < Error
42
- def initialize(method_name = 'this method')
43
- super("#{method_name} is not supported by this wallet implementation", 2)
44
+ # Code 5 wallet has insufficient funds for the requested operation.
45
+ class InsufficientFundsError < Error
46
+ def initialize(message = 'insufficient funds', stack: '')
47
+ super(message, code: 5, stack: stack)
48
+ end
49
+ end
50
+
51
+ # Code 6 — a required parameter is missing or invalid.
52
+ #
53
+ # Two calling conventions:
54
+ # InvalidParameterError.new('pubkey', 'a hex string') # raises "the pubkey parameter must be ..."
55
+ # InvalidParameterError.new('raw message') # wire-rehydration path
56
+ class InvalidParameterError < Error
57
+ attr_reader :parameter
58
+
59
+ def initialize(parameter, must_be = nil, stack: '')
60
+ @parameter = must_be ? parameter : nil
61
+ message = must_be ? "the #{parameter} parameter must be #{must_be}" : parameter
62
+ super(message, code: 6, stack: stack)
63
+ end
64
+ end
65
+
66
+ # Code 7 — actions require review before they can be processed.
67
+ class ReviewActionsError < Error
68
+ def initialize(message = 'actions require review', stack: '')
69
+ super(message, code: 7, stack: stack)
70
+ end
71
+ end
72
+
73
+ # Rehydrate a wire error frame into the appropriate subclass.
74
+ #
75
+ # @param code [Integer] error code byte from the result frame
76
+ # @param message [String] error message from the frame
77
+ # @param stack [String] stack trace from the frame (may be empty)
78
+ # @return [Error] an instance of the matching subclass
79
+ def self.error_from_wire(code, message, stack = '')
80
+ case code
81
+ when 2 then UnsupportedActionError.new(message, stack: stack)
82
+ when 3 then InvalidHmacError.new(message, stack: stack)
83
+ when 4 then InvalidSignatureError.new(message, stack: stack)
84
+ when 5 then InsufficientFundsError.new(message, stack: stack)
85
+ when 6 then InvalidParameterError.new(message, nil, stack: stack)
86
+ when 7 then ReviewActionsError.new(message, stack: stack)
87
+ else Error.new(message, code: code, stack: stack)
44
88
  end
45
89
  end
46
90
  end
@@ -5,68 +5,26 @@ module BSV
5
5
  class ProtoWallet
6
6
  # Validation helpers for BRC-100 wallet method parameters.
7
7
  #
8
- # Provides the subset of validators required by KeyDeriver and ProtoWallet.
9
- # Raises +InvalidParameterError+ for any invalid input.
8
+ # Delegates to Wire::Validation the single source of truth for all
9
+ # BRC-100 parameter validation. This module is retained for backwards
10
+ # compatibility with ProtoWallet's internal call sites.
10
11
  module Validators
11
12
  module_function
12
13
 
13
- # Validates a BRC-43 protocol ID.
14
- #
15
- # Must be an Array of [Integer(0-2), String(5-400 chars)]. The name is
16
- # normalized (stripped and downcased) before length/content checks.
17
- #
18
- # @param protocol_id [Object] the value to validate
19
- # @raise [InvalidParameterError]
20
14
  def validate_protocol_id!(protocol_id)
21
- unless protocol_id.is_a?(Array) && protocol_id.length == 2
22
- raise InvalidParameterError.new('protocol_id', 'an Array of [security_level, protocol_name]')
23
- end
24
-
25
- level, name = protocol_id
26
- raise InvalidParameterError.new('protocol_id security level', '0, 1, or 2') unless [0, 1, 2].include?(level)
27
- raise InvalidParameterError.new('protocol_id name', 'a String') unless name.is_a?(String)
28
-
29
- name = name.strip.downcase
30
- max_length = name.start_with?('specific linkage revelation') ? 430 : 400
31
- raise InvalidParameterError.new('protocol_id name', "between 5 and #{max_length} characters") if name.length < 5 || name.length > max_length
32
-
33
- raise InvalidParameterError.new('protocol_id name', 'lowercase letters, numbers, and spaces only') unless name.match?(/\A[a-z0-9 ]+\z/)
34
-
35
- raise InvalidParameterError.new('protocol_id name', 'free of consecutive spaces') if name.include?(' ')
15
+ Wire::Validation.wallet_protocol!('protocol_id', protocol_id)
36
16
  end
37
17
 
38
- # Validates a BRC-43 key ID.
39
- #
40
- # Must be a non-empty String of at most 800 bytes.
41
- #
42
- # @param key_id [Object] the value to validate
43
- # @raise [InvalidParameterError]
44
18
  def validate_key_id!(key_id)
45
- raise InvalidParameterError.new('key_id', 'a String') unless key_id.is_a?(String)
46
-
47
- byte_length = key_id.bytesize
48
- raise InvalidParameterError.new('key_id', 'between 1 and 800 bytes') if byte_length < 1 || byte_length > 800
19
+ Wire::Validation.key_id_string_1_to_800!('key_id', key_id)
49
20
  end
50
21
 
51
- # Validates a counterparty: 'self', 'anyone', or a 66-char hex pubkey.
52
- #
53
- # @param counterparty [Object] the value to validate
54
- # @raise [InvalidParameterError]
55
22
  def validate_counterparty!(counterparty)
56
- return if %w[self anyone].include?(counterparty)
57
-
58
- validate_pub_key_hex!(counterparty, 'counterparty')
23
+ Wire::Validation.wallet_counterparty!('counterparty', counterparty)
59
24
  end
60
25
 
61
- # Validates a compressed public key in hex form (66 chars, 02/03/04 prefix).
62
- #
63
- # @param value [Object] the value to validate
64
- # @param name [String] parameter name for error messages
65
- # @raise [InvalidParameterError]
66
26
  def validate_pub_key_hex!(value, name = 'public_key')
67
- raise InvalidParameterError.new(name, 'a String') unless value.is_a?(String)
68
-
69
- raise InvalidParameterError.new(name, 'a 66-character hex string (compressed public key)') unless value.match?(/\A[0-9a-f]{66}\z/)
27
+ Wire::Validation.pub_key_hex!(name, value)
70
28
  end
71
29
  end
72
30
  end
@@ -226,6 +226,8 @@ module BSV
226
226
  def reveal_counterparty_key_linkage(counterparty:, verifier:,
227
227
  privileged: false, privileged_reason: nil,
228
228
  originator: nil)
229
+ counterparty = normalise_pubkey_hex(counterparty)
230
+ verifier = normalise_pubkey_hex(verifier)
229
231
  raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
230
232
 
231
233
  Validators.validate_pub_key_hex!(verifier, 'verifier')
@@ -281,6 +283,8 @@ module BSV
281
283
  def reveal_specific_key_linkage(counterparty:, verifier:, protocol_id:, key_id:,
282
284
  privileged: false, privileged_reason: nil,
283
285
  originator: nil)
286
+ counterparty = normalise_pubkey_hex(counterparty)
287
+ verifier = normalise_pubkey_hex(verifier)
284
288
  raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
285
289
 
286
290
  Validators.validate_pub_key_hex!(verifier, 'verifier')
@@ -340,7 +344,16 @@ module BSV
340
344
  end
341
345
 
342
346
  def bytes_to_string(bytes)
343
- bytes.pack('C*')
347
+ bytes.is_a?(String) ? bytes.b : bytes.pack('C*')
348
+ end
349
+
350
+ # Normalise a public key argument to a 66-character compressed hex string.
351
+ # Accepts either a 33-byte binary string or a 66-character hex string.
352
+ def normalise_pubkey_hex(value)
353
+ return value if value.is_a?(String) && value.length == 66
354
+ return value.unpack1('H*') if value.is_a?(String) && value.bytesize == 33
355
+
356
+ value
344
357
  end
345
358
 
346
359
  def string_to_bytes(str)
@@ -350,13 +363,7 @@ module BSV
350
363
  def secure_compare(a, b)
351
364
  return false unless a.bytesize == b.bytesize
352
365
 
353
- if OpenSSL.respond_to?(:fixed_length_secure_compare)
354
- OpenSSL.fixed_length_secure_compare(a, b)
355
- else
356
- result = 0
357
- a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
358
- result.zero?
359
- end
366
+ OpenSSL.fixed_length_secure_compare(a, b)
360
367
  end
361
368
  end
362
369
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ module Serializer
6
+ # BRC-103 wire codec for the +abort_action+ call (call byte 3).
7
+ #
8
+ # Args wire layout:
9
+ # [remaining bytes: reference (raw binary)]
10
+ #
11
+ # Result wire layout:
12
+ # [empty — success is implicit from the frame error byte]
13
+ module AbortAction
14
+ module_function
15
+
16
+ def serialize_args(args)
17
+ ref = args[:reference]
18
+ return ''.b if ref.nil? || ref.empty?
19
+
20
+ ref.b
21
+ end
22
+
23
+ def deserialize_args(bytes)
24
+ ref = bytes.b
25
+ { reference: ref.empty? ? nil : ref }
26
+ end
27
+
28
+ def serialize_result(_result)
29
+ ''.b
30
+ end
31
+
32
+ def deserialize_result(_bytes)
33
+ { aborted: true }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end