bsv-wallet 0.1.2 → 0.2.1
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/lib/bsv/wallet_interface/file_store.rb +170 -0
- data/lib/bsv/wallet_interface/memory_store.rb +1 -0
- data/lib/bsv/wallet_interface/proto_wallet.rb +9 -9
- data/lib/bsv/wallet_interface/version.rb +1 -1
- data/lib/bsv/wallet_interface/wallet_client.rb +24 -4
- data/lib/bsv/wallet_interface.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 640f3d2d34c8ef7a9acbaa3680904bae8507d091b251213fcb1fd7d3dd4e1796
|
|
4
|
+
data.tar.gz: f0fdf19e4128bace6be1dd7a09c0c9bf0c63a5a9bb4699e1c3736b8263d5ffc1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e46a2a8781c325707ec1bfa6de79bb7f1c84ea8a5b904237ad1080d9cf2d22a3ac80d31111304bd9ed3138bd9418ba0efe2d77f0c0554476a011ef7308f68ccb
|
|
7
|
+
data.tar.gz: a238543de60ded0e911eb564c34fb47f4b5367c0570fd4d0fabefa02327caa2141ff9ba6d09308e6478692541446ae367934d095d321421c7ae13e02ac48fd39
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'logger'
|
|
6
|
+
|
|
7
|
+
module BSV
|
|
8
|
+
module Wallet
|
|
9
|
+
# JSON file-backed storage adapter.
|
|
10
|
+
#
|
|
11
|
+
# Persists actions, outputs, and certificates as JSON files in a
|
|
12
|
+
# configurable directory (default: +~/.bsv-wallet/+). Data survives
|
|
13
|
+
# process restarts.
|
|
14
|
+
#
|
|
15
|
+
# Inherits all filtering and pagination logic from {MemoryStore} and
|
|
16
|
+
# adds load-on-init / save-on-mutation.
|
|
17
|
+
#
|
|
18
|
+
# @example Default location
|
|
19
|
+
# store = BSV::Wallet::FileStore.new
|
|
20
|
+
# # Data written to ~/.bsv-wallet/
|
|
21
|
+
#
|
|
22
|
+
# @example Custom directory
|
|
23
|
+
# store = BSV::Wallet::FileStore.new(dir: '/var/lib/my-app/wallet')
|
|
24
|
+
class FileStore < MemoryStore
|
|
25
|
+
DEFAULT_DIR = File.expand_path('~/.bsv-wallet')
|
|
26
|
+
|
|
27
|
+
# @param dir [String] directory for JSON files
|
|
28
|
+
# (default: +~/.bsv-wallet/+ or +BSV_WALLET_DIR+ env var)
|
|
29
|
+
# @param dir [String] directory for JSON files
|
|
30
|
+
# @param logger [Logger, nil] logger for permission warnings (default: Logger to STDERR)
|
|
31
|
+
def initialize(dir: nil, logger: nil)
|
|
32
|
+
super()
|
|
33
|
+
@dir = dir || ENV.fetch('BSV_WALLET_DIR', DEFAULT_DIR)
|
|
34
|
+
@logger = logger || Logger.new($stderr, progname: 'bsv-wallet')
|
|
35
|
+
FileUtils.mkdir_p(@dir, mode: 0o700)
|
|
36
|
+
check_permissions
|
|
37
|
+
load_from_disk
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String] the storage directory path
|
|
41
|
+
attr_reader :dir
|
|
42
|
+
|
|
43
|
+
# --- Mutations: delegate to super, then persist ---
|
|
44
|
+
|
|
45
|
+
def store_action(action_data)
|
|
46
|
+
result = super
|
|
47
|
+
save_actions
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def store_output(output_data)
|
|
52
|
+
result = super
|
|
53
|
+
save_outputs
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete_output(outpoint)
|
|
58
|
+
result = super
|
|
59
|
+
save_outputs if result
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def store_certificate(cert_data)
|
|
64
|
+
result = super
|
|
65
|
+
save_certificates
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def delete_certificate(type:, serial_number:, certifier:)
|
|
70
|
+
result = super
|
|
71
|
+
save_certificates if result
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def check_permissions
|
|
78
|
+
dir_mode = File.stat(@dir).mode & 0o777
|
|
79
|
+
if dir_mode != 0o700
|
|
80
|
+
@logger.warn("Wallet directory #{@dir} has permissions #{format('%04o', dir_mode)} (expected 0700). " \
|
|
81
|
+
'Other users may be able to access wallet data.')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
[actions_path, outputs_path, certificates_path].each do |path|
|
|
85
|
+
next unless File.exist?(path)
|
|
86
|
+
|
|
87
|
+
file_mode = File.stat(path).mode & 0o777
|
|
88
|
+
next if file_mode == 0o600
|
|
89
|
+
|
|
90
|
+
@logger.warn("Wallet file #{path} has permissions #{format('%04o', file_mode)} (expected 0600). " \
|
|
91
|
+
'Other users may be able to read wallet data.')
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def actions_path
|
|
96
|
+
File.join(@dir, 'actions.json')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def outputs_path
|
|
100
|
+
File.join(@dir, 'outputs.json')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def certificates_path
|
|
104
|
+
File.join(@dir, 'certificates.json')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def load_from_disk
|
|
108
|
+
@actions = load_file(actions_path)
|
|
109
|
+
@outputs = load_file(outputs_path)
|
|
110
|
+
@certificates = load_file(certificates_path)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def load_file(path)
|
|
114
|
+
return [] unless File.exist?(path)
|
|
115
|
+
|
|
116
|
+
data = JSON.parse(File.read(path))
|
|
117
|
+
return [] unless data.is_a?(Array)
|
|
118
|
+
|
|
119
|
+
# Symbolise top-level keys for consistency with MemoryStore
|
|
120
|
+
data.map { |entry| symbolise_keys(entry) }
|
|
121
|
+
rescue JSON::ParserError
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def save_actions
|
|
126
|
+
write_file(actions_path, @actions)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def save_outputs
|
|
130
|
+
write_file(outputs_path, @outputs)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def save_certificates
|
|
134
|
+
write_file(certificates_path, @certificates)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def write_file(path, data)
|
|
138
|
+
json = JSON.pretty_generate(stringify_keys_deep(data))
|
|
139
|
+
tmp = "#{path}.tmp"
|
|
140
|
+
File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC, 0o600) { |f| f.write(json) }
|
|
141
|
+
File.rename(tmp, path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def symbolise_keys(hash)
|
|
145
|
+
return hash unless hash.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
hash.each_with_object({}) do |(k, v), result|
|
|
148
|
+
result[k.to_sym] = case v
|
|
149
|
+
when Hash then symbolise_keys(v)
|
|
150
|
+
when Array then v.map { |e| e.is_a?(Hash) ? symbolise_keys(e) : e }
|
|
151
|
+
else v
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def stringify_keys_deep(obj)
|
|
157
|
+
case obj
|
|
158
|
+
when Hash
|
|
159
|
+
obj.each_with_object({}) do |(k, v), result|
|
|
160
|
+
result[k.to_s] = stringify_keys_deep(v)
|
|
161
|
+
end
|
|
162
|
+
when Array
|
|
163
|
+
obj.map { |e| stringify_keys_deep(e) }
|
|
164
|
+
else
|
|
165
|
+
obj
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -91,6 +91,7 @@ module BSV
|
|
|
91
91
|
|
|
92
92
|
def filter_outputs(query)
|
|
93
93
|
results = @outputs
|
|
94
|
+
results = results.select { |o| o[:outpoint] == query[:outpoint] } if query[:outpoint]
|
|
94
95
|
results = results.select { |o| o[:basket] == query[:basket] } if query[:basket]
|
|
95
96
|
if query[:tags]
|
|
96
97
|
mode = query[:tag_query_mode] || 'any'
|
|
@@ -45,7 +45,7 @@ module BSV
|
|
|
45
45
|
# @option args [Boolean] :for_self derive from own identity
|
|
46
46
|
# @param originator [String, nil] FQDN of the originating application
|
|
47
47
|
# @return [Hash] { public_key: String } hex-encoded compressed public key
|
|
48
|
-
def get_public_key(args,
|
|
48
|
+
def get_public_key(args, originator: nil)
|
|
49
49
|
if args[:identity_key]
|
|
50
50
|
{ public_key: @key_deriver.identity_key }
|
|
51
51
|
else
|
|
@@ -69,7 +69,7 @@ module BSV
|
|
|
69
69
|
# @option args [String] :counterparty public key hex, 'self', or 'anyone'
|
|
70
70
|
# @param originator [String, nil] FQDN of the originating application
|
|
71
71
|
# @return [Hash] { ciphertext: Array<Integer> }
|
|
72
|
-
def encrypt(args,
|
|
72
|
+
def encrypt(args, originator: nil)
|
|
73
73
|
sym_key = derive_sym_key(args)
|
|
74
74
|
ciphertext = sym_key.encrypt(bytes_to_string(args[:plaintext]))
|
|
75
75
|
{ ciphertext: string_to_bytes(ciphertext) }
|
|
@@ -84,7 +84,7 @@ module BSV
|
|
|
84
84
|
# @option args [String] :counterparty public key hex, 'self', or 'anyone'
|
|
85
85
|
# @param originator [String, nil] FQDN of the originating application
|
|
86
86
|
# @return [Hash] { plaintext: Array<Integer> }
|
|
87
|
-
def decrypt(args,
|
|
87
|
+
def decrypt(args, originator: nil)
|
|
88
88
|
sym_key = derive_sym_key(args)
|
|
89
89
|
plaintext = sym_key.decrypt(bytes_to_string(args[:ciphertext]))
|
|
90
90
|
{ plaintext: string_to_bytes(plaintext) }
|
|
@@ -99,7 +99,7 @@ module BSV
|
|
|
99
99
|
# @option args [String] :counterparty public key hex, 'self', or 'anyone'
|
|
100
100
|
# @param originator [String, nil] FQDN of the originating application
|
|
101
101
|
# @return [Hash] { hmac: Array<Integer> }
|
|
102
|
-
def create_hmac(args,
|
|
102
|
+
def create_hmac(args, originator: nil)
|
|
103
103
|
sym_key = derive_sym_key(args)
|
|
104
104
|
hmac = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(args[:data]))
|
|
105
105
|
{ hmac: string_to_bytes(hmac) }
|
|
@@ -116,7 +116,7 @@ module BSV
|
|
|
116
116
|
# @param originator [String, nil] FQDN of the originating application
|
|
117
117
|
# @return [Hash] { valid: true }
|
|
118
118
|
# @raise [InvalidHmacError] if the HMAC does not match
|
|
119
|
-
def verify_hmac(args,
|
|
119
|
+
def verify_hmac(args, originator: nil)
|
|
120
120
|
sym_key = derive_sym_key(args)
|
|
121
121
|
expected = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(args[:data]))
|
|
122
122
|
provided = bytes_to_string(args[:hmac])
|
|
@@ -140,7 +140,7 @@ module BSV
|
|
|
140
140
|
# @option args [String] :counterparty public key hex, 'self', or 'anyone'
|
|
141
141
|
# @param originator [String, nil] FQDN of the originating application
|
|
142
142
|
# @return [Hash] { signature: Array<Integer> } DER-encoded signature as byte array
|
|
143
|
-
def create_signature(args,
|
|
143
|
+
def create_signature(args, originator: nil)
|
|
144
144
|
counterparty = args[:counterparty] || 'self'
|
|
145
145
|
priv_key = @key_deriver.derive_private_key(args[:protocol_id], args[:key_id], counterparty)
|
|
146
146
|
|
|
@@ -170,7 +170,7 @@ module BSV
|
|
|
170
170
|
# @param originator [String, nil] FQDN of the originating application
|
|
171
171
|
# @return [Hash] { valid: true }
|
|
172
172
|
# @raise [InvalidSignatureError] if the signature does not verify
|
|
173
|
-
def verify_signature(args,
|
|
173
|
+
def verify_signature(args, originator: nil)
|
|
174
174
|
counterparty = args[:counterparty] || 'self'
|
|
175
175
|
for_self = args[:for_self] || false
|
|
176
176
|
|
|
@@ -208,7 +208,7 @@ module BSV
|
|
|
208
208
|
# @param originator [String, nil] FQDN of the originating application
|
|
209
209
|
# @return [Hash] with :prover, :verifier, :counterparty, :revelation_time,
|
|
210
210
|
# :encrypted_linkage, :encrypted_linkage_proof
|
|
211
|
-
def reveal_counterparty_key_linkage(args,
|
|
211
|
+
def reveal_counterparty_key_linkage(args, originator: nil)
|
|
212
212
|
counterparty = args[:counterparty]
|
|
213
213
|
verifier = args[:verifier]
|
|
214
214
|
|
|
@@ -270,7 +270,7 @@ module BSV
|
|
|
270
270
|
# @param originator [String, nil] FQDN of the originating application
|
|
271
271
|
# @return [Hash] with :prover, :verifier, :counterparty, :protocol_id, :key_id,
|
|
272
272
|
# :encrypted_linkage, :encrypted_linkage_proof, :proof_type
|
|
273
|
-
def reveal_specific_key_linkage(args,
|
|
273
|
+
def reveal_specific_key_linkage(args, originator: nil)
|
|
274
274
|
counterparty = args[:counterparty]
|
|
275
275
|
verifier = args[:verifier]
|
|
276
276
|
protocol_id = args[:protocol_id]
|
|
@@ -36,11 +36,12 @@ module BSV
|
|
|
36
36
|
attr_reader :network
|
|
37
37
|
|
|
38
38
|
# @param key [BSV::Primitives::PrivateKey, String, KeyDeriver] signing key
|
|
39
|
-
# @param storage [StorageAdapter] persistence adapter (default:
|
|
39
|
+
# @param storage [StorageAdapter] persistence adapter (default: FileStore).
|
|
40
|
+
# Use +storage: MemoryStore.new+ for tests.
|
|
40
41
|
# @param network [String] 'mainnet' (default) or 'testnet'
|
|
41
42
|
# @param chain_provider [ChainProvider] blockchain data provider (default: NullChainProvider)
|
|
42
43
|
# @param http_client [#request, nil] injectable HTTP client for certificate issuance
|
|
43
|
-
def initialize(key, storage:
|
|
44
|
+
def initialize(key, storage: FileStore.new, network: 'mainnet', chain_provider: NullChainProvider.new, http_client: nil)
|
|
44
45
|
super(key)
|
|
45
46
|
@storage = storage
|
|
46
47
|
@network = network
|
|
@@ -482,8 +483,17 @@ module BSV
|
|
|
482
483
|
)
|
|
483
484
|
|
|
484
485
|
wire_source(input, txid_hex, output_index, beef) if beef
|
|
485
|
-
|
|
486
|
-
|
|
486
|
+
wire_source_from_storage(input, spec[:outpoint]) if input.source_satoshis.nil? || input.source_locking_script.nil?
|
|
487
|
+
|
|
488
|
+
case spec[:unlocking_script]
|
|
489
|
+
when BSV::Transaction::UnlockingScriptTemplate
|
|
490
|
+
input.unlocking_script_template = spec[:unlocking_script]
|
|
491
|
+
when String
|
|
492
|
+
input.unlocking_script = BSV::Script::Script.from_hex(spec[:unlocking_script])
|
|
493
|
+
when nil then nil
|
|
494
|
+
else
|
|
495
|
+
raise InvalidParameterError.new('unlocking_script', 'a hex String or UnlockingScriptTemplate')
|
|
496
|
+
end
|
|
487
497
|
|
|
488
498
|
tx.add_input(input)
|
|
489
499
|
end
|
|
@@ -503,6 +513,15 @@ module BSV
|
|
|
503
513
|
input.source_locking_script = source_tx.outputs[output_index].locking_script
|
|
504
514
|
end
|
|
505
515
|
|
|
516
|
+
def wire_source_from_storage(input, outpoint)
|
|
517
|
+
results = @storage.find_outputs({ outpoint: outpoint, limit: 1 })
|
|
518
|
+
stored = results.first
|
|
519
|
+
return unless stored
|
|
520
|
+
|
|
521
|
+
input.source_satoshis = stored[:satoshis]
|
|
522
|
+
input.source_locking_script = BSV::Script::Script.from_hex(stored[:locking_script])
|
|
523
|
+
end
|
|
524
|
+
|
|
506
525
|
def build_outputs(tx, outputs)
|
|
507
526
|
outputs.each do |spec|
|
|
508
527
|
output = BSV::Transaction::TransactionOutput.new(
|
|
@@ -539,6 +558,7 @@ module BSV
|
|
|
539
558
|
end
|
|
540
559
|
|
|
541
560
|
def finalize_action(tx, args)
|
|
561
|
+
tx.sign_all if tx.inputs.any?(&:unlocking_script_template)
|
|
542
562
|
txid = tx.txid_hex
|
|
543
563
|
status = args.dig(:options, :no_send) ? 'nosend' : 'completed'
|
|
544
564
|
|
data/lib/bsv/wallet_interface.rb
CHANGED
|
@@ -13,6 +13,7 @@ module BSV
|
|
|
13
13
|
autoload :Validators, 'bsv/wallet_interface/validators'
|
|
14
14
|
autoload :StorageAdapter, 'bsv/wallet_interface/storage_adapter'
|
|
15
15
|
autoload :MemoryStore, 'bsv/wallet_interface/memory_store'
|
|
16
|
+
autoload :FileStore, 'bsv/wallet_interface/file_store'
|
|
16
17
|
autoload :ChainProvider, 'bsv/wallet_interface/chain_provider'
|
|
17
18
|
autoload :NullChainProvider, 'bsv/wallet_interface/null_chain_provider'
|
|
18
19
|
autoload :WalletClient, 'bsv/wallet_interface/wallet_client'
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bsv-wallet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.
|
|
32
|
+
version: '0.4'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.
|
|
39
|
+
version: '0.4'
|
|
40
40
|
description: Implements the BRC-100 standard wallet-to-application interface for the
|
|
41
41
|
BSV Blockchain.
|
|
42
42
|
executables: []
|
|
@@ -52,6 +52,7 @@ files:
|
|
|
52
52
|
- lib/bsv/wallet_interface/errors/invalid_signature_error.rb
|
|
53
53
|
- lib/bsv/wallet_interface/errors/unsupported_action_error.rb
|
|
54
54
|
- lib/bsv/wallet_interface/errors/wallet_error.rb
|
|
55
|
+
- lib/bsv/wallet_interface/file_store.rb
|
|
55
56
|
- lib/bsv/wallet_interface/interface.rb
|
|
56
57
|
- lib/bsv/wallet_interface/key_deriver.rb
|
|
57
58
|
- lib/bsv/wallet_interface/memory_store.rb
|