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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b8425ff5a48801f390149006396bac80983a36231e101ff9e96255b4c702f68
4
- data.tar.gz: 5df7b47ab142634281a76a68de73cd30562a31c4864cddb1ebf5cfe332e60ac0
3
+ metadata.gz: 640f3d2d34c8ef7a9acbaa3680904bae8507d091b251213fcb1fd7d3dd4e1796
4
+ data.tar.gz: f0fdf19e4128bace6be1dd7a09c0c9bf0c63a5a9bb4699e1c3736b8263d5ffc1
5
5
  SHA512:
6
- metadata.gz: 7a8faf79916348e238b86b1006a78cf16ce4bcc0317170c7bd0ba80a2627615a129b6b2a694af884a2492379a29f354994581cd74e54adc62cd6138103906094
7
- data.tar.gz: 9f3f53a96072ed5dd27d46265a54558b3bd8073811d7532458276d4d287f59c5b77714017d024f970b95686f7fca8c1f5fc0891ba32e493cb671287ac84ff74c
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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, _originator: nil)
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]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BSV
4
4
  module WalletInterface
5
- VERSION = '0.1.2'
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  end
@@ -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: MemoryStore)
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: MemoryStore.new, network: 'mainnet', chain_provider: NullChainProvider.new, http_client: nil)
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
- input.unlocking_script = BSV::Script::Script.from_hex(spec[:unlocking_script]) if spec[:unlocking_script]
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
 
@@ -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.2
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-03-30 00:00:00.000000000 Z
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.3'
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.3'
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