bsv-wallet 0.7.0 → 0.9.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 +63 -0
- data/lib/bsv/wallet_interface/broadcast_queue.rb +15 -0
- data/lib/bsv/wallet_interface/inline_queue.rb +37 -8
- data/lib/bsv/wallet_interface/substrates/http_wallet_json.rb +268 -0
- data/lib/bsv/wallet_interface/substrates/http_wallet_wire.rb +85 -0
- data/lib/bsv/wallet_interface/substrates/wallet_wire_transceiver.rb +168 -0
- data/lib/bsv/wallet_interface/substrates.rb +16 -0
- data/lib/bsv/wallet_interface/version.rb +2 -2
- data/lib/bsv/wallet_interface/wallet_client.rb +221 -41
- data/lib/bsv/wallet_interface.rb +3 -4
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7959ebc47fc5d89d3d0db18f4136c925e228f6ea483553decffd4b9e7de9defc
|
|
4
|
+
data.tar.gz: 2365a17193357b64d50c15a61aa12422b966b8f0155a6ab989b3bacb700a0929
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 895d3c7a661e00b7d70dd833d12e4dd0e17ca2a8d0d8ba6a2c0ef622dfbac6bc0d6fb534a3fe06c14c0b1747981f1e0c9b8b61e9f164e3cc8fea222e5f9e70c1
|
|
7
|
+
data.tar.gz: 3086ff6e236c57d903c947ac7c230148104993837b3d2f46ddef3a09a9b43da42bab1a5ea6f1c7b375b105cc7e8feffe2c8defbd83a325ec7ca0775dbd1f2ea1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,69 @@ All notable changes to the `bsv-wallet` 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.9.0 — 2026-04-16
|
|
9
|
+
|
|
10
|
+
### Changed — **Breaking**
|
|
11
|
+
|
|
12
|
+
#### Action status taxonomy aligned with BRC-100 reference SDK (HLR #455)
|
|
13
|
+
|
|
14
|
+
This release realigns the action status values with the wallet-toolbox reference
|
|
15
|
+
implementation. The meaning of `'completed'` has changed — **consumers must
|
|
16
|
+
update any code that checks for `'completed'` as the post-broadcast success
|
|
17
|
+
state**.
|
|
18
|
+
|
|
19
|
+
| Status | Old meaning | New meaning |
|
|
20
|
+
|--------|-------------|-------------|
|
|
21
|
+
| `'nosend'` | No change | Transaction built but not broadcast (`no_send: true`) |
|
|
22
|
+
| `'sending'` | No change | Async queue accepted; worker has not yet attempted broadcast |
|
|
23
|
+
| `'unproven'` | *(new)* | Broadcast succeeded; awaiting merkle proof |
|
|
24
|
+
| `'completed'` | Broadcast succeeded | **Merkle proof received and stored** |
|
|
25
|
+
| `'failed'` | No change | Broadcast rejected or transaction invalid |
|
|
26
|
+
|
|
27
|
+
**Migration:**
|
|
28
|
+
|
|
29
|
+
- Code querying `list_actions(status: 'completed')` will return fewer results
|
|
30
|
+
until a proof-watcher is implemented (out of scope for this release). To find
|
|
31
|
+
successfully-broadcast actions that have not yet received a proof, query
|
|
32
|
+
`status: 'unproven'` instead.
|
|
33
|
+
- `create_action` and `sign_action` now raise `BSV::Wallet::WalletError` when
|
|
34
|
+
no broadcaster is configured and `options: { no_send: true }` is not set.
|
|
35
|
+
Previously these calls succeeded silently. To resolve: pass
|
|
36
|
+
`broadcaster: BSV::Network::ARC.default` to `WalletClient.new`, or pass
|
|
37
|
+
`options: { no_send: true }` to `create_action` to build without
|
|
38
|
+
broadcasting.
|
|
39
|
+
- `internalize_action` now sets status to `'completed'` only when the supplied
|
|
40
|
+
BEEF contains a merkle proof for the subject transaction. Plain BEEF (raw
|
|
41
|
+
transaction, no BUMP) results in status `'unproven'`.
|
|
42
|
+
- Wallets configured with a `SolidQueueAdapter` satisfy the broadcaster
|
|
43
|
+
requirement if the adapter carries an embedded `broadcaster:` — the
|
|
44
|
+
`WalletClient` itself does not need one.
|
|
45
|
+
|
|
46
|
+
**Related upstream incidents:** x402-rack #148, x402-rack #158, x402-doom #196.
|
|
47
|
+
**Tracking issue:** #455.
|
|
48
|
+
|
|
49
|
+
## 0.8.0 — 2026-04-15
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- BRC-100 substrates: `HTTPWalletJSON` for JSON-over-HTTP, `HTTPWalletWire`
|
|
53
|
+
for binary transport, and `WalletWireTransceiver` Interface adapter (#449–#451)
|
|
54
|
+
- `WalletClient` accepts `substrate:` constructor param for remote wallet
|
|
55
|
+
delegation — all Interface methods delegate to the substrate when set (#452)
|
|
56
|
+
- `list_actions` and `list_outputs` honour `include_labels`, `include_inputs`,
|
|
57
|
+
and `include_outputs` flags (#448)
|
|
58
|
+
- `acquire_certificate` uses `AuthFetch` for BRC-104 authenticated certificate
|
|
59
|
+
issuance (#453)
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- `prove_certificate` now uses correct protocol ID (`'certificate field encryption'`)
|
|
63
|
+
and key ID format (`"#{serial_number} #{field_name}"`) matching TS/Go SDKs —
|
|
64
|
+
previously incompatible cross-SDK (#424)
|
|
65
|
+
- Code review findings addressed for substrates and include flags
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- `BSV::WalletInterface` module removed — `VERSION` now lives in `BSV::Wallet::VERSION`
|
|
69
|
+
where all other wallet constants already reside
|
|
70
|
+
|
|
8
71
|
## 0.7.0 — 2026-04-12
|
|
9
72
|
|
|
10
73
|
### Added
|
|
@@ -68,6 +68,21 @@ module BSV
|
|
|
68
68
|
false
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Returns +false+ by default — adapters without a broadcaster cannot
|
|
72
|
+
# broadcast on-chain.
|
|
73
|
+
#
|
|
74
|
+
# Override in adapters that hold a broadcaster reference so that
|
|
75
|
+
# +WalletClient+ can determine broadcast availability from the queue
|
|
76
|
+
# alone. This is the correct delegation point because users may pass a
|
|
77
|
+
# broadcaster-equipped queue (e.g.
|
|
78
|
+
# +SolidQueueAdapter.new(broadcaster: arc)+) without also passing
|
|
79
|
+
# +broadcaster:+ directly to +WalletClient+.
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def broadcast_enabled?
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
71
86
|
# Returns the broadcast status for a previously enqueued transaction.
|
|
72
87
|
#
|
|
73
88
|
# @param _txid [String] hex transaction identifier
|
|
@@ -30,6 +30,17 @@ module BSV
|
|
|
30
30
|
false
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Returns +true+ when a broadcaster has been configured.
|
|
34
|
+
#
|
|
35
|
+
# +WalletClient+ delegates its own +broadcast_enabled?+ to this method
|
|
36
|
+
# so the check works correctly when the broadcaster is embedded in the
|
|
37
|
+
# queue rather than passed directly to the wallet.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def broadcast_enabled?
|
|
41
|
+
!@broadcaster.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
33
44
|
# Returns the broadcast status for a previously enqueued transaction.
|
|
34
45
|
#
|
|
35
46
|
# Delegates to storage and returns the action status field, or +nil+ if
|
|
@@ -96,8 +107,10 @@ module BSV
|
|
|
96
107
|
}
|
|
97
108
|
end
|
|
98
109
|
|
|
99
|
-
# Broadcast succeeded — promote all pending state to
|
|
100
|
-
|
|
110
|
+
# Broadcast succeeded — promote all pending state; set status to
|
|
111
|
+
# 'unproven' (transaction is on-chain but lacks a merkle proof).
|
|
112
|
+
# 'completed' is reserved for transactions confirmed by a proof-watcher.
|
|
113
|
+
promote(input_outpoints, change_outpoints, txid, status: 'unproven')
|
|
101
114
|
|
|
102
115
|
result = {
|
|
103
116
|
txid: txid,
|
|
@@ -109,14 +122,24 @@ module BSV
|
|
|
109
122
|
result
|
|
110
123
|
end
|
|
111
124
|
|
|
112
|
-
# Promotes UTXO state without broadcasting
|
|
113
|
-
# used when no broadcaster is configured.
|
|
125
|
+
# Promotes UTXO state without broadcasting.
|
|
114
126
|
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
127
|
+
# This path is reached when no broadcaster is configured. It is only
|
|
128
|
+
# valid when +accept_delayed_broadcast+ is set on the create_action
|
|
129
|
+
# call — the caller explicitly accepts that the transaction will be
|
|
130
|
+
# broadcast out-of-band. Action status is set to +unproven+.
|
|
131
|
+
#
|
|
132
|
+
# +completed+ is reserved for transactions that have received a merkle
|
|
133
|
+
# proof (set by +internalize_action+ or a future proof-watcher).
|
|
134
|
+
#
|
|
135
|
+
# Defensive guard: raises +WalletError+ if reached without
|
|
136
|
+
# +accept_delayed_broadcast+. The normal entry point for this guard is
|
|
137
|
+
# the +create_action+ validation added in Task 1 (#456), but this guard
|
|
138
|
+
# protects against other code paths that bypass it.
|
|
117
139
|
#
|
|
118
140
|
# @param payload [Hash] broadcast payload
|
|
119
141
|
# @return [Hash] result hash containing +:txid+ and +:tx+
|
|
142
|
+
# @raise [BSV::Wallet::WalletError] if +accept_delayed_broadcast+ is not set
|
|
120
143
|
def promote_without_broadcast(payload)
|
|
121
144
|
txid = payload[:txid]
|
|
122
145
|
beef_binary = payload[:beef_binary]
|
|
@@ -124,8 +147,14 @@ module BSV
|
|
|
124
147
|
change_outpoints = payload[:change_outpoints]
|
|
125
148
|
delayed = payload[:accept_delayed_broadcast]
|
|
126
149
|
|
|
127
|
-
|
|
128
|
-
|
|
150
|
+
unless delayed
|
|
151
|
+
raise BSV::Wallet::WalletError,
|
|
152
|
+
'InlineQueue cannot promote without a broadcaster unless ' \
|
|
153
|
+
'accept_delayed_broadcast is set. This indicates a bypass of ' \
|
|
154
|
+
'the create_action guard — report as a bug.'
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
promote(input_outpoints, change_outpoints, txid, status: 'unproven')
|
|
129
158
|
|
|
130
159
|
{ txid: txid, tx: beef_binary.unpack('C*') }
|
|
131
160
|
end
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module BSV
|
|
8
|
+
module Wallet
|
|
9
|
+
module Substrates
|
|
10
|
+
# BRC-100 wallet substrate that delegates all Interface methods to a remote
|
|
11
|
+
# wallet server via JSON-over-HTTP (POST #{base_url}/#{camelCaseMethodName}).
|
|
12
|
+
#
|
|
13
|
+
# Key conversion is handled by {BSV::WireFormat}: Ruby snake_case symbol keys
|
|
14
|
+
# in args are converted to camelCase strings before the request, and the
|
|
15
|
+
# camelCase JSON response is converted back to snake_case symbol keys.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# wallet = BSV::Wallet::Substrates::HTTPWalletJSON.new('http://localhost:3321',
|
|
19
|
+
# originator: 'myapp.example.com')
|
|
20
|
+
# result = wallet.get_public_key({ identity_key: true })
|
|
21
|
+
# # => { public_key: '02abc...' }
|
|
22
|
+
class HTTPWalletJSON
|
|
23
|
+
include BSV::Wallet::Interface
|
|
24
|
+
|
|
25
|
+
# Maps the 28 BRC-100 Interface method symbols to their camelCase HTTP endpoint names.
|
|
26
|
+
# Derived from Wire::Serializer::CALL_CODES keys via BSV::WireFormat.snake_to_camel.
|
|
27
|
+
METHOD_NAMES = BSV::Wallet::Wire::Serializer::CALL_CODES.keys.to_h do |sym|
|
|
28
|
+
[sym, BSV::WireFormat.snake_to_camel(sym.to_s)]
|
|
29
|
+
end.freeze
|
|
30
|
+
|
|
31
|
+
# @param base_url [String] base URL of the remote wallet server (e.g. 'http://localhost:3321')
|
|
32
|
+
# @param originator [String, nil] FQDN of the originating application (sent as Origin/Originator headers)
|
|
33
|
+
# @param http_client [Object, nil] injectable HTTP client for testing; must respond to
|
|
34
|
+
# `start(uri, &block)` returning a Net::HTTP-compatible response
|
|
35
|
+
def initialize(base_url, originator: nil, http_client: nil)
|
|
36
|
+
@base_url = base_url
|
|
37
|
+
@originator = originator
|
|
38
|
+
@http_client = http_client
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Per-call originator is accepted for Interface conformance but not forwarded.
|
|
42
|
+
# The Origin header uses the constructor-level @originator for the lifetime of
|
|
43
|
+
# the connection — matching the TS SDK's HTTPWalletJSON, which also ignores per-call
|
|
44
|
+
# originator. WalletWireTransceiver supports per-call originator because the wire
|
|
45
|
+
# frame encodes it per-message; HTTP substrates identify by connection, not by call.
|
|
46
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
47
|
+
|
|
48
|
+
def create_action(args, originator: nil)
|
|
49
|
+
call(METHOD_NAMES[:create_action], args)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sign_action(args, originator: nil)
|
|
53
|
+
call(METHOD_NAMES[:sign_action], args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def abort_action(args, originator: nil)
|
|
57
|
+
call(METHOD_NAMES[:abort_action], args)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def list_actions(args, originator: nil)
|
|
61
|
+
call(METHOD_NAMES[:list_actions], args)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def internalize_action(args, originator: nil)
|
|
65
|
+
call(METHOD_NAMES[:internalize_action], args)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def list_outputs(args, originator: nil)
|
|
69
|
+
call(METHOD_NAMES[:list_outputs], args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def relinquish_output(args, originator: nil)
|
|
73
|
+
call(METHOD_NAMES[:relinquish_output], args)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def get_public_key(args, originator: nil)
|
|
77
|
+
call(METHOD_NAMES[:get_public_key], args)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reveal_counterparty_key_linkage(args, originator: nil)
|
|
81
|
+
call(METHOD_NAMES[:reveal_counterparty_key_linkage], args)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reveal_specific_key_linkage(args, originator: nil)
|
|
85
|
+
call(METHOD_NAMES[:reveal_specific_key_linkage], args)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def encrypt(args, originator: nil)
|
|
89
|
+
call(METHOD_NAMES[:encrypt], args)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def decrypt(args, originator: nil)
|
|
93
|
+
call(METHOD_NAMES[:decrypt], args)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def create_hmac(args, originator: nil)
|
|
97
|
+
call(METHOD_NAMES[:create_hmac], args)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def verify_hmac(args, originator: nil)
|
|
101
|
+
call(METHOD_NAMES[:verify_hmac], args)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def create_signature(args, originator: nil)
|
|
105
|
+
call(METHOD_NAMES[:create_signature], args)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def verify_signature(args, originator: nil)
|
|
109
|
+
call(METHOD_NAMES[:verify_signature], args)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def acquire_certificate(args, originator: nil)
|
|
113
|
+
call(METHOD_NAMES[:acquire_certificate], args)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def list_certificates(args, originator: nil)
|
|
117
|
+
call(METHOD_NAMES[:list_certificates], args)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def prove_certificate(args, originator: nil)
|
|
121
|
+
call(METHOD_NAMES[:prove_certificate], args)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def relinquish_certificate(args, originator: nil)
|
|
125
|
+
call(METHOD_NAMES[:relinquish_certificate], args)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def discover_by_identity_key(args, originator: nil)
|
|
129
|
+
call(METHOD_NAMES[:discover_by_identity_key], args)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def discover_by_attributes(args, originator: nil)
|
|
133
|
+
call(METHOD_NAMES[:discover_by_attributes], args)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def is_authenticated(args = {}, originator: nil)
|
|
137
|
+
call(METHOD_NAMES[:is_authenticated], args)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def wait_for_authentication(args = {}, originator: nil)
|
|
141
|
+
call(METHOD_NAMES[:wait_for_authentication], args)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def get_height(args = {}, originator: nil)
|
|
145
|
+
call(METHOD_NAMES[:get_height], args)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def get_header_for_height(args, originator: nil)
|
|
149
|
+
call(METHOD_NAMES[:get_header_for_height], args)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def get_network(args = {}, originator: nil)
|
|
153
|
+
call(METHOD_NAMES[:get_network], args)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get_version(args = {}, originator: nil)
|
|
157
|
+
call(METHOD_NAMES[:get_version], args)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
# Posts args to the remote wallet endpoint for the given camelCase method name.
|
|
165
|
+
#
|
|
166
|
+
# Outbound: converts snake_case symbol keys to camelCase strings via WireFormat.to_wire.
|
|
167
|
+
# Inbound: converts camelCase string keys to snake_case symbols via WireFormat.from_wire.
|
|
168
|
+
# Errors: non-2xx response raises the appropriate WalletError subclass.
|
|
169
|
+
#
|
|
170
|
+
# @param method_name [String] camelCase endpoint name (e.g. 'getPublicKey')
|
|
171
|
+
# @param args [Hash] method arguments (snake_case symbol keys)
|
|
172
|
+
# @return [Hash] response with snake_case symbol keys
|
|
173
|
+
def call(method_name, args)
|
|
174
|
+
args ||= {}
|
|
175
|
+
wire_args = BSV::WireFormat.to_wire(args)
|
|
176
|
+
body = JSON.generate(wire_args)
|
|
177
|
+
|
|
178
|
+
uri = build_uri(method_name)
|
|
179
|
+
headers = build_headers
|
|
180
|
+
|
|
181
|
+
response = execute_request(uri, body, headers)
|
|
182
|
+
|
|
183
|
+
handle_response(response, method_name, args)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def build_uri(method_name)
|
|
187
|
+
URI.parse("#{@base_url}/#{method_name}")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_headers
|
|
191
|
+
h = {
|
|
192
|
+
'Content-Type' => 'application/json',
|
|
193
|
+
'Accept' => 'application/json'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if @originator
|
|
197
|
+
origin_value = to_origin_header(@originator)
|
|
198
|
+
h['Origin'] = origin_value
|
|
199
|
+
h['Originator'] = origin_value
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
h
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Converts an originator domain to a full Origin header value.
|
|
206
|
+
# Prepends 'http://' if no scheme is present (matching TS SDK toOriginHeader).
|
|
207
|
+
def to_origin_header(originator)
|
|
208
|
+
return originator if originator.match?(%r{\A[a-z][a-z0-9+.-]*://}i)
|
|
209
|
+
|
|
210
|
+
"http://#{originator}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def execute_request(uri, body, headers)
|
|
214
|
+
if @http_client
|
|
215
|
+
@http_client.post(uri, body, headers)
|
|
216
|
+
else
|
|
217
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
218
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
219
|
+
request.body = body
|
|
220
|
+
http.request(request)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def handle_response(response, method_name, args)
|
|
226
|
+
code = response.code.to_i
|
|
227
|
+
|
|
228
|
+
unless (200..299).cover?(code)
|
|
229
|
+
data = begin
|
|
230
|
+
parse_json_body(response.body)
|
|
231
|
+
rescue JSON::ParserError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
raise_error_response(code, data, method_name, args)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
data = parse_json_body(response.body)
|
|
238
|
+
return {} if data.nil?
|
|
239
|
+
|
|
240
|
+
data.is_a?(Hash) ? BSV::WireFormat.from_wire(data) : data
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def parse_json_body(body)
|
|
244
|
+
return nil if body.nil? || body.empty?
|
|
245
|
+
|
|
246
|
+
JSON.parse(body)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def raise_error_response(code, data, method_name, _args)
|
|
250
|
+
if code == 400 && data.is_a?(Hash) && data['isError']
|
|
251
|
+
case data['code']
|
|
252
|
+
when 5
|
|
253
|
+
raise BSV::Wallet::WalletError.new(data['message'] || 'Review actions required', 5)
|
|
254
|
+
when 6
|
|
255
|
+
raise BSV::Wallet::InvalidParameterError, data['parameter'] || 'unknown'
|
|
256
|
+
when 7
|
|
257
|
+
raise BSV::Wallet::InsufficientFundsError, data['message']
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
message = (data.is_a?(Hash) && data['message']) ||
|
|
262
|
+
"HTTP #{code} error calling #{method_name}"
|
|
263
|
+
raise BSV::Wallet::WalletError, message
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module BSV
|
|
7
|
+
module Wallet
|
|
8
|
+
module Substrates
|
|
9
|
+
# Binary wire transport that transmits BRC-100 wallet wire messages over HTTP.
|
|
10
|
+
#
|
|
11
|
+
# Implements the single-method WalletWire interface: given a raw binary frame
|
|
12
|
+
# (as an Array of byte integers), parses the call code, maps it to a URL path,
|
|
13
|
+
# and POSTs the payload to the remote wallet endpoint.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# wire = BSV::Wallet::Substrates::HTTPWalletWire.new('http://localhost:3301')
|
|
17
|
+
# response_bytes = wire.transmit_to_wallet(frame_bytes)
|
|
18
|
+
class HTTPWalletWire
|
|
19
|
+
# @param base_url [String] the base URL of the remote wallet (e.g. 'http://localhost:3301')
|
|
20
|
+
# @param originator [String, nil] FQDN of the calling application (sent as Origin header)
|
|
21
|
+
# @param http_client [#call, nil] injectable HTTP client for testing; nil uses Net::HTTP
|
|
22
|
+
def initialize(base_url, originator: nil, http_client: nil)
|
|
23
|
+
@base_url = base_url
|
|
24
|
+
@originator = originator
|
|
25
|
+
@http_client = http_client
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Transmits a binary wallet wire message to the remote wallet.
|
|
29
|
+
#
|
|
30
|
+
# Parses the call code from byte 0, reads the originator from the header,
|
|
31
|
+
# and POSTs the remaining payload bytes to the appropriate URL path.
|
|
32
|
+
#
|
|
33
|
+
# @param message [Array<Integer>] raw wire frame as array of byte integers
|
|
34
|
+
# @return [Array<Integer>] response body as array of byte integers
|
|
35
|
+
# @raise [ArgumentError] if the message is empty or contains an unknown call code
|
|
36
|
+
# @raise [RuntimeError] if the HTTP response indicates an error (non-2xx)
|
|
37
|
+
def transmit_to_wallet(message)
|
|
38
|
+
raise ArgumentError, 'message must not be empty' if message.nil? || message.empty?
|
|
39
|
+
|
|
40
|
+
call_code = message[0]
|
|
41
|
+
call_name = BSV::Wallet::Wire::Serializer::METHODS_BY_CODE[call_code]
|
|
42
|
+
raise ArgumentError, "unknown call code: #{call_code}" if call_name.nil?
|
|
43
|
+
|
|
44
|
+
originator_length = message[1] || 0
|
|
45
|
+
originator = message[2, originator_length].pack('C*').force_encoding('UTF-8') if originator_length.positive?
|
|
46
|
+
|
|
47
|
+
payload_start = 2 + originator_length
|
|
48
|
+
payload = message[payload_start..] || []
|
|
49
|
+
|
|
50
|
+
camel_name = BSV::WireFormat.snake_to_camel(call_name.to_s)
|
|
51
|
+
url = "#{@base_url}/#{camel_name}"
|
|
52
|
+
|
|
53
|
+
response_body = post_binary(url, payload, originator || @originator)
|
|
54
|
+
response_body.bytes.to_a
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def post_binary(url, payload_bytes, originator)
|
|
60
|
+
uri = URI.parse(url)
|
|
61
|
+
body = payload_bytes.pack('C*')
|
|
62
|
+
|
|
63
|
+
if @http_client
|
|
64
|
+
@http_client.call(uri, body, originator)
|
|
65
|
+
else
|
|
66
|
+
perform_request(uri, body, originator)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def perform_request(uri, body, originator)
|
|
71
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
72
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
73
|
+
request['Content-Type'] = 'application/octet-stream'
|
|
74
|
+
request['Origin'] = originator if originator && !originator.empty?
|
|
75
|
+
request.body = body
|
|
76
|
+
response = http.request(request)
|
|
77
|
+
raise "HTTPWalletWire: HTTP #{response.code} from #{uri}" unless response.is_a?(Net::HTTPSuccess)
|
|
78
|
+
|
|
79
|
+
response.body || ''
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
module Substrates
|
|
6
|
+
# BRC-100 wallet Interface implementation that transmits calls over a binary wire transport.
|
|
7
|
+
#
|
|
8
|
+
# Serialises each Interface method call into a binary wire frame via
|
|
9
|
+
# {BSV::Wallet::Wire::Serializer}, transmits it via a wire transport (any object
|
|
10
|
+
# responding to `#transmit_to_wallet`), then deserialises the response.
|
|
11
|
+
#
|
|
12
|
+
# The wire transport is duck-typed — any object that accepts
|
|
13
|
+
# `transmit_to_wallet(message)` where +message+ is an Array of byte integers
|
|
14
|
+
# and returns an Array of byte integers qualifies. The canonical wire transport
|
|
15
|
+
# is {HTTPWalletWire}.
|
|
16
|
+
#
|
|
17
|
+
# @example Using with HTTPWalletWire
|
|
18
|
+
# wire = BSV::Wallet::Substrates::HTTPWalletWire.new('http://localhost:3301')
|
|
19
|
+
# wallet = BSV::Wallet::Substrates::WalletWireTransceiver.new(wire, originator: 'myapp.example.com')
|
|
20
|
+
# result = wallet.get_public_key({ identity_key: true })
|
|
21
|
+
# # => { public_key: '02abc...' }
|
|
22
|
+
class WalletWireTransceiver
|
|
23
|
+
include BSV::Wallet::Interface
|
|
24
|
+
|
|
25
|
+
# @param wire [#transmit_to_wallet] wire transport (duck-typed)
|
|
26
|
+
# @param originator [String, nil] default FQDN of the originating application;
|
|
27
|
+
# may be overridden per-call via the method-level originator keyword argument
|
|
28
|
+
def initialize(wire, originator: nil)
|
|
29
|
+
@wire = wire
|
|
30
|
+
@originator = originator
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_action(args, originator: nil)
|
|
34
|
+
transmit(:create_action, args, originator || @originator)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sign_action(args, originator: nil)
|
|
38
|
+
transmit(:sign_action, args, originator || @originator)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def abort_action(args, originator: nil)
|
|
42
|
+
transmit(:abort_action, args, originator || @originator)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def list_actions(args, originator: nil)
|
|
46
|
+
transmit(:list_actions, args, originator || @originator)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def internalize_action(args, originator: nil)
|
|
50
|
+
transmit(:internalize_action, args, originator || @originator)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def list_outputs(args, originator: nil)
|
|
54
|
+
transmit(:list_outputs, args, originator || @originator)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def relinquish_output(args, originator: nil)
|
|
58
|
+
transmit(:relinquish_output, args, originator || @originator)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_public_key(args, originator: nil)
|
|
62
|
+
transmit(:get_public_key, args, originator || @originator)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reveal_counterparty_key_linkage(args, originator: nil)
|
|
66
|
+
transmit(:reveal_counterparty_key_linkage, args, originator || @originator)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reveal_specific_key_linkage(args, originator: nil)
|
|
70
|
+
transmit(:reveal_specific_key_linkage, args, originator || @originator)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def encrypt(args, originator: nil)
|
|
74
|
+
transmit(:encrypt, args, originator || @originator)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def decrypt(args, originator: nil)
|
|
78
|
+
transmit(:decrypt, args, originator || @originator)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def create_hmac(args, originator: nil)
|
|
82
|
+
transmit(:create_hmac, args, originator || @originator)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def verify_hmac(args, originator: nil)
|
|
86
|
+
transmit(:verify_hmac, args, originator || @originator)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def create_signature(args, originator: nil)
|
|
90
|
+
transmit(:create_signature, args, originator || @originator)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def verify_signature(args, originator: nil)
|
|
94
|
+
transmit(:verify_signature, args, originator || @originator)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def acquire_certificate(args, originator: nil)
|
|
98
|
+
transmit(:acquire_certificate, args, originator || @originator)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def list_certificates(args, originator: nil)
|
|
102
|
+
transmit(:list_certificates, args, originator || @originator)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def prove_certificate(args, originator: nil)
|
|
106
|
+
transmit(:prove_certificate, args, originator || @originator)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def relinquish_certificate(args, originator: nil)
|
|
110
|
+
transmit(:relinquish_certificate, args, originator || @originator)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def discover_by_identity_key(args, originator: nil)
|
|
114
|
+
transmit(:discover_by_identity_key, args, originator || @originator)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def discover_by_attributes(args, originator: nil)
|
|
118
|
+
transmit(:discover_by_attributes, args, originator || @originator)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def is_authenticated(args = {}, originator: nil)
|
|
122
|
+
transmit(:is_authenticated, args, originator || @originator)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def wait_for_authentication(args = {}, originator: nil)
|
|
126
|
+
transmit(:wait_for_authentication, args, originator || @originator)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def get_height(args = {}, originator: nil)
|
|
130
|
+
transmit(:get_height, args, originator || @originator)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def get_header_for_height(args, originator: nil)
|
|
134
|
+
transmit(:get_header_for_height, args, originator || @originator)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def get_network(args = {}, originator: nil)
|
|
138
|
+
transmit(:get_network, args, originator || @originator)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def get_version(args = {}, originator: nil)
|
|
142
|
+
transmit(:get_version, args, originator || @originator)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# Serialises +method_name+ and +args+ into a binary wire frame, transmits it
|
|
148
|
+
# via the wire transport, and deserialises the response.
|
|
149
|
+
#
|
|
150
|
+
# Error responses (non-zero first byte) are parsed and raised as {WalletError}
|
|
151
|
+
# by {BSV::Wallet::Wire::Serializer.deserialize_response}.
|
|
152
|
+
#
|
|
153
|
+
# @param method_name [Symbol] BRC-100 method name (snake_case)
|
|
154
|
+
# @param args [Hash] method arguments
|
|
155
|
+
# @param orig [String, nil] FQDN of the originating application
|
|
156
|
+
# @return [Hash] deserialised result
|
|
157
|
+
def transmit(method_name, args, orig)
|
|
158
|
+
frame = BSV::Wallet::Wire::Serializer.serialize_request(
|
|
159
|
+
method_name, args || {}, originator: orig.to_s
|
|
160
|
+
)
|
|
161
|
+
response_bytes = @wire.transmit_to_wallet(frame.bytes.to_a)
|
|
162
|
+
response_binary = response_bytes.pack('C*')
|
|
163
|
+
BSV::Wallet::Wire::Serializer.deserialize_response(method_name, response_binary)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
# BRC-100 wallet substrates — alternative transport layers for the wallet interface.
|
|
6
|
+
#
|
|
7
|
+
# Substrates are raw transport adapters. They are not full Wallet::Interface implementations;
|
|
8
|
+
# instead they provide the low-level connectivity that a WalletWireTransceiver or
|
|
9
|
+
# HTTPWalletJSON wraps to expose the full interface.
|
|
10
|
+
module Substrates
|
|
11
|
+
autoload :HTTPWalletJSON, 'bsv/wallet_interface/substrates/http_wallet_json'
|
|
12
|
+
autoload :HTTPWalletWire, 'bsv/wallet_interface/substrates/http_wallet_wire'
|
|
13
|
+
autoload :WalletWireTransceiver, 'bsv/wallet_interface/substrates/wallet_wire_transceiver'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -45,6 +45,9 @@ module BSV
|
|
|
45
45
|
# @return [BroadcastQueue] the broadcast queue used to dispatch transactions
|
|
46
46
|
attr_reader :broadcast_queue
|
|
47
47
|
|
|
48
|
+
# @return [Interface, nil] the optional substrate for remote wallet delegation
|
|
49
|
+
attr_reader :substrate
|
|
50
|
+
|
|
48
51
|
# @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
|
|
49
52
|
# @param storage [StorageAdapter] persistence adapter (default: FileStore).
|
|
50
53
|
# Use +storage: MemoryStore.new+ for tests.
|
|
@@ -54,6 +57,10 @@ module BSV
|
|
|
54
57
|
# @param http_client [#request, nil] injectable HTTP client for certificate issuance
|
|
55
58
|
# @param broadcaster [#broadcast, nil] optional broadcaster; any object responding to #broadcast(tx)
|
|
56
59
|
# @param broadcast_queue [BroadcastQueue, nil] optional broadcast queue; defaults to InlineQueue
|
|
60
|
+
# @param substrate [Interface, nil] optional remote wallet substrate; when set, all Interface
|
|
61
|
+
# methods delegate to the substrate instead of using local storage and key derivation.
|
|
62
|
+
# Accepts any object implementing {Interface} (e.g. {Substrates::HTTPWalletJSON},
|
|
63
|
+
# {Substrates::WalletWireTransceiver}).
|
|
57
64
|
def initialize(
|
|
58
65
|
key,
|
|
59
66
|
storage: FileStore.new,
|
|
@@ -65,9 +72,11 @@ module BSV
|
|
|
65
72
|
coin_selector: nil,
|
|
66
73
|
change_generator: nil,
|
|
67
74
|
broadcaster: nil,
|
|
68
|
-
broadcast_queue: nil
|
|
75
|
+
broadcast_queue: nil,
|
|
76
|
+
substrate: nil
|
|
69
77
|
)
|
|
70
78
|
super(key)
|
|
79
|
+
@substrate = substrate
|
|
71
80
|
@storage = storage
|
|
72
81
|
@network = network
|
|
73
82
|
@chain_provider = chain_provider
|
|
@@ -85,9 +94,13 @@ module BSV
|
|
|
85
94
|
)
|
|
86
95
|
end
|
|
87
96
|
|
|
88
|
-
# Returns true when
|
|
97
|
+
# Returns +true+ when broadcast is available.
|
|
98
|
+
#
|
|
99
|
+
# Delegates to the broadcast queue so that queue-embedded broadcasters
|
|
100
|
+
# (e.g. +SolidQueueAdapter.new(broadcaster: arc)+) are recognised even
|
|
101
|
+
# when no +broadcaster:+ was passed directly to +WalletClient+.
|
|
89
102
|
def broadcast_enabled?
|
|
90
|
-
|
|
103
|
+
@broadcast_queue.broadcast_enabled?
|
|
91
104
|
end
|
|
92
105
|
|
|
93
106
|
# --- Transaction Operations ---
|
|
@@ -110,8 +123,11 @@ module BSV
|
|
|
110
123
|
# UTXOs and generates change; requires +:outputs+ and no +:inputs+
|
|
111
124
|
# @param _originator [String, nil] FQDN of the originating application
|
|
112
125
|
# @return [Hash] finalised result or signable_transaction
|
|
113
|
-
def create_action(args,
|
|
126
|
+
def create_action(args, originator: nil)
|
|
127
|
+
return @substrate.create_action(args, originator: originator) if @substrate
|
|
128
|
+
|
|
114
129
|
validate_create_action!(args)
|
|
130
|
+
validate_broadcast_configuration!(args)
|
|
115
131
|
|
|
116
132
|
send_with_txids = Array(args.dig(:options, :send_with))
|
|
117
133
|
|
|
@@ -156,7 +172,9 @@ module BSV
|
|
|
156
172
|
# @option args [String] :reference base64 reference from create_action
|
|
157
173
|
# @param _originator [String, nil] FQDN of the originating application
|
|
158
174
|
# @return [Hash] with :txid and :tx (BEEF bytes)
|
|
159
|
-
def sign_action(args,
|
|
175
|
+
def sign_action(args, originator: nil)
|
|
176
|
+
return @substrate.sign_action(args, originator: originator) if @substrate
|
|
177
|
+
|
|
160
178
|
reference = args[:reference]
|
|
161
179
|
pending = @pending[reference]
|
|
162
180
|
raise WalletError, 'Transaction not found for the given reference' unless pending
|
|
@@ -172,6 +190,11 @@ module BSV
|
|
|
172
190
|
else
|
|
173
191
|
pending[:args]
|
|
174
192
|
end
|
|
193
|
+
|
|
194
|
+
# Re-validate broadcast configuration on merged args — options may have
|
|
195
|
+
# been flipped between create_action and sign_action (e.g. no_send toggled).
|
|
196
|
+
validate_broadcast_configuration!(merged_args)
|
|
197
|
+
|
|
175
198
|
finalize_action(tx, merged_args)
|
|
176
199
|
end
|
|
177
200
|
|
|
@@ -185,7 +208,9 @@ module BSV
|
|
|
185
208
|
# @option args [String] :reference base64 reference to abort
|
|
186
209
|
# @param _originator [String, nil] FQDN of the originating application
|
|
187
210
|
# @return [Hash] { aborted: true }
|
|
188
|
-
def abort_action(args,
|
|
211
|
+
def abort_action(args, originator: nil)
|
|
212
|
+
return @substrate.abort_action(args, originator: originator) if @substrate
|
|
213
|
+
|
|
189
214
|
reference = args[:reference]
|
|
190
215
|
raise WalletError, 'Transaction not found for the given reference' unless @pending.key?(reference)
|
|
191
216
|
|
|
@@ -210,12 +235,14 @@ module BSV
|
|
|
210
235
|
# @option args [Integer] :offset results to skip (default 0)
|
|
211
236
|
# @param _originator [String, nil] FQDN of the originating application
|
|
212
237
|
# @return [Hash] { total_actions: Integer, actions: Array }
|
|
213
|
-
def list_actions(args,
|
|
238
|
+
def list_actions(args, originator: nil)
|
|
239
|
+
return @substrate.list_actions(args, originator: originator) if @substrate
|
|
240
|
+
|
|
214
241
|
validate_list_actions!(args)
|
|
215
242
|
query = build_action_query(args)
|
|
216
243
|
total = @storage.count_actions(query)
|
|
217
244
|
actions = @storage.find_actions(query)
|
|
218
|
-
{ total_actions: total, actions: actions }
|
|
245
|
+
{ total_actions: total, actions: strip_action_fields(actions, args) }
|
|
219
246
|
end
|
|
220
247
|
|
|
221
248
|
# Lists spendable outputs in a basket.
|
|
@@ -228,12 +255,14 @@ module BSV
|
|
|
228
255
|
# @option args [Integer] :offset results to skip (default 0)
|
|
229
256
|
# @param _originator [String, nil] FQDN of the originating application
|
|
230
257
|
# @return [Hash] { total_outputs: Integer, outputs: Array }
|
|
231
|
-
def list_outputs(args,
|
|
258
|
+
def list_outputs(args, originator: nil)
|
|
259
|
+
return @substrate.list_outputs(args, originator: originator) if @substrate
|
|
260
|
+
|
|
232
261
|
validate_list_outputs!(args)
|
|
233
262
|
query = build_output_query(args)
|
|
234
263
|
total = @storage.count_outputs(query)
|
|
235
264
|
outputs = @storage.find_outputs(query)
|
|
236
|
-
{ total_outputs: total, outputs: outputs }
|
|
265
|
+
{ total_outputs: total, outputs: strip_output_fields(outputs, args) }
|
|
237
266
|
end
|
|
238
267
|
|
|
239
268
|
# Removes an output from basket tracking.
|
|
@@ -243,7 +272,9 @@ module BSV
|
|
|
243
272
|
# @option args [String] :output outpoint string
|
|
244
273
|
# @param _originator [String, nil] FQDN of the originating application
|
|
245
274
|
# @return [Hash] { relinquished: true }
|
|
246
|
-
def relinquish_output(args,
|
|
275
|
+
def relinquish_output(args, originator: nil)
|
|
276
|
+
return @substrate.relinquish_output(args, originator: originator) if @substrate
|
|
277
|
+
|
|
247
278
|
Validators.validate_basket!(args[:basket])
|
|
248
279
|
Validators.validate_outpoint!(args[:output])
|
|
249
280
|
raise WalletError, 'Output not found' unless @storage.delete_output(args[:output])
|
|
@@ -264,7 +295,9 @@ module BSV
|
|
|
264
295
|
# @option args [Array<String>] :labels optional labels
|
|
265
296
|
# @param _originator [String, nil] FQDN of the originating application
|
|
266
297
|
# @return [Hash] { accepted: true }
|
|
267
|
-
def internalize_action(args,
|
|
298
|
+
def internalize_action(args, originator: nil)
|
|
299
|
+
return @substrate.internalize_action(args, originator: originator) if @substrate
|
|
300
|
+
|
|
268
301
|
validate_internalize_action!(args)
|
|
269
302
|
beef_binary = args[:tx].pack('C*')
|
|
270
303
|
beef = BSV::Transaction::Beef.from_binary(beef_binary)
|
|
@@ -280,7 +313,8 @@ module BSV
|
|
|
280
313
|
store_proofs_from_beef(beef)
|
|
281
314
|
@storage.store_transaction(tx.txid_hex, tx.to_hex)
|
|
282
315
|
process_internalize_outputs(tx, args[:outputs])
|
|
283
|
-
|
|
316
|
+
has_proof = !beef.find_bump(tx.txid).nil?
|
|
317
|
+
store_action(tx, args, status: has_proof ? 'completed' : 'unproven')
|
|
284
318
|
{ accepted: true }
|
|
285
319
|
end
|
|
286
320
|
|
|
@@ -290,7 +324,9 @@ module BSV
|
|
|
290
324
|
#
|
|
291
325
|
# @param _args [Hash] unused (empty hash)
|
|
292
326
|
# @return [Hash] { height: Integer }
|
|
293
|
-
def get_height(
|
|
327
|
+
def get_height(args = {}, originator: nil)
|
|
328
|
+
return @substrate.get_height(args, originator: originator) if @substrate
|
|
329
|
+
|
|
294
330
|
{ height: @chain_provider.get_height }
|
|
295
331
|
end
|
|
296
332
|
|
|
@@ -299,7 +335,9 @@ module BSV
|
|
|
299
335
|
# @param args [Hash]
|
|
300
336
|
# @option args [Integer] :height block height
|
|
301
337
|
# @return [Hash] { header: String } 80-byte hex-encoded block header
|
|
302
|
-
def get_header_for_height(args,
|
|
338
|
+
def get_header_for_height(args, originator: nil)
|
|
339
|
+
return @substrate.get_header_for_height(args, originator: originator) if @substrate
|
|
340
|
+
|
|
303
341
|
raise InvalidParameterError.new('height', 'a positive Integer') unless args[:height].is_a?(Integer) && args[:height].positive?
|
|
304
342
|
|
|
305
343
|
{ header: @chain_provider.get_header(args[:height]) }
|
|
@@ -309,7 +347,9 @@ module BSV
|
|
|
309
347
|
#
|
|
310
348
|
# @param _args [Hash] unused (empty hash)
|
|
311
349
|
# @return [Hash] { network: String } 'mainnet' or 'testnet'
|
|
312
|
-
def get_network(
|
|
350
|
+
def get_network(args = {}, originator: nil)
|
|
351
|
+
return @substrate.get_network(args, originator: originator) if @substrate
|
|
352
|
+
|
|
313
353
|
{ network: @network }
|
|
314
354
|
end
|
|
315
355
|
|
|
@@ -317,8 +357,10 @@ module BSV
|
|
|
317
357
|
#
|
|
318
358
|
# @param _args [Hash] unused (empty hash)
|
|
319
359
|
# @return [Hash] { version: String } in vendor-major.minor.patch format
|
|
320
|
-
def get_version(
|
|
321
|
-
|
|
360
|
+
def get_version(args = {}, originator: nil)
|
|
361
|
+
return @substrate.get_version(args, originator: originator) if @substrate
|
|
362
|
+
|
|
363
|
+
{ version: "bsv-wallet-#{BSV::Wallet::VERSION}" }
|
|
322
364
|
end
|
|
323
365
|
|
|
324
366
|
# Discovers on-chain UTXOs for the wallet's identity address and imports
|
|
@@ -427,7 +469,9 @@ module BSV
|
|
|
427
469
|
#
|
|
428
470
|
# @param _args [Hash] unused (empty hash)
|
|
429
471
|
# @return [Hash] { authenticated: Boolean }
|
|
430
|
-
def is_authenticated(
|
|
472
|
+
def is_authenticated(args = {}, originator: nil)
|
|
473
|
+
return @substrate.is_authenticated(args, originator: originator) if @substrate
|
|
474
|
+
|
|
431
475
|
{ authenticated: true }
|
|
432
476
|
end
|
|
433
477
|
|
|
@@ -436,7 +480,9 @@ module BSV
|
|
|
436
480
|
#
|
|
437
481
|
# @param _args [Hash] unused (empty hash)
|
|
438
482
|
# @return [Hash] { authenticated: true }
|
|
439
|
-
def wait_for_authentication(
|
|
483
|
+
def wait_for_authentication(args = {}, originator: nil)
|
|
484
|
+
return @substrate.wait_for_authentication(args, originator: originator) if @substrate
|
|
485
|
+
|
|
440
486
|
{ authenticated: true }
|
|
441
487
|
end
|
|
442
488
|
|
|
@@ -458,7 +504,9 @@ module BSV
|
|
|
458
504
|
# @option args [String] :keyring_revealer pubkey hex or 'certifier' (required for direct)
|
|
459
505
|
# @option args [Hash] :keyring_for_subject field_name => base64 key (required for direct)
|
|
460
506
|
# @return [Hash] the stored certificate
|
|
461
|
-
def acquire_certificate(args,
|
|
507
|
+
def acquire_certificate(args, originator: nil)
|
|
508
|
+
return @substrate.acquire_certificate(args, originator: originator) if @substrate
|
|
509
|
+
|
|
462
510
|
validate_acquire_certificate!(args)
|
|
463
511
|
|
|
464
512
|
cert = if args[:acquisition_protocol] == 'issuance'
|
|
@@ -479,7 +527,9 @@ module BSV
|
|
|
479
527
|
# @option args [Integer] :limit max results (default 10)
|
|
480
528
|
# @option args [Integer] :offset number to skip (default 0)
|
|
481
529
|
# @return [Hash] { total_certificates:, certificates: [...] }
|
|
482
|
-
def list_certificates(args,
|
|
530
|
+
def list_certificates(args, originator: nil)
|
|
531
|
+
return @substrate.list_certificates(args, originator: originator) if @substrate
|
|
532
|
+
|
|
483
533
|
raise InvalidParameterError.new('certifiers', 'a non-empty Array') unless args[:certifiers].is_a?(Array) && !args[:certifiers].empty?
|
|
484
534
|
raise InvalidParameterError.new('types', 'a non-empty Array') unless args[:types].is_a?(Array) && !args[:types].empty?
|
|
485
535
|
|
|
@@ -505,7 +555,9 @@ module BSV
|
|
|
505
555
|
# @option args [Array<String>] :fields_to_reveal field names to reveal
|
|
506
556
|
# @option args [String] :verifier verifier public key hex
|
|
507
557
|
# @return [Hash] { keyring_for_verifier: { field_name => Array<Integer> } }
|
|
508
|
-
def prove_certificate(args,
|
|
558
|
+
def prove_certificate(args, originator: nil)
|
|
559
|
+
return @substrate.prove_certificate(args, originator: originator) if @substrate
|
|
560
|
+
|
|
509
561
|
cert_arg = args[:certificate]
|
|
510
562
|
fields_to_reveal = args[:fields_to_reveal]
|
|
511
563
|
verifier = args[:verifier]
|
|
@@ -528,8 +580,8 @@ module BSV
|
|
|
528
580
|
# Encrypt the keyring entry for the verifier
|
|
529
581
|
encrypted = encrypt({
|
|
530
582
|
plaintext: key_value.bytes,
|
|
531
|
-
protocol_id: [2, 'certificate field
|
|
532
|
-
key_id: "#{cert_arg[:
|
|
583
|
+
protocol_id: [2, 'certificate field encryption'],
|
|
584
|
+
key_id: "#{cert_arg[:serial_number]} #{field_name}",
|
|
533
585
|
counterparty: verifier
|
|
534
586
|
})
|
|
535
587
|
keyring_for_verifier[field_name] = encrypted[:ciphertext]
|
|
@@ -545,7 +597,9 @@ module BSV
|
|
|
545
597
|
# @option args [String] :serial_number serial number
|
|
546
598
|
# @option args [String] :certifier certifier public key hex
|
|
547
599
|
# @return [Hash] { relinquished: true }
|
|
548
|
-
def relinquish_certificate(args,
|
|
600
|
+
def relinquish_certificate(args, originator: nil)
|
|
601
|
+
return @substrate.relinquish_certificate(args, originator: originator) if @substrate
|
|
602
|
+
|
|
549
603
|
deleted = @storage.delete_certificate(
|
|
550
604
|
type: args[:type],
|
|
551
605
|
serial_number: args[:serial_number],
|
|
@@ -566,7 +620,9 @@ module BSV
|
|
|
566
620
|
# @option args [Integer] :limit max results (default 10)
|
|
567
621
|
# @option args [Integer] :offset number to skip (default 0)
|
|
568
622
|
# @return [Hash] { total_certificates:, certificates: [...] }
|
|
569
|
-
def discover_by_identity_key(args,
|
|
623
|
+
def discover_by_identity_key(args, originator: nil)
|
|
624
|
+
return @substrate.discover_by_identity_key(args, originator: originator) if @substrate
|
|
625
|
+
|
|
570
626
|
Validators.validate_pub_key_hex!(args[:identity_key], 'identity_key')
|
|
571
627
|
|
|
572
628
|
query = { subject: args[:identity_key], limit: args[:limit] || 10, offset: args[:offset] || 0 }
|
|
@@ -585,7 +641,9 @@ module BSV
|
|
|
585
641
|
# @option args [Integer] :limit max results (default 10)
|
|
586
642
|
# @option args [Integer] :offset number to skip (default 0)
|
|
587
643
|
# @return [Hash] { total_certificates:, certificates: [...] }
|
|
588
|
-
def discover_by_attributes(args,
|
|
644
|
+
def discover_by_attributes(args, originator: nil)
|
|
645
|
+
return @substrate.discover_by_attributes(args, originator: originator) if @substrate
|
|
646
|
+
|
|
589
647
|
raise InvalidParameterError.new('attributes', 'a non-empty Hash') unless args[:attributes].is_a?(Hash) && !args[:attributes].empty?
|
|
590
648
|
|
|
591
649
|
query = { attributes: args[:attributes], limit: args[:limit] || 10, offset: args[:offset] || 0 }
|
|
@@ -594,6 +652,65 @@ module BSV
|
|
|
594
652
|
{ total_certificates: total, certificates: certs.map { |c| cert_without_keyring(c) } }
|
|
595
653
|
end
|
|
596
654
|
|
|
655
|
+
# --- ProtoWallet crypto method overrides for substrate delegation ---
|
|
656
|
+
#
|
|
657
|
+
# When a substrate is configured, these methods delegate to it rather than
|
|
658
|
+
# performing local key derivation and cryptographic operations.
|
|
659
|
+
|
|
660
|
+
def get_public_key(args, originator: nil)
|
|
661
|
+
return @substrate.get_public_key(args, originator: originator) if @substrate
|
|
662
|
+
|
|
663
|
+
super
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def reveal_counterparty_key_linkage(args, originator: nil)
|
|
667
|
+
return @substrate.reveal_counterparty_key_linkage(args, originator: originator) if @substrate
|
|
668
|
+
|
|
669
|
+
super
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def reveal_specific_key_linkage(args, originator: nil)
|
|
673
|
+
return @substrate.reveal_specific_key_linkage(args, originator: originator) if @substrate
|
|
674
|
+
|
|
675
|
+
super
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def encrypt(args, originator: nil)
|
|
679
|
+
return @substrate.encrypt(args, originator: originator) if @substrate
|
|
680
|
+
|
|
681
|
+
super
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def decrypt(args, originator: nil)
|
|
685
|
+
return @substrate.decrypt(args, originator: originator) if @substrate
|
|
686
|
+
|
|
687
|
+
super
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def create_hmac(args, originator: nil)
|
|
691
|
+
return @substrate.create_hmac(args, originator: originator) if @substrate
|
|
692
|
+
|
|
693
|
+
super
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def verify_hmac(args, originator: nil)
|
|
697
|
+
return @substrate.verify_hmac(args, originator: originator) if @substrate
|
|
698
|
+
|
|
699
|
+
super
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def create_signature(args, originator: nil)
|
|
703
|
+
return @substrate.create_signature(args, originator: originator) if @substrate
|
|
704
|
+
|
|
705
|
+
super
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def verify_signature(args, originator: nil)
|
|
709
|
+
return @substrate.verify_signature(args, originator: originator) if @substrate
|
|
710
|
+
|
|
711
|
+
super
|
|
712
|
+
end
|
|
713
|
+
|
|
597
714
|
# Maximum ancestor depth to traverse when wiring source transactions.
|
|
598
715
|
# Guards against stack overflow on pathologically deep or cyclic chains.
|
|
599
716
|
ANCESTOR_DEPTH_CAP = 64
|
|
@@ -962,6 +1079,25 @@ module BSV
|
|
|
962
1079
|
|
|
963
1080
|
# --- Validation ---
|
|
964
1081
|
|
|
1082
|
+
# Raises +WalletError+ when a broadcast is required but unavailable.
|
|
1083
|
+
#
|
|
1084
|
+
# Broadcast is required whenever +no_send+ is absent or false. Callers
|
|
1085
|
+
# that do not intend to broadcast on-chain must pass
|
|
1086
|
+
# +options: { no_send: true }+ to opt out.
|
|
1087
|
+
#
|
|
1088
|
+
# @param args [Hash] action arguments (merged, as seen at call site)
|
|
1089
|
+
# @raise [WalletError] if broadcast is needed but +broadcast_enabled?+ is false
|
|
1090
|
+
def validate_broadcast_configuration!(args)
|
|
1091
|
+
no_send = args.dig(:options, :no_send)
|
|
1092
|
+
return if no_send
|
|
1093
|
+
return if broadcast_enabled?
|
|
1094
|
+
|
|
1095
|
+
raise WalletError,
|
|
1096
|
+
'create_action requires a broadcaster for on-chain broadcast. ' \
|
|
1097
|
+
'Pass broadcaster: BSV::Network::ARC.default to WalletClient.new, ' \
|
|
1098
|
+
'or options: { no_send: true } to build a transaction without broadcasting.'
|
|
1099
|
+
end
|
|
1100
|
+
|
|
965
1101
|
def validate_create_action!(args)
|
|
966
1102
|
Validators.validate_description!(args[:description])
|
|
967
1103
|
inputs_present = args[:inputs] && !args[:inputs].empty?
|
|
@@ -1436,6 +1572,47 @@ module BSV
|
|
|
1436
1572
|
query
|
|
1437
1573
|
end
|
|
1438
1574
|
|
|
1575
|
+
# --- Include-flag stripping ---
|
|
1576
|
+
|
|
1577
|
+
def strip_action_fields(actions, args)
|
|
1578
|
+
actions.map do |action|
|
|
1579
|
+
a = action.dup
|
|
1580
|
+
a.delete(:labels) unless args[:include_labels] == true
|
|
1581
|
+
a.delete(:inputs) unless args[:include_inputs] == true
|
|
1582
|
+
|
|
1583
|
+
if a.key?(:inputs)
|
|
1584
|
+
strip_src = args[:include_input_source_locking_scripts] != true
|
|
1585
|
+
strip_unlock = args[:include_input_unlocking_scripts] != true
|
|
1586
|
+
if strip_src || strip_unlock
|
|
1587
|
+
a[:inputs] = a[:inputs].map do |i|
|
|
1588
|
+
d = i.dup
|
|
1589
|
+
d.delete(:source_locking_script) if strip_src
|
|
1590
|
+
d.delete(:unlocking_script) if strip_unlock
|
|
1591
|
+
d
|
|
1592
|
+
end
|
|
1593
|
+
end
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
a.delete(:outputs) unless args[:include_outputs] == true
|
|
1597
|
+
|
|
1598
|
+
if a.key?(:outputs) && args[:include_output_locking_scripts] != true
|
|
1599
|
+
a[:outputs] = a[:outputs].map { |o| o.dup.tap { |h| h.delete(:locking_script) } }
|
|
1600
|
+
end
|
|
1601
|
+
|
|
1602
|
+
a
|
|
1603
|
+
end
|
|
1604
|
+
end
|
|
1605
|
+
|
|
1606
|
+
def strip_output_fields(outputs, args)
|
|
1607
|
+
outputs.map do |output|
|
|
1608
|
+
o = output.dup
|
|
1609
|
+
o.delete(:tags) unless args[:include_tags] == true
|
|
1610
|
+
o.delete(:labels) unless args[:include_labels] == true
|
|
1611
|
+
o.delete(:custom_instructions) unless args[:include_custom_instructions] == true
|
|
1612
|
+
o
|
|
1613
|
+
end
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1439
1616
|
# --- Internalize helpers ---
|
|
1440
1617
|
|
|
1441
1618
|
def store_proofs_from_beef(beef)
|
|
@@ -1580,20 +1757,19 @@ module BSV
|
|
|
1580
1757
|
end
|
|
1581
1758
|
|
|
1582
1759
|
def acquire_via_issuance(args)
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
code = response.code.to_i
|
|
1760
|
+
response = auth_fetch_client.fetch(
|
|
1761
|
+
args[:certifier_url],
|
|
1762
|
+
method: 'POST',
|
|
1763
|
+
headers: { 'content-type' => 'application/json' },
|
|
1764
|
+
body: JSON.generate({
|
|
1765
|
+
type: args[:type],
|
|
1766
|
+
subject: @key_deriver.identity_key,
|
|
1767
|
+
certifier: args[:certifier],
|
|
1768
|
+
fields: args[:fields]
|
|
1769
|
+
})
|
|
1770
|
+
)
|
|
1595
1771
|
|
|
1596
|
-
raise WalletError, "Certificate issuance failed: HTTP #{
|
|
1772
|
+
raise WalletError, "Certificate issuance failed: HTTP #{response.status}" unless (200..299).cover?(response.status)
|
|
1597
1773
|
|
|
1598
1774
|
body = JSON.parse(response.body)
|
|
1599
1775
|
|
|
@@ -1619,6 +1795,10 @@ module BSV
|
|
|
1619
1795
|
raise WalletError, 'Certificate issuance failed: invalid JSON response'
|
|
1620
1796
|
end
|
|
1621
1797
|
|
|
1798
|
+
def auth_fetch_client
|
|
1799
|
+
@auth_fetch_client ||= BSV::Auth::AuthFetch.new(wallet: self)
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1622
1802
|
def execute_http(uri, request)
|
|
1623
1803
|
if @http_client
|
|
1624
1804
|
@http_client.request(uri, request)
|
data/lib/bsv/wallet_interface.rb
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BSV
|
|
4
|
-
module WalletInterface
|
|
5
|
-
autoload :VERSION, 'bsv/wallet_interface/version'
|
|
6
|
-
end
|
|
7
|
-
|
|
8
4
|
module Wallet
|
|
5
|
+
autoload :VERSION, 'bsv/wallet_interface/version'
|
|
6
|
+
|
|
9
7
|
# BRC-100 Interface
|
|
10
8
|
autoload :Interface, 'bsv/wallet_interface/interface'
|
|
11
9
|
autoload :KeyDeriver, 'bsv/wallet_interface/key_deriver'
|
|
@@ -23,6 +21,7 @@ module BSV
|
|
|
23
21
|
autoload :WhatsOnChainProvider, 'bsv/wallet_interface/whats_on_chain_provider'
|
|
24
22
|
autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
|
|
25
23
|
autoload :Wire, 'bsv/wallet_interface/wire'
|
|
24
|
+
autoload :Substrates, 'bsv/wallet_interface/substrates'
|
|
26
25
|
autoload :CertificateSignature, 'bsv/wallet_interface/certificate_signature'
|
|
27
26
|
autoload :FeeModel, 'bsv/wallet_interface/fee_model'
|
|
28
27
|
autoload :FeeEstimator, 'bsv/wallet_interface/fee_estimator'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-wallet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
@@ -29,7 +29,7 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.11.0
|
|
33
33
|
- - "<"
|
|
34
34
|
- !ruby/object:Gem::Version
|
|
35
35
|
version: '1.0'
|
|
@@ -39,7 +39,7 @@ dependencies:
|
|
|
39
39
|
requirements:
|
|
40
40
|
- - ">="
|
|
41
41
|
- !ruby/object:Gem::Version
|
|
42
|
-
version: 0.
|
|
42
|
+
version: 0.11.0
|
|
43
43
|
- - "<"
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
45
|
version: '1.0'
|
|
@@ -75,6 +75,10 @@ files:
|
|
|
75
75
|
- lib/bsv/wallet_interface/proof_store.rb
|
|
76
76
|
- lib/bsv/wallet_interface/proto_wallet.rb
|
|
77
77
|
- lib/bsv/wallet_interface/storage_adapter.rb
|
|
78
|
+
- lib/bsv/wallet_interface/substrates.rb
|
|
79
|
+
- lib/bsv/wallet_interface/substrates/http_wallet_json.rb
|
|
80
|
+
- lib/bsv/wallet_interface/substrates/http_wallet_wire.rb
|
|
81
|
+
- lib/bsv/wallet_interface/substrates/wallet_wire_transceiver.rb
|
|
78
82
|
- lib/bsv/wallet_interface/validators.rb
|
|
79
83
|
- lib/bsv/wallet_interface/version.rb
|
|
80
84
|
- lib/bsv/wallet_interface/wallet_client.rb
|