Skip to content

Commit 9f10268

Browse files
authored
Merge pull request #417 from sgbett/feat/390-attest-create-action
feat: rework bsv-attest to use wallet create_action
2 parents 5d27b0c + 8f124c3 commit 9f10268

File tree

11 files changed

+282
-68
lines changed

11 files changed

+282
-68
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Plan: Rework bsv-attest to use wallet create_action (#390)
2+
3+
## Context
4+
5+
`bsv-attest` v0.1.0 was written before `bsv-wallet` existed. Its `publish` method calls `wallet.fund_and_sign(tx)` (doesn't exist) and requires a separate `broadcaster:`. The x402-rack companion gem demonstrates the correct pattern: delegate to `wallet.create_action`, letting the wallet handle funding, signing, broadcasting, and UTXO state.
6+
7+
This is an integration rework — the core hash/verify logic is sound and unchanged.
8+
9+
The gem's scope is deliberately narrow: OP_RETURN attestation with the simplicity that entails. Hash data, publish the hash, verify it by txid. No BEEF, no merkle proofs, no proof documents. A future archival service can layer proof-based verification on top using the wallet's BEEF output directly — that's not this gem's concern.
10+
11+
## Tasks
12+
13+
### Task 1: Update Configuration — remove `broadcaster:`, keep `wallet:` and `provider:`
14+
15+
**File:** `gem/bsv-attest/lib/bsv/attest/configuration.rb`
16+
17+
Remove `broadcaster` from `attr_accessor`. Configuration becomes:
18+
```ruby
19+
attr_accessor :wallet, :provider
20+
```
21+
22+
**File:** `gem/bsv-attest/spec/bsv/attest/configuration_spec.rb`
23+
24+
Remove broadcaster-related specs, update the "supports setting all attributes" spec to cover only `wallet` and `provider`.
25+
26+
### Task 2: Update Response — drop `transaction`, keep it simple
27+
28+
**File:** `gem/bsv-attest/lib/bsv/attest/response.rb`
29+
30+
Current: `attr_reader :hash, :transaction, :txid`
31+
32+
New:
33+
```ruby
34+
attr_reader :hash, :txid
35+
36+
def initialize(hash:, txid:)
37+
@hash = hash
38+
@txid = txid
39+
end
40+
```
41+
42+
- Drop `transaction` — we no longer build the Transaction object ourselves
43+
- No BEEF, no broadcast_status — the gem returns what the caller needs: the hash and where to find it
44+
- `hash_hex` stays the same
45+
46+
**File:** `gem/bsv-attest/spec/bsv/attest/response_spec.rb`
47+
48+
Update to test the new attributes (just `hash` and `txid`), remove `transaction` specs.
49+
50+
### Task 3: Add BroadcastError class
51+
52+
**File:** `gem/bsv-attest/lib/bsv/attest/broadcast_error.rb` (new)
53+
54+
```ruby
55+
class BroadcastError < StandardError; end
56+
```
57+
58+
Raised when `create_action` returns a `broadcast_error` in its result. This gives callers a distinct exception type for broadcast failures vs argument errors.
59+
60+
**File:** `gem/bsv-attest/lib/bsv/attest.rb` — add autoload entry.
61+
62+
### Task 4: Rewrite `publish` to use `wallet.create_action`
63+
64+
**File:** `gem/bsv-attest/lib/bsv/attest.rb`
65+
66+
Current signature: `publish(data, wallet: nil, broadcaster: nil)`
67+
New signature: `publish(data, wallet: nil, description: nil)`
68+
69+
Implementation:
70+
```ruby
71+
def publish(data, wallet: nil, description: nil)
72+
w = wallet || configuration.wallet
73+
raise ArgumentError, 'wallet is required' unless w
74+
75+
digest = hash(data)
76+
script = BSV::Script::Script.op_return(digest)
77+
78+
desc = description || 'Attest data hash to chain'
79+
80+
result = w.create_action(
81+
description: desc,
82+
outputs: [{
83+
locking_script: script.to_hex,
84+
satoshis: 0,
85+
output_description: 'Attestation hash'
86+
}],
87+
options: { randomize_outputs: false }
88+
)
89+
90+
if result[:broadcast_error]
91+
raise BroadcastError, result[:broadcast_error]
92+
end
93+
94+
Response.new(hash: digest, txid: result[:txid])
95+
end
96+
```
97+
98+
Key decisions:
99+
- **No `broadcaster:` parameter** — wallet owns broadcasting
100+
- **`description:` parameter** — optional override for the wallet action description (default: 'Attest data hash to chain', 26 chars, within 5-50 limit)
101+
- **`randomize_outputs: false`** — deterministic output ordering (OP_RETURN first, then change)
102+
- **BroadcastError on failure** — if the wallet returns broadcast_error, raise rather than return a partial response
103+
- **Response is just hash + txid** — no BEEF, no broadcast metadata; the gem returns what you need to verify later
104+
- **`verify` unchanged** — provider-based verification is independent of the publish rework
105+
106+
### Task 5: Update gemspec and version
107+
108+
**File:** `gem/bsv-attest/bsv-attest.gemspec`
109+
110+
```ruby
111+
spec.add_dependency 'bsv-sdk', '>= 0.11.0', '< 1.0'
112+
spec.add_dependency 'bsv-wallet', '>= 0.7.0', '< 1.0'
113+
```
114+
115+
The wallet is a runtime dependency because `publish` calls `create_action` directly. Version floors match current releases.
116+
117+
**File:** `gem/bsv-attest/lib/bsv/attest/version.rb`
118+
119+
Bump to `0.2.0` — breaking change (dropped broadcaster, changed Response shape).
120+
121+
### Task 6: Rewrite specs
122+
123+
**File:** `gem/bsv-attest/spec/bsv/attest_spec.rb`
124+
125+
**publish specs** — replace mock wallet/broadcaster with a mock that responds to `create_action`:
126+
127+
```ruby
128+
let(:mock_wallet) do
129+
Class.new do
130+
def create_action(args)
131+
{ txid: 'bb' * 32 }
132+
end
133+
end.new
134+
end
135+
```
136+
137+
Update specs:
138+
- "builds an OP_RETURN transaction" → "delegates to create_action and returns Response"
139+
- "includes the hash in an OP_RETURN output" → verify the create_action args include correct locking_script (spy mock)
140+
- Remove "raises ArgumentError without broadcaster"
141+
- Add "raises BroadcastError on broadcast failure"
142+
- Add "passes custom description to create_action"
143+
- Update per-call override spec (wallet only, no broadcaster)
144+
- Update fallback spec (wallet only)
145+
- Keep verify specs unchanged (they test provider, not wallet)
146+
147+
To verify create_action receives the correct output spec, use a spy-style mock:
148+
149+
```ruby
150+
let(:spy_wallet) do
151+
Class.new do
152+
attr_reader :last_args
153+
def create_action(args)
154+
@last_args = args
155+
{ txid: 'bb' * 32 }
156+
end
157+
end.new
158+
end
159+
```
160+
161+
Then assert `spy_wallet.last_args[:outputs].first[:locking_script]` contains the expected OP_RETURN hex.
162+
163+
## Files Modified
164+
165+
| File | Action |
166+
|------|--------|
167+
| `gem/bsv-attest/lib/bsv/attest.rb` | Edit: rewrite publish, add autoloads |
168+
| `gem/bsv-attest/lib/bsv/attest/configuration.rb` | Edit: remove broadcaster |
169+
| `gem/bsv-attest/lib/bsv/attest/response.rb` | Edit: simplify to hash + txid only |
170+
| `gem/bsv-attest/lib/bsv/attest/broadcast_error.rb` | New: BroadcastError class |
171+
| `gem/bsv-attest/lib/bsv/attest/version.rb` | Edit: bump to 0.2.0 |
172+
| `gem/bsv-attest/bsv-attest.gemspec` | Edit: add bsv-wallet dep, pin bsv-sdk |
173+
| `gem/bsv-attest/spec/bsv/attest_spec.rb` | Edit: rewrite publish specs |
174+
| `gem/bsv-attest/spec/bsv/attest/configuration_spec.rb` | Edit: remove broadcaster specs |
175+
| `gem/bsv-attest/spec/bsv/attest/response_spec.rb` | Edit: test simplified attributes |
176+
177+
## Verification
178+
179+
```bash
180+
cd gem/bsv-attest && bundle exec rspec # all specs pass
181+
bundle exec rubocop gem/bsv-attest # no offences
182+
cd gem/bsv-attest && gem build bsv-attest.gemspec # gem builds
183+
```

gem/bsv-attest/bsv-attest.gemspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ Gem::Specification.new do |spec|
2727
end + %w[LICENSE CHANGELOG.md]
2828
spec.require_paths = ['lib']
2929

30-
spec.add_dependency 'bsv-sdk'
30+
spec.add_dependency 'bsv-sdk', '>= 0.11.0', '< 1.0'
31+
spec.add_dependency 'bsv-wallet', '>= 0.7.0', '< 1.0'
3132
end

gem/bsv-attest/lib/bsv/attest.rb

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module BSV
44
module Attest
5+
autoload :BroadcastError, 'bsv/attest/broadcast_error'
56
autoload :Configuration, 'bsv/attest/configuration'
67
autoload :Response, 'bsv/attest/response'
78
autoload :VerificationError, 'bsv/attest/verification_error'
@@ -24,25 +25,30 @@ def hash(data)
2425
BSV::Primitives::Digest.sha256(data)
2526
end
2627

27-
def publish(data, wallet: nil, broadcaster: nil)
28+
def publish(data, wallet: nil, description: nil)
2829
w = wallet || configuration.wallet
29-
b = broadcaster || configuration.broadcaster
3030
raise ArgumentError, 'wallet is required' unless w
31-
raise ArgumentError, 'broadcaster is required' unless b
3231

3332
digest = hash(data)
34-
3533
script = BSV::Script::Script.op_return(digest)
36-
output = BSV::Transaction::TransactionOutput.new(satoshis: 0, locking_script: script)
3734

38-
tx = BSV::Transaction::Transaction.new
39-
tx.add_output(output)
35+
desc = description || 'Attest data hash to chain'
4036

41-
w.fund_and_sign(tx)
37+
result = w.create_action(
38+
{
39+
description: desc,
40+
outputs: [{
41+
locking_script: script.to_hex,
42+
satoshis: 0,
43+
output_description: 'Attestation hash'
44+
}],
45+
options: { randomize_outputs: false }
46+
}
47+
)
4248

43-
broadcast_response = b.broadcast(tx)
49+
raise BroadcastError, result[:broadcast_error] if result[:broadcast_error]
4450

45-
Response.new(hash: digest, transaction: tx, txid: broadcast_response.txid)
51+
Response.new(hash: digest, txid: result[:txid])
4652
end
4753

4854
def verify(data, txid, provider: nil)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module BSV
4+
module Attest
5+
class BroadcastError < StandardError; end
6+
end
7+
end

gem/bsv-attest/lib/bsv/attest/configuration.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module BSV
44
module Attest
55
class Configuration
6-
attr_accessor :wallet, :broadcaster, :provider
6+
attr_accessor :wallet, :provider
77
end
88
end
99
end

gem/bsv-attest/lib/bsv/attest/response.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
module BSV
44
module Attest
55
class Response
6-
attr_reader :hash, :transaction, :txid
6+
attr_reader :hash, :txid
77

8-
def initialize(hash:, transaction:, txid:)
8+
def initialize(hash:, txid:)
99
@hash = hash
10-
@transaction = transaction
1110
@txid = txid
1211
end
1312

gem/bsv-attest/lib/bsv/attest/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module BSV
44
module Attest
5-
VERSION = '0.1.0'
5+
VERSION = '0.2.0'
66
end
77
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
require 'bsv-attest'
6+
7+
RSpec.describe BSV::Attest::BroadcastError do
8+
it 'is a StandardError subclass' do
9+
expect(described_class).to be < StandardError
10+
end
11+
12+
it 'stores a message' do
13+
error = described_class.new('broadcast failed')
14+
expect(error.message).to eq('broadcast failed')
15+
end
16+
end

gem/bsv-attest/spec/bsv/attest/configuration_spec.rb

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,22 @@
1111
expect(config.wallet).to be_nil
1212
end
1313

14-
it 'defaults broadcaster to nil' do
15-
expect(config.broadcaster).to be_nil
16-
end
17-
1814
it 'defaults provider to nil' do
1915
expect(config.provider).to be_nil
2016
end
2117

22-
it 'supports setting all three attributes' do
18+
it 'supports setting all attributes' do
2319
wallet = Object.new
24-
broadcaster = Object.new
2520
provider = Object.new
2621

2722
config.wallet = wallet
28-
config.broadcaster = broadcaster
2923
config.provider = provider
3024

3125
expect(config.wallet).to eq(wallet)
32-
expect(config.broadcaster).to eq(broadcaster)
3326
expect(config.provider).to eq(provider)
3427
end
28+
29+
it 'does not respond to broadcaster' do
30+
expect(config).not_to respond_to(:broadcaster)
31+
end
3532
end

gem/bsv-attest/spec/bsv/attest/response_spec.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,24 @@
66

77
RSpec.describe BSV::Attest::Response do
88
subject(:response) do
9-
described_class.new(hash: hash_bytes, transaction: transaction, txid: txid)
9+
described_class.new(hash: hash_bytes, txid: txid)
1010
end
1111

1212
let(:hash_bytes) { BSV::Primitives::Digest.sha256('test data') }
13-
let(:transaction) { BSV::Transaction::Transaction.new }
1413
let(:txid) { 'aa' * 32 }
1514

1615
it 'stores the hash' do
1716
expect(response.hash).to eq(hash_bytes)
1817
end
1918

20-
it 'stores the transaction' do
21-
expect(response.transaction).to eq(transaction)
22-
end
23-
2419
it 'stores the txid' do
2520
expect(response.txid).to eq(txid)
2621
end
2722

23+
it 'does not respond to transaction' do
24+
expect(response).not_to respond_to(:transaction)
25+
end
26+
2827
describe '#hash_hex' do
2928
it 'returns a 64-character hex string' do
3029
hex = response.hash_hex

0 commit comments

Comments
 (0)