bsv-sdk 0.14.0 → 0.16.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 +43 -0
- data/README.md +14 -2
- data/lib/bsv/auth/auth_middleware.rb +6 -6
- data/lib/bsv/auth/certificate.rb +16 -16
- data/lib/bsv/auth/master_certificate.rb +5 -5
- data/lib/bsv/auth/nonce.rb +13 -13
- data/lib/bsv/auth/peer.rb +53 -53
- data/lib/bsv/auth/verifiable_certificate.rb +1 -1
- data/lib/bsv/identity/client.rb +26 -32
- data/lib/bsv/mcp/tools/broadcast_p2pkh.rb +17 -11
- data/lib/bsv/mcp/tools/check_balance.rb +16 -4
- data/lib/bsv/mcp/tools/fetch_tx.rb +11 -4
- data/lib/bsv/mcp/tools/fetch_utxos.rb +16 -4
- data/lib/bsv/network/arc.rb +13 -153
- data/lib/bsv/network/whats_on_chain.rb +13 -107
- data/lib/bsv/overlay/admin_token_template.rb +4 -4
- data/lib/bsv/primitives/base58.rb +2 -1
- data/lib/bsv/primitives/curve.rb +37 -12
- data/lib/bsv/primitives/ecdsa.rb +4 -4
- data/lib/bsv/primitives/openssl_ec_shim.rb +32 -5
- data/lib/bsv/primitives/private_key.rb +2 -2
- data/lib/bsv/primitives/public_key.rb +1 -1
- data/lib/bsv/primitives/schnorr.rb +4 -4
- data/lib/bsv/primitives/secp256k1.rb +4 -595
- data/lib/bsv/primitives/signature.rb +2 -0
- data/lib/bsv/primitives/signed_message.rb +6 -5
- data/lib/bsv/registry/client.rb +23 -27
- data/lib/bsv/script/push_drop_template.rb +4 -4
- data/lib/bsv/secp256k1_native.bundle +0 -0
- data/lib/bsv/version.rb +1 -1
- data/lib/bsv/wallet/errors.rb +47 -0
- data/lib/bsv/wallet/interface/brc100.rb +267 -0
- data/lib/bsv/wallet/interface.rb +9 -0
- data/lib/bsv/wallet/proto_wallet/key_deriver.rb +150 -0
- data/lib/bsv/wallet/proto_wallet/validators.rb +74 -0
- data/lib/bsv/wallet/proto_wallet.rb +321 -0
- data/lib/bsv/wallet.rb +16 -0
- data/lib/bsv-sdk.rb +4 -1
- metadata +37 -1
data/lib/bsv/registry/client.rb
CHANGED
|
@@ -66,18 +66,16 @@ module BSV
|
|
|
66
66
|
|
|
67
67
|
basket_name = basket_name_for(definition_type)
|
|
68
68
|
create_result = @wallet.create_action(
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
options: { randomize_outputs: false }
|
|
80
|
-
},
|
|
69
|
+
description: "Register a new #{definition_type} definition",
|
|
70
|
+
outputs: [
|
|
71
|
+
{
|
|
72
|
+
satoshis: Constants::TOKEN_AMOUNT,
|
|
73
|
+
locking_script: locking_script.to_hex,
|
|
74
|
+
output_description: "New #{definition_type} registration token",
|
|
75
|
+
basket: basket_name
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
options: { randomize_outputs: false },
|
|
81
79
|
originator: @originator
|
|
82
80
|
)
|
|
83
81
|
|
|
@@ -122,7 +120,7 @@ module BSV
|
|
|
122
120
|
def list_own_registry_entries(definition_type)
|
|
123
121
|
basket_name = basket_name_for(definition_type)
|
|
124
122
|
list_result = @wallet.list_outputs(
|
|
125
|
-
|
|
123
|
+
basket: basket_name, include: 'entire transactions',
|
|
126
124
|
originator: @originator
|
|
127
125
|
)
|
|
128
126
|
|
|
@@ -160,18 +158,16 @@ module BSV
|
|
|
160
158
|
outpoint = "#{registered_definition.txid}.#{registered_definition.output_index}"
|
|
161
159
|
|
|
162
160
|
create_result = @wallet.create_action(
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
options: { randomize_outputs: false, no_send: true }
|
|
174
|
-
},
|
|
161
|
+
description: "Revoke #{definition_type} definition: #{item_identifier(registered_definition)}",
|
|
162
|
+
input_beef: registered_definition.beef,
|
|
163
|
+
inputs: [
|
|
164
|
+
{
|
|
165
|
+
outpoint: outpoint,
|
|
166
|
+
unlocking_script_length: BSV::Script::PushDropTemplate::Unlocker::ESTIMATED_LENGTH,
|
|
167
|
+
input_description: "Revoking #{definition_type} token"
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
options: { randomize_outputs: false, no_send: true },
|
|
175
171
|
originator: @originator
|
|
176
172
|
)
|
|
177
173
|
|
|
@@ -210,7 +206,7 @@ module BSV
|
|
|
210
206
|
def identity_key
|
|
211
207
|
return @identity_key if defined?(@identity_key)
|
|
212
208
|
|
|
213
|
-
result = @wallet.get_public_key(
|
|
209
|
+
result = @wallet.get_public_key(identity_key: true, originator: @originator)
|
|
214
210
|
@identity_key = result[:public_key] || result['public_key'] || result['publicKey'] || result[:publicKey]
|
|
215
211
|
end
|
|
216
212
|
|
|
@@ -561,7 +557,7 @@ module BSV
|
|
|
561
557
|
#
|
|
562
558
|
# @return [Symbol] :mainnet or :testnet
|
|
563
559
|
def wallet_network
|
|
564
|
-
result = @wallet.get_network(
|
|
560
|
+
result = @wallet.get_network(originator: @originator)
|
|
565
561
|
net_str = result[:network] || result['network'] || 'mainnet'
|
|
566
562
|
net_str.to_sym
|
|
567
563
|
end
|
|
@@ -108,14 +108,14 @@ module BSV
|
|
|
108
108
|
counterparty: @counterparty
|
|
109
109
|
}
|
|
110
110
|
orig_kw = @originator ? { originator: @originator } : {}
|
|
111
|
-
result = @wallet.create_signature(sig_args, **orig_kw)
|
|
111
|
+
result = @wallet.create_signature(**sig_args, **orig_kw)
|
|
112
112
|
|
|
113
113
|
sig_bytes = result[:signature].pack('C*')
|
|
114
114
|
sig_with_hashtype = sig_bytes + [sighash_type].pack('C')
|
|
115
115
|
|
|
116
116
|
# Fetch the derived public key so the P2PKH unlock can include it
|
|
117
117
|
pub_args = { protocol_id: @protocol_id, key_id: @key_id, counterparty: @counterparty }
|
|
118
|
-
pub_result = @wallet.get_public_key(pub_args, **orig_kw)
|
|
118
|
+
pub_result = @wallet.get_public_key(**pub_args, **orig_kw)
|
|
119
119
|
pubkey_bytes = [pub_result[:public_key]].pack('H*')
|
|
120
120
|
|
|
121
121
|
BSV::Script::Script.pushdrop_unlock(
|
|
@@ -176,7 +176,7 @@ module BSV
|
|
|
176
176
|
else
|
|
177
177
|
pub_args = { protocol_id: protocol_id, key_id: key_id, counterparty: counterparty }
|
|
178
178
|
orig_kw = @originator ? { originator: @originator } : {}
|
|
179
|
-
pub_result = @wallet.get_public_key(pub_args, **orig_kw)
|
|
179
|
+
pub_result = @wallet.get_public_key(**pub_args, **orig_kw)
|
|
180
180
|
[pub_result[:public_key]].pack('H*')
|
|
181
181
|
end
|
|
182
182
|
|
|
@@ -187,7 +187,7 @@ module BSV
|
|
|
187
187
|
data_to_sign = all_fields.reduce(''.b) { |acc, f| acc + f }.unpack('C*')
|
|
188
188
|
sig_args = { data: data_to_sign, protocol_id: protocol_id, key_id: key_id, counterparty: counterparty }
|
|
189
189
|
orig_kw = @originator ? { originator: @originator } : {}
|
|
190
|
-
sig_result = @wallet.create_signature(sig_args, **orig_kw)
|
|
190
|
+
sig_result = @wallet.create_signature(**sig_args, **orig_kw)
|
|
191
191
|
all_fields << sig_result[:signature].pack('C*')
|
|
192
192
|
end
|
|
193
193
|
|
|
Binary file
|
data/lib/bsv/version.rb
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
# Base error for all wallet operations. Carries a machine-readable code
|
|
6
|
+
# per the BRC-100 error structure.
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :code
|
|
9
|
+
|
|
10
|
+
def initialize(message, code = 1)
|
|
11
|
+
@code = code
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Raised when a required parameter is missing or invalid.
|
|
17
|
+
class InvalidParameterError < Error
|
|
18
|
+
attr_reader :parameter
|
|
19
|
+
|
|
20
|
+
def initialize(parameter, must_be = 'valid')
|
|
21
|
+
@parameter = parameter
|
|
22
|
+
super("the #{parameter} parameter must be #{must_be}", 6)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Raised when an HMAC fails to verify.
|
|
27
|
+
class InvalidHmacError < Error
|
|
28
|
+
def initialize(message = 'the provided HMAC is invalid')
|
|
29
|
+
super(message, 3)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Raised when a signature fails to verify.
|
|
34
|
+
class InvalidSignatureError < Error
|
|
35
|
+
def initialize(message = 'the provided signature is invalid')
|
|
36
|
+
super(message, 4)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
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
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
# BRC-100 abstract wallet interface — all 28 methods.
|
|
6
|
+
#
|
|
7
|
+
# Include this module and override the methods your implementation supports.
|
|
8
|
+
# Unimplemented methods raise +NotImplementedError+.
|
|
9
|
+
#
|
|
10
|
+
# The 28 methods are grouped into six functional areas matching
|
|
11
|
+
# the BRC-100 Interface Structure specification.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# class MyWallet
|
|
15
|
+
# include BSV::Wallet::Interface::BRC100
|
|
16
|
+
#
|
|
17
|
+
# def get_height(originator: nil)
|
|
18
|
+
# { height: 800_000 }
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
module Interface
|
|
22
|
+
module BRC100
|
|
23
|
+
# --- Transaction Operations (codes 1-7) ---
|
|
24
|
+
|
|
25
|
+
# Creates a new Bitcoin transaction.
|
|
26
|
+
#
|
|
27
|
+
# @param description [String] human-readable description (5-50 chars)
|
|
28
|
+
# @param inputs [Array<Hash>] optional inputs to consume
|
|
29
|
+
# - :outpoint [String] txid.index being consumed
|
|
30
|
+
# - :unlocking_script [String] hex unlocking script
|
|
31
|
+
# - :unlocking_script_length [Integer] length, if script provided later via {#sign_action}
|
|
32
|
+
# - :input_description [String] what this input consumes (5-50 chars)
|
|
33
|
+
# - :sequence_number [Integer] optional sequence number
|
|
34
|
+
# @param outputs [Array<Hash>] optional outputs to create
|
|
35
|
+
# - :locking_script [String] hex locking script
|
|
36
|
+
# - :satoshis [Integer] output value
|
|
37
|
+
# - :output_description [String] what this output represents (5-50 chars)
|
|
38
|
+
# - :basket [String] optional basket name for UTXO tracking
|
|
39
|
+
# - :custom_instructions [String] application-specific context
|
|
40
|
+
# - :tags [Array<String>] output tags for filtering
|
|
41
|
+
# @return [Hash] :txid, :tx, :no_send_change, :send_with_results, :signable_transaction
|
|
42
|
+
def create_action(description:, input_beef: nil, inputs: nil, outputs: nil,
|
|
43
|
+
lock_time: nil, version: nil, labels: nil,
|
|
44
|
+
sign_and_process: true, accept_delayed_broadcast: true,
|
|
45
|
+
trust_self: nil, known_txids: nil, return_txid_only: false,
|
|
46
|
+
no_send: false, no_send_change: nil, send_with: nil,
|
|
47
|
+
randomize_outputs: true, originator: nil)
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Signs a transaction previously created with {#create_action}.
|
|
52
|
+
#
|
|
53
|
+
# @param spends [Hash{Integer => Hash}] input index => { unlocking_script:, sequence_number: }
|
|
54
|
+
# @param reference [String] reference returned by {#create_action}
|
|
55
|
+
def sign_action(spends:, reference:,
|
|
56
|
+
accept_delayed_broadcast: true, return_txid_only: false,
|
|
57
|
+
no_send: false, send_with: nil, originator: nil)
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Aborts a transaction that has not yet been finalized.
|
|
62
|
+
def abort_action(reference:, originator: nil)
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Lists transactions matching the specified labels.
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash] :total_actions, :actions
|
|
69
|
+
def list_actions(labels:, label_query_mode: :any,
|
|
70
|
+
include_labels: false, include_inputs: false,
|
|
71
|
+
include_input_source_locking_scripts: false,
|
|
72
|
+
include_input_unlocking_scripts: false,
|
|
73
|
+
include_outputs: false, include_output_locking_scripts: false,
|
|
74
|
+
limit: 10, offset: 0, seek_permission: true, originator: nil)
|
|
75
|
+
raise NotImplementedError
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Internalizes a transaction — labels it, pays outputs to the wallet balance,
|
|
79
|
+
# inserts outputs into baskets, and/or tags them.
|
|
80
|
+
#
|
|
81
|
+
# @param tx [Array<Integer>] Atomic BEEF-formatted transaction (byte array)
|
|
82
|
+
# @param outputs [Array<Hash>] metadata per output
|
|
83
|
+
# - :output_index [Integer] index within the transaction
|
|
84
|
+
# - :protocol [Symbol] :wallet_payment or :basket_insertion
|
|
85
|
+
# - :payment_remittance [Hash] for payments: { derivation_prefix:, derivation_suffix:, sender_identity_key: }
|
|
86
|
+
# - :insertion_remittance [Hash] for insertions: { basket:, custom_instructions:, tags: }
|
|
87
|
+
def internalize_action(tx:, outputs:, description:, labels: nil,
|
|
88
|
+
seek_permission: true, originator: nil)
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Lists spendable outputs in a basket.
|
|
93
|
+
#
|
|
94
|
+
# @param include [Symbol] nil, :locking_scripts, or :entire_transactions
|
|
95
|
+
# @return [Hash] :total_outputs, :beef, :outputs
|
|
96
|
+
def list_outputs(basket:, tags: nil, tag_query_mode: :any, include: nil,
|
|
97
|
+
include_custom_instructions: false, include_tags: false,
|
|
98
|
+
include_labels: false, limit: 10, offset: 0,
|
|
99
|
+
seek_permission: true, originator: nil)
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Removes an output from a basket without spending it.
|
|
104
|
+
def relinquish_output(basket:, output:, originator: nil)
|
|
105
|
+
raise NotImplementedError
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# --- Public Key Management (codes 8-10) ---
|
|
109
|
+
|
|
110
|
+
# Retrieves a derived or identity public key.
|
|
111
|
+
#
|
|
112
|
+
# @param protocol_id [Array(Integer, String)] security level (0-2) and protocol string
|
|
113
|
+
# @param counterparty [String] public key hex, 'self', or 'anyone'
|
|
114
|
+
# @return [Hash] :public_key
|
|
115
|
+
def get_public_key(identity_key: false, protocol_id: nil, key_id: nil,
|
|
116
|
+
privileged: false, privileged_reason: nil,
|
|
117
|
+
counterparty: nil, for_self: false,
|
|
118
|
+
seek_permission: true, originator: nil)
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Reveals key linkage with a counterparty to a verifier, across all interactions.
|
|
123
|
+
def reveal_counterparty_key_linkage(counterparty:, verifier:,
|
|
124
|
+
privileged: false, privileged_reason: nil,
|
|
125
|
+
originator: nil)
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Reveals key linkage for a specific protocol and key interaction.
|
|
130
|
+
def reveal_specific_key_linkage(counterparty:, verifier:, protocol_id:, key_id:,
|
|
131
|
+
privileged: false, privileged_reason: nil,
|
|
132
|
+
originator: nil)
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# --- Cryptography Operations (codes 11-16) ---
|
|
137
|
+
|
|
138
|
+
# Encrypts plaintext using derived keys.
|
|
139
|
+
def encrypt(plaintext:, protocol_id:, key_id:,
|
|
140
|
+
privileged: false, privileged_reason: nil,
|
|
141
|
+
counterparty: nil, seek_permission: true, originator: nil)
|
|
142
|
+
raise NotImplementedError
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Decrypts ciphertext using derived keys.
|
|
146
|
+
def decrypt(ciphertext:, protocol_id:, key_id:,
|
|
147
|
+
privileged: false, privileged_reason: nil,
|
|
148
|
+
counterparty: nil, seek_permission: true, originator: nil)
|
|
149
|
+
raise NotImplementedError
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Creates an HMAC for the provided data.
|
|
153
|
+
def create_hmac(data:, protocol_id:, key_id:,
|
|
154
|
+
privileged: false, privileged_reason: nil,
|
|
155
|
+
counterparty: nil, seek_permission: true, originator: nil)
|
|
156
|
+
raise NotImplementedError
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Verifies an HMAC against the provided data.
|
|
160
|
+
def verify_hmac(data:, hmac:, protocol_id:, key_id:,
|
|
161
|
+
privileged: false, privileged_reason: nil,
|
|
162
|
+
counterparty: nil, seek_permission: true, originator: nil)
|
|
163
|
+
raise NotImplementedError
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Creates a digital signature (ECDSA) for data or a pre-computed hash.
|
|
167
|
+
def create_signature(protocol_id:, key_id:, data: nil, hash_to_directly_sign: nil,
|
|
168
|
+
privileged: false, privileged_reason: nil,
|
|
169
|
+
counterparty: nil, seek_permission: true, originator: nil)
|
|
170
|
+
raise NotImplementedError
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Verifies a digital signature against data or a pre-computed hash.
|
|
174
|
+
def verify_signature(signature:, protocol_id:, key_id:, data: nil,
|
|
175
|
+
hash_to_directly_verify: nil,
|
|
176
|
+
privileged: false, privileged_reason: nil,
|
|
177
|
+
counterparty: nil, for_self: false,
|
|
178
|
+
seek_permission: true, originator: nil)
|
|
179
|
+
raise NotImplementedError
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# --- Identity and Certificate Management (codes 17-22) ---
|
|
183
|
+
|
|
184
|
+
# Acquires an identity certificate from a certifier or by direct receipt.
|
|
185
|
+
#
|
|
186
|
+
# @param acquisition_protocol [Symbol] :direct or :issuance
|
|
187
|
+
# @param fields [Hash{String => String}] certificate field names to values
|
|
188
|
+
def acquire_certificate(type:, certifier:, acquisition_protocol:, fields:,
|
|
189
|
+
serial_number: nil, revocation_outpoint: nil,
|
|
190
|
+
signature: nil, certifier_url: nil,
|
|
191
|
+
keyring_revealer: nil, keyring_for_subject: nil,
|
|
192
|
+
privileged: false, privileged_reason: nil, originator: nil)
|
|
193
|
+
raise NotImplementedError
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Lists identity certificates filtered by certifier(s) and type(s).
|
|
197
|
+
def list_certificates(certifiers:, types:, limit: 10, offset: 0,
|
|
198
|
+
privileged: false, privileged_reason: nil, originator: nil)
|
|
199
|
+
raise NotImplementedError
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Proves select fields of a certificate to a verifier.
|
|
203
|
+
#
|
|
204
|
+
# @param certificate [Hash] the full certificate (type, subject, serial_number,
|
|
205
|
+
# certifier, revocation_outpoint, signature, fields)
|
|
206
|
+
# @param fields_to_reveal [Array<String>] field names to disclose
|
|
207
|
+
def prove_certificate(certificate:, fields_to_reveal:, verifier:,
|
|
208
|
+
privileged: false, privileged_reason: nil, originator: nil)
|
|
209
|
+
raise NotImplementedError
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Removes a certificate from the wallet.
|
|
213
|
+
def relinquish_certificate(type:, serial_number:, certifier:, originator: nil)
|
|
214
|
+
raise NotImplementedError
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Discovers certificates issued to a given identity key.
|
|
218
|
+
def discover_by_identity_key(identity_key:, limit: 10, offset: 0,
|
|
219
|
+
seek_permission: true, originator: nil)
|
|
220
|
+
raise NotImplementedError
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Discovers certificates matching specific attribute values.
|
|
224
|
+
#
|
|
225
|
+
# @param attributes [Hash{String => String}] field name/value pairs to match
|
|
226
|
+
def discover_by_attributes(attributes:, limit: 10, offset: 0,
|
|
227
|
+
seek_permission: true, originator: nil)
|
|
228
|
+
raise NotImplementedError
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# --- Authentication (codes 23-24) ---
|
|
232
|
+
|
|
233
|
+
# Checks whether the user is authenticated.
|
|
234
|
+
def authenticated?(originator: nil)
|
|
235
|
+
raise NotImplementedError
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Blocks until the user is authenticated.
|
|
239
|
+
def wait_for_authentication(originator: nil)
|
|
240
|
+
raise NotImplementedError
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# --- Blockchain and Network Data (codes 25-28) ---
|
|
244
|
+
|
|
245
|
+
# Returns the current blockchain height.
|
|
246
|
+
def get_height(originator: nil)
|
|
247
|
+
raise NotImplementedError
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Returns the 80-byte block header at the given height.
|
|
251
|
+
def get_header_for_height(height:, originator: nil)
|
|
252
|
+
raise NotImplementedError
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns the network (:mainnet or :testnet).
|
|
256
|
+
def get_network(originator: nil)
|
|
257
|
+
raise NotImplementedError
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Returns the wallet version string.
|
|
261
|
+
def get_version(originator: nil)
|
|
262
|
+
raise NotImplementedError
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module BSV
|
|
6
|
+
module Wallet
|
|
7
|
+
class ProtoWallet
|
|
8
|
+
# BRC-42/43 key derivation.
|
|
9
|
+
#
|
|
10
|
+
# Derives child keys from a root private key using BKDS (BSV Key Derivation
|
|
11
|
+
# Scheme). Supports protocol IDs, key IDs, counterparties, and security
|
|
12
|
+
# levels as defined in BRC-43.
|
|
13
|
+
class KeyDeriver
|
|
14
|
+
include Validators
|
|
15
|
+
|
|
16
|
+
ANYONE_BN = OpenSSL::BN.new(1)
|
|
17
|
+
|
|
18
|
+
attr_reader :root_key
|
|
19
|
+
|
|
20
|
+
# @param root_key [BSV::Primitives::PrivateKey, String] a private key or 'anyone'
|
|
21
|
+
def initialize(root_key)
|
|
22
|
+
@root_key = if root_key == 'anyone'
|
|
23
|
+
BSV::Primitives::PrivateKey.new(ANYONE_BN)
|
|
24
|
+
elsif root_key.is_a?(BSV::Primitives::PrivateKey)
|
|
25
|
+
root_key
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "expected a BSV::Primitives::PrivateKey or 'anyone', got #{root_key.class}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the identity public key as a hex string.
|
|
32
|
+
# @return [String] 66-character compressed public key hex
|
|
33
|
+
def identity_key
|
|
34
|
+
@identity_key ||= @root_key.public_key.to_hex
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Derives a public key using BRC-42 key derivation.
|
|
38
|
+
#
|
|
39
|
+
# @param protocol_id [Array] [security_level, protocol_name]
|
|
40
|
+
# @param key_id [String] key identifier
|
|
41
|
+
# @param counterparty [String] public key hex, 'self', or 'anyone'
|
|
42
|
+
# @param for_self [Boolean] derive from own identity rather than counterparty's
|
|
43
|
+
# @return [BSV::Primitives::PublicKey]
|
|
44
|
+
def derive_public_key(protocol_id, key_id, counterparty, for_self: false)
|
|
45
|
+
Validators.validate_protocol_id!(protocol_id)
|
|
46
|
+
Validators.validate_key_id!(key_id)
|
|
47
|
+
invoice = compute_invoice_number(protocol_id, key_id)
|
|
48
|
+
counterparty_pub = resolve_counterparty(counterparty)
|
|
49
|
+
|
|
50
|
+
if for_self
|
|
51
|
+
@root_key.derive_child(counterparty_pub, invoice).public_key
|
|
52
|
+
else
|
|
53
|
+
counterparty_pub.derive_child(@root_key, invoice)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Derives a private key using BRC-42 key derivation.
|
|
58
|
+
#
|
|
59
|
+
# @param protocol_id [Array] [security_level, protocol_name]
|
|
60
|
+
# @param key_id [String] key identifier
|
|
61
|
+
# @param counterparty [String] public key hex, 'self', or 'anyone'
|
|
62
|
+
# @return [BSV::Primitives::PrivateKey]
|
|
63
|
+
def derive_private_key(protocol_id, key_id, counterparty)
|
|
64
|
+
Validators.validate_protocol_id!(protocol_id)
|
|
65
|
+
Validators.validate_key_id!(key_id)
|
|
66
|
+
invoice = compute_invoice_number(protocol_id, key_id)
|
|
67
|
+
counterparty_pub = resolve_counterparty(counterparty)
|
|
68
|
+
@root_key.derive_child(counterparty_pub, invoice)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Derives a symmetric key for encryption/HMAC operations.
|
|
72
|
+
#
|
|
73
|
+
# Uses ECDH between the derived private and public child keys to
|
|
74
|
+
# produce a shared secret, then uses the X-coordinate as the key.
|
|
75
|
+
#
|
|
76
|
+
# @param protocol_id [Array] [security_level, protocol_name]
|
|
77
|
+
# @param key_id [String] key identifier
|
|
78
|
+
# @param counterparty [String] public key hex, 'self', or 'anyone'
|
|
79
|
+
# @return [BSV::Primitives::SymmetricKey]
|
|
80
|
+
def derive_symmetric_key(protocol_id, key_id, counterparty)
|
|
81
|
+
Validators.validate_protocol_id!(protocol_id)
|
|
82
|
+
Validators.validate_key_id!(key_id)
|
|
83
|
+
invoice = compute_invoice_number(protocol_id, key_id)
|
|
84
|
+
counterparty_pub = resolve_counterparty(counterparty)
|
|
85
|
+
|
|
86
|
+
derived_private = @root_key.derive_child(counterparty_pub, invoice)
|
|
87
|
+
derived_public = counterparty_pub.derive_child(@root_key, invoice)
|
|
88
|
+
|
|
89
|
+
BSV::Primitives::SymmetricKey.from_ecdh(derived_private, derived_public)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Reveals the ECDH shared secret between this wallet and a counterparty.
|
|
93
|
+
# Used for BRC-69 Method 1 (counterparty key linkage).
|
|
94
|
+
#
|
|
95
|
+
# @param counterparty [String] public key hex (not 'self')
|
|
96
|
+
# @return [String] compressed shared secret bytes
|
|
97
|
+
def reveal_counterparty_secret(counterparty)
|
|
98
|
+
raise InvalidParameterError.new('counterparty', 'not "self" for key linkage revelation') if counterparty == 'self'
|
|
99
|
+
|
|
100
|
+
counterparty_pub = resolve_counterparty(counterparty)
|
|
101
|
+
@root_key.derive_shared_secret(counterparty_pub).compressed
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reveals the specific key offset for a particular derived key.
|
|
105
|
+
# Used for BRC-69 Method 2 (specific key linkage).
|
|
106
|
+
#
|
|
107
|
+
# @param counterparty [String] public key hex
|
|
108
|
+
# @param protocol_id [Array] [security_level, protocol_name]
|
|
109
|
+
# @param key_id [String] key identifier
|
|
110
|
+
# @return [String] HMAC-SHA256 bytes (the key offset)
|
|
111
|
+
def reveal_specific_secret(counterparty, protocol_id, key_id)
|
|
112
|
+
Validators.validate_protocol_id!(protocol_id)
|
|
113
|
+
Validators.validate_key_id!(key_id)
|
|
114
|
+
counterparty_pub = resolve_counterparty(counterparty)
|
|
115
|
+
shared = @root_key.derive_shared_secret(counterparty_pub)
|
|
116
|
+
invoice = compute_invoice_number(protocol_id, key_id)
|
|
117
|
+
BSV::Primitives::Digest.hmac_sha256(shared.compressed, invoice.encode('UTF-8'))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Resolves a counterparty identifier to a PublicKey.
|
|
123
|
+
#
|
|
124
|
+
# @param counterparty [String] 'self', 'anyone', or a hex public key
|
|
125
|
+
# @return [BSV::Primitives::PublicKey]
|
|
126
|
+
def resolve_counterparty(counterparty)
|
|
127
|
+
case counterparty
|
|
128
|
+
when 'self'
|
|
129
|
+
@root_key.public_key
|
|
130
|
+
when 'anyone'
|
|
131
|
+
BSV::Primitives::PrivateKey.new(ANYONE_BN).public_key
|
|
132
|
+
else
|
|
133
|
+
Validators.validate_counterparty!(counterparty)
|
|
134
|
+
BSV::Primitives::PublicKey.from_hex(counterparty)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Computes the invoice number from a protocol ID and key ID.
|
|
139
|
+
# Format: "#{security_level}-#{protocol_name}-#{key_id}"
|
|
140
|
+
#
|
|
141
|
+
# @param protocol_id [Array] [security_level, protocol_name]
|
|
142
|
+
# @param key_id [String]
|
|
143
|
+
# @return [String]
|
|
144
|
+
def compute_invoice_number(protocol_id, key_id)
|
|
145
|
+
"#{protocol_id[0]}-#{protocol_id[1].downcase.strip}-#{key_id}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BSV
|
|
4
|
+
module Wallet
|
|
5
|
+
class ProtoWallet
|
|
6
|
+
# Validation helpers for BRC-100 wallet method parameters.
|
|
7
|
+
#
|
|
8
|
+
# Provides the subset of validators required by KeyDeriver and ProtoWallet.
|
|
9
|
+
# Raises +InvalidParameterError+ for any invalid input.
|
|
10
|
+
module Validators
|
|
11
|
+
module_function
|
|
12
|
+
|
|
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
|
+
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?(' ')
|
|
36
|
+
end
|
|
37
|
+
|
|
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
|
+
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
|
|
49
|
+
end
|
|
50
|
+
|
|
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
|
+
def validate_counterparty!(counterparty)
|
|
56
|
+
return if %w[self anyone].include?(counterparty)
|
|
57
|
+
|
|
58
|
+
validate_pub_key_hex!(counterparty, 'counterparty')
|
|
59
|
+
end
|
|
60
|
+
|
|
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
|
+
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/)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|