x402-rack 0.2.0 → 0.3.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 +90 -0
- data/README.md +63 -3
- data/docs/architecture.md +1 -1
- data/docs/operations/deployment.md +1 -2
- data/docs/operations/treasury.md +11 -3
- data/docs/process-flow/proof-gateway.md +1 -1
- data/docs/schemes/bsv-proof.md +31 -7
- data/docs/security.md +1 -1
- data/lib/x402/bsv/pay_gateway.rb +9 -7
- data/lib/x402/bsv/proof_gateway.rb +35 -74
- data/lib/x402/configuration.rb +132 -2
- data/lib/x402/middleware.rb +2 -2
- data/lib/x402/protocol/base64url.rb +2 -2
- data/lib/x402/protocol/challenge.rb +2 -2
- data/lib/x402/protocol/proof.rb +2 -2
- data/lib/x402/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cd89fcca7ffc76bbe6c64b59f9a3874f31dcd4ef81b0b5e5de2bdf6ef06cb1a
|
|
4
|
+
data.tar.gz: 9ed4a30017bf928d1cfcce9da5ff119f7976fd2b30e83720ebed86e16d4ae5de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ef8a23f261ea47e375f0c40177713960f5023f624ffd52463f865143aa633503a8c4f048a5cb0d39dc5af5940de78faebd82d01de68161b7533b3571db44398
|
|
7
|
+
data.tar.gz: 917457986e6916d200acc8eee63a583984577ea6440dd5a6c8300c786f20288bf5228fde48e666e0b8bc56a42de8f6cdbcdda39ff387c60af715508725a92258
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-04-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Configuration DSL** — `config.enable :pay_gateway` with shared dependencies (ARC client, payee script) wired automatically. Convenience options: `server_wif:` builds KeyDeriver, `nonce_wif:` builds PrivateKey, default PrefixStore for BRC-105. Per-gateway overrides supported. Deferred construction at `validate!` time. Full backwards compatibility with `config.gateways = [...]`.
|
|
13
|
+
- **Copilot review instructions** — `.github/copilot-instructions.md` with payment-bypass-focused review guidance.
|
|
14
|
+
- **Dependabot** — weekly checks for bundler and GitHub Actions dependencies.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Treasury refactor** — ProofGateway no longer holds the treasury's private key (`nonce_key:` removed). The `nonce_provider` callable now optionally returns a pre-signed `partial_tx:` for Profile B. Signing responsibility pushed from gateway to treasury. Trust boundary: `[(X)+(B)] <-> [(T)]`.
|
|
19
|
+
- **nonce_provider interface** — now receives `payee:` and `amount:` kwargs. Profile detection moved from constructor config to provider response.
|
|
20
|
+
- **Dependencies** — `bsv-sdk ~> 0.4`, `bsv-wallet ~> 0.2` (BRC-100 wallet interface now available).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **ARC wait_for enforced** — PayGateway now passes `arc_wait_for` to `broadcast(tx, wait_for:)`. Previously stored but never used.
|
|
25
|
+
- **Mempool status validated** — ProofGateway `check_mempool!` now verifies `tx_status` is `SEEN_ON_NETWORK`, `ANNOUNCED_TO_NETWORK`, or `MINED`. Non-propagated statuses (`RECEIVED`, `STORED`) correctly rejected.
|
|
26
|
+
- **Error messages hardened** — all gateways, middleware, and protocol parsers now return fixed generic strings. No SDK exception messages forwarded to HTTP clients.
|
|
27
|
+
- **Config DSL memoisation** — `shared_arc_client` checks injected `arc_client` on every call, not just first.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- **`nonce_key:` parameter** from ProofGateway constructor (breaking change for Profile B users — signing moves to nonce_provider).
|
|
32
|
+
- **`nonce_wif:` and `nonce_key:` convenience options** from configuration DSL (no longer applicable).
|
|
33
|
+
|
|
34
|
+
## [0.2.0] - 2026-03-30
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **BRC-105 Gateway** — `X402::BSV::BRC105Gateway` implementing the BSV Association's native payment protocol (`x-bsv-*` headers). Uses BRC-29 key derivation for unique per-payment addresses and AtomicBEEF (BRC-95) transaction format.
|
|
39
|
+
- **Prefix Store** — `X402::BSV::PrefixStore::Memory` for BRC-105 derivation prefix replay protection. Thread-safe via Monitor, with TTL-based expiry (default 300s) and max capacity cap (default 10,000).
|
|
40
|
+
- **BRC-103 composition** — BRC105Gateway works standalone (advertises server identity key in header) or composes with future BRC-103 mutual authentication middleware. Detects mode automatically from `env['brc103.identity_key']`.
|
|
41
|
+
- **BRC-105 e2e test** — full standalone payment flow against BSV testnet (derive address, build tx, encode AtomicBEEF, verify, broadcast).
|
|
42
|
+
- **Comprehensive documentation** — scheme doc (`docs/schemes/brc-105.md`), process flow diagrams for both standalone and authenticated modes, security analysis, client integration guide.
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
|
|
46
|
+
- Derivation prefix consumed after full transaction validation, not before (prevents MITM prefix burning).
|
|
47
|
+
- BRC-103 identity key validated as compressed pubkey hex before trusting as BRC-29 counterparty (prevents sentinel injection).
|
|
48
|
+
- SDK exception messages not forwarded to HTTP clients — fixed generic strings returned.
|
|
49
|
+
- `PrefixStore::Memory` bounded with TTL and max capacity to prevent heap exhaustion from unauthenticated challenge requests.
|
|
50
|
+
- `StoreFullError` returns 503 (server at capacity), not 400.
|
|
51
|
+
|
|
52
|
+
## [0.1.0] - 2026-03-28
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
#### Middleware
|
|
57
|
+
|
|
58
|
+
- **`X402::Middleware`** — pure Rack dispatcher for payment-gated HTTP. No blockchain knowledge, no keys. Matches routes, polls gateways for challenge headers, dispatches proofs to the matching gateway.
|
|
59
|
+
- **Multi-gateway support** — multiple gateways can be configured simultaneously. The middleware issues challenge headers from all gateways and dispatches proofs to whichever gateway recognises the proof header.
|
|
60
|
+
- **Payment content negotiation** — different x402 ecosystems use different HTTP headers. A server sends multiple challenge headers; the client picks the one it can satisfy.
|
|
61
|
+
|
|
62
|
+
#### Gateways
|
|
63
|
+
|
|
64
|
+
- **`X402::BSV::PayGateway`** — Coinbase v2 headers (`Payment-Required` / `Payment-Signature` / `Payment-Response`). Server broadcasts via ARC. HMAC-signed `payToSig` prevents payee address tampering. OP_RETURN request binding (strict/permissive mode). This is the recommended "BSV way" — vendor verifies, vendor broadcasts, vendor serves.
|
|
65
|
+
- **`X402::BSV::ProofGateway`** — merkleworks x402 headers (`X402-Challenge` / `X402-Proof`). Client broadcasts, server checks mempool. Profile A (bare nonce metadata) and Profile B (pre-signed template with 0xC3 nonce signature for provenance).
|
|
66
|
+
- **`X402::BSV::Gateway`** — base class for template-based gateways. Builds partial transaction templates with payment output and OP_RETURN binding. Supports wallet-based address derivation (BRC-43) or static payee address.
|
|
67
|
+
|
|
68
|
+
#### Configuration
|
|
69
|
+
|
|
70
|
+
- **Route protection** — `config.protect method: :GET, path: "/api/expensive", amount_sats: 100`
|
|
71
|
+
- **Duplicate proof header detection** — validates no two gateways claim the same proof header name.
|
|
72
|
+
|
|
73
|
+
#### Testing
|
|
74
|
+
|
|
75
|
+
- **E2e test suite** — PayGateway, ProofGateway (Profile B), and fee delegation flows tested against BSV testnet with real ARC broadcasts.
|
|
76
|
+
- **E2ELogger** — pretty logging with actors, timestamps, transaction links, and timestamped markdown log files.
|
|
77
|
+
- **Fee delegation e2e** — treasury + client + delegator + payee four-wallet flow with 0xC3 sighash alignment.
|
|
78
|
+
|
|
79
|
+
#### Documentation
|
|
80
|
+
|
|
81
|
+
- **Architecture docs** — middleware as dispatcher, gateway interface, component boundaries, unified template model, 0xC3 sighash rationale.
|
|
82
|
+
- **Scheme docs** — BSV-pay and BSV-proof with headers, challenge/settlement flows, replay protection.
|
|
83
|
+
- **Process flow diagrams** — mermaid sequence diagrams for PayGateway and ProofGateway.
|
|
84
|
+
- **Security docs** — threat model, payToSig HMAC, nonce provenance (Profile B), OP_RETURN binding, error handling.
|
|
85
|
+
- **Operations docs** — deployment, performance, treasury/nonce lifecycle.
|
|
86
|
+
- **Ecosystem docs** — Coinbase v2, merkleworks, BRC-105 positioning and header namespace reservations.
|
|
87
|
+
|
|
88
|
+
[0.3.0]: https://github.com/sgbett/x402-rack/compare/v0.2.0...v0.3.0
|
|
89
|
+
[0.2.0]: https://github.com/sgbett/x402-rack/compare/v0.1.0...v0.2.0
|
|
90
|
+
[0.1.0]: https://github.com/sgbett/x402-rack/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -20,20 +20,77 @@ gem "x402-rack", git: "https://github.com/sgbett/x402-rack.git"
|
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
+
### Configuration DSL (recommended)
|
|
24
|
+
|
|
25
|
+
The DSL handles shared dependencies (ARC client, payee script) and gateway
|
|
26
|
+
construction automatically. You declare *what* you want; the middleware builds it
|
|
27
|
+
at validation time.
|
|
28
|
+
|
|
23
29
|
```ruby
|
|
24
30
|
# config.ru or Rails initialiser
|
|
25
31
|
require "x402"
|
|
26
32
|
|
|
33
|
+
X402.configure do |config|
|
|
34
|
+
config.domain = "api.example.com"
|
|
35
|
+
config.payee_locking_script_hex = "76a914...88ac"
|
|
36
|
+
|
|
37
|
+
# Shared ARC connection — built once, used by all gateways
|
|
38
|
+
config.arc_url = "https://arc.taal.com"
|
|
39
|
+
config.arc_api_key = "..." # optional — ARC supports anonymous access
|
|
40
|
+
|
|
41
|
+
# Enable gateways — constructed automatically from shared deps
|
|
42
|
+
config.enable :pay_gateway
|
|
43
|
+
config.enable :proof_gateway, nonce_provider: my_nonce_provider
|
|
44
|
+
config.enable :brc105_gateway, server_wif: "L3..."
|
|
45
|
+
|
|
46
|
+
config.protect method: :GET, path: "/api/expensive", amount_sats: 100
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
use X402::Middleware
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Convenience options
|
|
53
|
+
|
|
54
|
+
Each gateway type supports convenience options that expand into their full form:
|
|
55
|
+
|
|
56
|
+
- **`:brc105_gateway`** — `server_wif: "L3..."` builds a `KeyDeriver` from a WIF
|
|
57
|
+
string. `server_key:` accepts a `PrivateKey` directly. A default in-memory
|
|
58
|
+
`PrefixStore` is used if `prefix_store:` is not provided.
|
|
59
|
+
- **`:proof_gateway`** — `nonce_wif: "L3..."` builds a `PrivateKey` for the
|
|
60
|
+
`nonce_key:` parameter.
|
|
61
|
+
- **`:pay_gateway`** — pass-through options: `arc_wait_for:`, `arc_timeout:`,
|
|
62
|
+
`binding_mode:`, `wallet:`, `challenge_secret:`.
|
|
63
|
+
|
|
64
|
+
#### Per-gateway overrides
|
|
65
|
+
|
|
66
|
+
Any gateway can override shared dependencies:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
config.enable :pay_gateway, arc_client: my_custom_arc
|
|
70
|
+
config.enable :proof_gateway, nonce_provider: np, payee_locking_script_hex: "deadbeef..."
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Order independence
|
|
74
|
+
|
|
75
|
+
`enable` records gateway specs; construction is deferred to `validate!`. This
|
|
76
|
+
means `arc_url` can be set after `enable` calls — order within the configure
|
|
77
|
+
block does not matter.
|
|
78
|
+
|
|
79
|
+
### Manual gateway construction (power-user escape hatch)
|
|
80
|
+
|
|
81
|
+
For full control, construct gateways yourself and assign them directly. This
|
|
82
|
+
bypasses DSL construction entirely and works exactly as before:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
27
85
|
X402.configure do |config|
|
|
28
86
|
config.domain = "api.example.com"
|
|
29
87
|
config.payee_locking_script_hex = "76a914...88ac"
|
|
30
88
|
|
|
31
89
|
config.gateways = [
|
|
32
90
|
X402::BSV::PayGateway.new(
|
|
33
|
-
|
|
34
|
-
|
|
91
|
+
arc_client: BSV::Network::ARC.new("https://arc.taal.com", api_key: "..."),
|
|
92
|
+
payee_locking_script_hex: "76a914...88ac"
|
|
35
93
|
),
|
|
36
|
-
# BRC-105 gateway (BSV Association payment protocol)
|
|
37
94
|
X402::BSV::BRC105Gateway.new(
|
|
38
95
|
key_deriver: BSV::Wallet::KeyDeriver.new(server_private_key),
|
|
39
96
|
prefix_store: X402::BSV::PrefixStore::Memory.new,
|
|
@@ -47,6 +104,9 @@ end
|
|
|
47
104
|
use X402::Middleware
|
|
48
105
|
```
|
|
49
106
|
|
|
107
|
+
If `config.gateways` is set to a non-empty array, any `enable` calls are
|
|
108
|
+
ignored.
|
|
109
|
+
|
|
50
110
|
## How It Works
|
|
51
111
|
|
|
52
112
|
1. Client requests a protected resource
|
data/docs/architecture.md
CHANGED
|
@@ -15,7 +15,7 @@ The middleware never decodes transactions, checks mempool, broadcasts, or intera
|
|
|
15
15
|
|
|
16
16
|
## Gateways
|
|
17
17
|
|
|
18
|
-
Gateways are pluggable backends that handle chain-specific settlement. They
|
|
18
|
+
Gateways are pluggable backends that handle chain-specific settlement. They delegate key management and signing to external providers (e.g. the treasury via `nonce_provider`). Each gateway:
|
|
19
19
|
|
|
20
20
|
- Builds challenge data (including partial transaction templates)
|
|
21
21
|
- Verifies and settles proofs
|
data/docs/operations/treasury.md
CHANGED
|
@@ -12,7 +12,7 @@ The treasury creates 1-sat P2PKH outputs locked to the nonce key. Each output be
|
|
|
12
12
|
|
|
13
13
|
When the ProofGateway builds a challenge, it requests a nonce from the treasury (via the `nonce_provider` callable). The nonce UTXO details (txid, vout, satoshis, locking_script_hex) are included in the challenge.
|
|
14
14
|
|
|
15
|
-
In Profile B, the
|
|
15
|
+
In Profile B, the treasury signs the nonce input with `0xC3` and returns the pre-signed template (as `partial_tx` binary) to the gateway. The gateway appends the OP_RETURN and includes the template in the challenge.
|
|
16
16
|
|
|
17
17
|
### Spending
|
|
18
18
|
|
|
@@ -51,12 +51,20 @@ The treasury is not on the critical path for settlement — it only participates
|
|
|
51
51
|
|
|
52
52
|
## Current Implementation
|
|
53
53
|
|
|
54
|
-
The `nonce_provider` is
|
|
54
|
+
The `nonce_provider` is a callable injected into ProofGateway. It receives `payee:` and `amount:` kwargs so the treasury can build the template:
|
|
55
55
|
|
|
56
56
|
```ruby
|
|
57
|
-
|
|
57
|
+
# Profile A — bare UTXO metadata
|
|
58
|
+
nonce_provider = ->(request, payee:, amount:) {
|
|
58
59
|
{ txid: "...", vout: 0, satoshis: 1, locking_script_hex: "76a914...88ac" }
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
# Profile B — treasury builds and signs the template
|
|
63
|
+
nonce_provider = ->(request, payee:, amount:) {
|
|
64
|
+
tx = build_and_sign_template(payee: payee, amount: amount)
|
|
65
|
+
{ txid: "...", vout: 0, satoshis: 1, locking_script_hex: "76a914...88ac",
|
|
66
|
+
partial_tx: tx.to_binary }
|
|
67
|
+
}
|
|
60
68
|
```
|
|
61
69
|
|
|
62
70
|
Full wallet-backed treasury integration (basket-managed nonce pool, automatic minting, timelock expiry) is tracked in [bsv-ruby-sdk#196](https://github.com/sgbett/bsv-ruby-sdk/issues/196).
|
|
@@ -81,7 +81,7 @@ sequenceDiagram
|
|
|
81
81
|
## Notes
|
|
82
82
|
|
|
83
83
|
- **Client broadcasts, not server** — broadcasting is settlement, not authorisation. Keeps the server stateless (per Rui at merkleworks).
|
|
84
|
-
- **Profile B template** — the
|
|
84
|
+
- **Profile B template** — the treasury (via `nonce_provider`) builds and signs the template. The gateway appends the OP_RETURN and includes it in the challenge as `partial_tx_b64`. The nonce input at index 0 is signed with `0xC3`. The client extends it by appending funding inputs.
|
|
85
85
|
- **0xC3 sighash** — `SIGHASH_SINGLE | ANYONECANPAY | FORKID` commits to output 0 (payment) while allowing additional inputs and outputs. See [architecture.md](../architecture.md#why-0xc3-for-the-nonce-signature).
|
|
86
86
|
- **Fee delegation is optional** — shown as `opt` in the diagram. Most clients can fund their own fees (1-50 sats). The delegator adds fee inputs and signs only those.
|
|
87
87
|
- **Client signs with 0xC1** — `SIGHASH_ALL | ANYONECANPAY | FORKID` on funding inputs. Commits to all outputs but allows the delegator to append fee inputs.
|
data/docs/schemes/bsv-proof.md
CHANGED
|
@@ -11,21 +11,23 @@ Client broadcasts, server checks mempool. Proof-of-payment model. Nonce-bound wi
|
|
|
11
11
|
|
|
12
12
|
## Profiles
|
|
13
13
|
|
|
14
|
-
### Profile A (no
|
|
14
|
+
### Profile A (no partial template)
|
|
15
15
|
|
|
16
16
|
The challenge includes nonce UTXO metadata only. The client must construct the entire transaction including the nonce input. No cryptographic proof of nonce provenance.
|
|
17
17
|
|
|
18
18
|
Suitable for deployments using an external treasury service that returns bare UTXO references.
|
|
19
19
|
|
|
20
|
-
### Profile B (
|
|
20
|
+
### Profile B (treasury-signed template) — recommended
|
|
21
21
|
|
|
22
|
-
The
|
|
22
|
+
The **treasury** (via the `nonce_provider` callable) builds and signs a partial template. The gateway receives it and appends the OP_RETURN request binding:
|
|
23
23
|
|
|
24
24
|
- Input 0: nonce UTXO, signed with `SIGHASH_SINGLE | ANYONECANPAY | FORKID` (`0xC3`)
|
|
25
|
-
- Output 0: payment (committed by the signature)
|
|
26
|
-
- Output 1: OP_RETURN binding (
|
|
25
|
+
- Output 0: payment (committed by the treasury's signature)
|
|
26
|
+
- Output 1: OP_RETURN binding (appended by the gateway after receiving the template)
|
|
27
27
|
|
|
28
|
-
The
|
|
28
|
+
The gateway never holds a private key. Profile B is detected from the presence of `partial_tx` in the provider response.
|
|
29
|
+
|
|
30
|
+
The client extends the template by adding funding inputs. The `0xC3` signature proves the treasury issued the nonce — this is the provenance guarantee.
|
|
29
31
|
|
|
30
32
|
The template is included in the challenge as `partial_tx_b64` (base64-encoded), excluded from the canonical challenge hash (merkleworks spec compliance).
|
|
31
33
|
|
|
@@ -79,7 +81,29 @@ Per Rui at merkleworks: broadcasting is settlement, not authorisation. If the se
|
|
|
79
81
|
|
|
80
82
|
See [operations/treasury.md](../operations/treasury.md) for nonce lifecycle.
|
|
81
83
|
|
|
84
|
+
## Nonce Provider Interface
|
|
85
|
+
|
|
86
|
+
The `nonce_provider` is a callable that receives `(rack_request, payee:, amount:)` and returns a hash:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Profile A — bare UTXO metadata
|
|
90
|
+
provider = ->(request, payee:, amount:) {
|
|
91
|
+
{ txid: "...", vout: 0, satoshis: 1, locking_script_hex: "76a914...88ac" }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Profile B — includes pre-signed partial template
|
|
95
|
+
provider = ->(request, payee:, amount:) {
|
|
96
|
+
tx = build_and_sign_template(payee: payee, amount: amount)
|
|
97
|
+
{ txid: "...", vout: 0, satoshis: 1, locking_script_hex: "76a914...88ac",
|
|
98
|
+
partial_tx: tx.to_binary }
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The presence of `:partial_tx` in the response triggers Profile B behaviour. The gateway appends the OP_RETURN after deserialising the template.
|
|
103
|
+
|
|
82
104
|
## Infrastructure Required
|
|
83
105
|
|
|
84
|
-
-
|
|
106
|
+
Trust boundary: `[(X)+(B)] <-> [(T)]` — the server (x402-rack + BSV gateway) never holds keys; the treasury is a separate trust domain.
|
|
107
|
+
|
|
108
|
+
- **Treasury** (`nonce_provider`): mints nonce UTXOs, holds keys, signs templates (Profile B)
|
|
85
109
|
- **ARC**: mempool queries (`status(txid)`)
|
data/docs/security.md
CHANGED
|
@@ -48,7 +48,7 @@ The `0xC3` signature on input 0 proves the server issued the nonce. At settlemen
|
|
|
48
48
|
|
|
49
49
|
### Nonce Key Validation
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Key validation is the treasury's responsibility. The gateway never holds a private key — the `nonce_provider` callable builds and signs the template. Misconfiguration (key/UTXO mismatch) is caught at settlement time when `verify_input(0)` fails.
|
|
52
52
|
|
|
53
53
|
### Payee Verification
|
|
54
54
|
|
data/lib/x402/bsv/pay_gateway.rb
CHANGED
|
@@ -97,8 +97,8 @@ module X402
|
|
|
97
97
|
def decode_payment_payload(proof_payload)
|
|
98
98
|
json = Base64.strict_decode64(proof_payload)
|
|
99
99
|
JSON.parse(json)
|
|
100
|
-
rescue ArgumentError, JSON::ParserError
|
|
101
|
-
raise VerificationError.new("invalid payment payload
|
|
100
|
+
rescue ArgumentError, JSON::ParserError
|
|
101
|
+
raise VerificationError.new("invalid payment payload", status: 400)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def verify_accepted!(payload, route)
|
|
@@ -122,8 +122,8 @@ module X402
|
|
|
122
122
|
::BSV::Transaction::Transaction.from_binary(raw)
|
|
123
123
|
rescue VerificationError
|
|
124
124
|
raise
|
|
125
|
-
rescue StandardError
|
|
126
|
-
raise VerificationError.new("failed to decode transaction
|
|
125
|
+
rescue StandardError
|
|
126
|
+
raise VerificationError.new("failed to decode transaction", status: 400)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
def verify_payment_output!(transaction, route, payee_hex)
|
|
@@ -149,9 +149,11 @@ module X402
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
def broadcast!(transaction)
|
|
152
|
-
arc_client.broadcast(transaction)
|
|
153
|
-
rescue
|
|
154
|
-
raise
|
|
152
|
+
arc_client.broadcast(transaction, wait_for: arc_wait_for)
|
|
153
|
+
rescue VerificationError
|
|
154
|
+
raise
|
|
155
|
+
rescue StandardError
|
|
156
|
+
raise VerificationError.new("ARC broadcast failed", status: 502)
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
def build_settlement_result(transaction)
|
|
@@ -17,23 +17,22 @@ module X402
|
|
|
17
17
|
# Proof: X402-Proof (echoed challenge hash + rawtx + txid)
|
|
18
18
|
#
|
|
19
19
|
# Supports two modes:
|
|
20
|
-
# - Profile A
|
|
21
|
-
# - Profile B
|
|
22
|
-
# nonce input at index 0 signed with 0xC3
|
|
20
|
+
# - Profile A: challenge includes nonce UTXO metadata only
|
|
21
|
+
# - Profile B: nonce_provider returns a pre-signed partial_tx template
|
|
23
22
|
class ProofGateway < Gateway
|
|
24
|
-
|
|
23
|
+
ACCEPTABLE_MEMPOOL_STATUSES = %w[SEEN_ON_NETWORK ANNOUNCED_TO_NETWORK MINED].freeze
|
|
25
24
|
|
|
26
|
-
# @param nonce_provider [#call] callable returning nonce UTXO hash
|
|
25
|
+
# @param nonce_provider [#call] callable returning nonce UTXO hash;
|
|
26
|
+
# receives (rack_request, payee:, amount:) kwargs.
|
|
27
|
+
# Profile B providers include :partial_tx (binary) in the response.
|
|
27
28
|
# @param arc_client [#status] ARC client for mempool queries
|
|
28
|
-
# @param nonce_key [BSV::Primitives::PrivateKey, nil] key for signing nonce input (Profile B)
|
|
29
29
|
# @param payee_locking_script_hex [String, nil] payee script (falls back to config)
|
|
30
|
-
def initialize(nonce_provider:, arc_client:,
|
|
30
|
+
def initialize(nonce_provider:, arc_client:,
|
|
31
31
|
payee_locking_script_hex: nil, wallet: nil, challenge_secret: nil)
|
|
32
32
|
super(payee_locking_script_hex: payee_locking_script_hex, wallet: wallet,
|
|
33
33
|
challenge_secret: challenge_secret)
|
|
34
34
|
@nonce_provider = nonce_provider
|
|
35
35
|
@arc_client = arc_client
|
|
36
|
-
@nonce_key = nonce_key
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def challenge_headers(rack_request, route)
|
|
@@ -57,57 +56,14 @@ module X402
|
|
|
57
56
|
|
|
58
57
|
private
|
|
59
58
|
|
|
60
|
-
# Build a Profile B template: nonce input (signed 0xC3) + payment + OP_RETURN.
|
|
61
|
-
# The 0xC3 signature commits to output 0 (payment) only.
|
|
62
|
-
# The OP_RETURN is added AFTER signing — it's unsigned data.
|
|
63
|
-
# Delegated clients should pop the OP_RETURN before adding their change
|
|
64
|
-
# (to preserve SIGHASH_SINGLE index alignment) and re-append it last.
|
|
65
|
-
# Falls back to base class (Profile A) when nonce_key is nil.
|
|
66
|
-
def build_proof_template(rack_request, route, nonce)
|
|
67
|
-
return super(rack_request, route) unless @nonce_key
|
|
68
|
-
|
|
69
|
-
validate_nonce_key!(nonce)
|
|
70
|
-
|
|
71
|
-
tx = ::BSV::Transaction::Transaction.new
|
|
72
|
-
|
|
73
|
-
# Input 0: nonce UTXO (will be signed with 0xC3)
|
|
74
|
-
nonce_script = ::BSV::Script::Script.from_hex(nonce[:locking_script_hex])
|
|
75
|
-
nonce_input = ::BSV::Transaction::TransactionInput.new(
|
|
76
|
-
prev_tx_id: [nonce[:txid]].pack("H*").reverse,
|
|
77
|
-
prev_tx_out_index: nonce[:vout]
|
|
78
|
-
)
|
|
79
|
-
nonce_input.source_satoshis = nonce[:satoshis]
|
|
80
|
-
nonce_input.source_locking_script = nonce_script
|
|
81
|
-
tx.add_input(nonce_input)
|
|
82
|
-
|
|
83
|
-
# Output 0: payment (committed by 0xC3 signature on input 0)
|
|
84
|
-
payee_hex = derive_payee_hex
|
|
85
|
-
payee_script = ::BSV::Script::Script.from_hex(payee_hex)
|
|
86
|
-
tx.add_output(::BSV::Transaction::TransactionOutput.new(
|
|
87
|
-
satoshis: route.amount_sats,
|
|
88
|
-
locking_script: payee_script
|
|
89
|
-
))
|
|
90
|
-
|
|
91
|
-
# Sign input 0 with 0xC3 (commits to output 0 only)
|
|
92
|
-
tx.sign(0, @nonce_key, NONCE_SIGHASH)
|
|
93
|
-
|
|
94
|
-
# Output 1: OP_RETURN binding (added AFTER signing — unsigned)
|
|
95
|
-
binding_hash = request_binding_hash(rack_request)
|
|
96
|
-
tx.add_output(::BSV::Transaction::TransactionOutput.new(
|
|
97
|
-
satoshis: 0,
|
|
98
|
-
locking_script: build_op_return_script(binding_hash)
|
|
99
|
-
))
|
|
100
|
-
|
|
101
|
-
[tx, payee_hex]
|
|
102
|
-
end
|
|
103
|
-
|
|
104
59
|
def build_merkleworks_challenge(rack_request, route)
|
|
105
|
-
nonce = @nonce_provider.call(rack_request)
|
|
106
60
|
config = X402.configuration
|
|
107
61
|
payee_hex = derive_payee_hex
|
|
108
62
|
|
|
109
|
-
|
|
110
|
-
|
|
63
|
+
nonce = @nonce_provider.call(rack_request, payee: payee_hex, amount: route.amount_sats)
|
|
64
|
+
|
|
65
|
+
# Profile B: provider returns a pre-signed partial_tx (binary)
|
|
66
|
+
template_binary = nonce[:partial_tx]
|
|
111
67
|
|
|
112
68
|
attrs = {
|
|
113
69
|
version: Challenge::CURRENT_VERSION,
|
|
@@ -127,8 +83,16 @@ module X402
|
|
|
127
83
|
expires_at: Time.now.to_i + Challenge::DEFAULT_TTL
|
|
128
84
|
}
|
|
129
85
|
|
|
130
|
-
|
|
131
|
-
|
|
86
|
+
if template_binary
|
|
87
|
+
# Deserialise the treasury's template and append OP_RETURN
|
|
88
|
+
tx = ::BSV::Transaction::Transaction.from_binary(template_binary)
|
|
89
|
+
binding_hash = request_binding_hash(rack_request)
|
|
90
|
+
tx.add_output(::BSV::Transaction::TransactionOutput.new(
|
|
91
|
+
satoshis: 0,
|
|
92
|
+
locking_script: build_op_return_script(binding_hash)
|
|
93
|
+
))
|
|
94
|
+
attrs[:partial_tx_b64] = Base64.strict_encode64(tx.to_binary)
|
|
95
|
+
end
|
|
132
96
|
|
|
133
97
|
Challenge.new(attrs)
|
|
134
98
|
end
|
|
@@ -154,7 +118,7 @@ module X402
|
|
|
154
118
|
transaction = decode_transaction(proof)
|
|
155
119
|
check_txid!(transaction, proof)
|
|
156
120
|
check_nonce_input!(transaction, challenge)
|
|
157
|
-
verify_nonce_provenance!(transaction, challenge) if
|
|
121
|
+
verify_nonce_provenance!(transaction, challenge) if challenge.partial_tx_b64
|
|
158
122
|
server_payee_hex = resolve_static_payee_hex
|
|
159
123
|
verify_payment_output!(transaction, route, server_payee_hex)
|
|
160
124
|
transaction
|
|
@@ -163,8 +127,10 @@ module X402
|
|
|
163
127
|
def decode_transaction(proof)
|
|
164
128
|
raw = Base64.decode64(proof.rawtx_b64)
|
|
165
129
|
::BSV::Transaction::Transaction.from_binary(raw)
|
|
166
|
-
rescue
|
|
167
|
-
raise
|
|
130
|
+
rescue VerificationError
|
|
131
|
+
raise
|
|
132
|
+
rescue StandardError
|
|
133
|
+
raise VerificationError, "failed to decode transaction"
|
|
168
134
|
end
|
|
169
135
|
|
|
170
136
|
def check_txid!(transaction, proof)
|
|
@@ -199,8 +165,8 @@ module X402
|
|
|
199
165
|
end
|
|
200
166
|
rescue VerificationError
|
|
201
167
|
raise
|
|
202
|
-
rescue StandardError
|
|
203
|
-
raise VerificationError, "nonce provenance check failed
|
|
168
|
+
rescue StandardError
|
|
169
|
+
raise VerificationError, "nonce provenance check failed"
|
|
204
170
|
end
|
|
205
171
|
|
|
206
172
|
def verify_payment_output!(transaction, route, payee_hex)
|
|
@@ -214,19 +180,14 @@ module X402
|
|
|
214
180
|
end
|
|
215
181
|
|
|
216
182
|
def check_mempool!(txid)
|
|
217
|
-
@arc_client.status(txid)
|
|
218
|
-
|
|
219
|
-
raise VerificationError.new("mempool check failed: #{e.message}", status: 502)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Validate that the nonce key matches the nonce UTXO's P2PKH locking script
|
|
223
|
-
def validate_nonce_key!(nonce)
|
|
224
|
-
expected_h160 = @nonce_key.public_key.hash160.unpack1("H*")
|
|
225
|
-
expected_script = "76a914#{expected_h160}88ac"
|
|
226
|
-
return if nonce[:locking_script_hex] == expected_script
|
|
183
|
+
response = @arc_client.status(txid)
|
|
184
|
+
return if ACCEPTABLE_MEMPOOL_STATUSES.include?(response.tx_status)
|
|
227
185
|
|
|
228
|
-
raise
|
|
229
|
-
|
|
186
|
+
raise VerificationError.new("transaction not yet visible in mempool", status: 402)
|
|
187
|
+
rescue VerificationError
|
|
188
|
+
raise
|
|
189
|
+
rescue StandardError
|
|
190
|
+
raise VerificationError.new("mempool check failed", status: 502)
|
|
230
191
|
end
|
|
231
192
|
end
|
|
232
193
|
end
|
data/lib/x402/configuration.rb
CHANGED
|
@@ -6,12 +6,58 @@ module X402
|
|
|
6
6
|
|
|
7
7
|
GATEWAY_METHODS = %i[challenge_headers proof_header_names settle!].freeze
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
PAY_GATEWAY_KNOWN_OPTS = %i[
|
|
10
|
+
arc_client payee_locking_script_hex arc_wait_for arc_timeout
|
|
11
|
+
binding_mode wallet challenge_secret
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
PROOF_GATEWAY_KNOWN_OPTS = %i[
|
|
15
|
+
arc_client payee_locking_script_hex nonce_provider
|
|
16
|
+
wallet challenge_secret
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
BRC105_GATEWAY_KNOWN_OPTS = %i[
|
|
20
|
+
arc_client key_deriver server_wif server_key prefix_store
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
GATEWAY_REGISTRY = {
|
|
24
|
+
pay_gateway: "X402::BSV::PayGateway",
|
|
25
|
+
proof_gateway: "X402::BSV::ProofGateway",
|
|
26
|
+
brc105_gateway: "X402::BSV::BRC105Gateway"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
attr_accessor :domain, :payee_locking_script_hex, :gateways,
|
|
30
|
+
:arc_url, :arc_api_key, :arc_client
|
|
31
|
+
attr_reader :routes, :gateway_specs
|
|
11
32
|
|
|
12
33
|
def initialize
|
|
13
34
|
@routes = []
|
|
14
35
|
@gateways = []
|
|
36
|
+
@gateway_specs = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Record a gateway to be constructed later during +validate!+.
|
|
40
|
+
#
|
|
41
|
+
# @param name [Symbol] registered gateway name (e.g. +:pay_gateway+)
|
|
42
|
+
# @param options [Hash] options forwarded to the gateway constructor
|
|
43
|
+
# @raise [ConfigurationError] if the gateway name is not registered
|
|
44
|
+
def enable(name, **options)
|
|
45
|
+
class_name = GATEWAY_REGISTRY[name]
|
|
46
|
+
raise ConfigurationError, "unknown gateway: #{name.inspect}" unless class_name
|
|
47
|
+
|
|
48
|
+
@gateway_specs << [class_name, options]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns a memoised ARC client instance. If +arc_client+ has been
|
|
52
|
+
# injected directly, that takes precedence. Otherwise, builds one
|
|
53
|
+
# from +arc_url+ (and optional +arc_api_key+).
|
|
54
|
+
#
|
|
55
|
+
# @return [BSV::Network::ARC]
|
|
56
|
+
# @raise [ConfigurationError] if +arc_url+ is nil and no +arc_client+ injected
|
|
57
|
+
def shared_arc_client
|
|
58
|
+
return @arc_client if @arc_client
|
|
59
|
+
|
|
60
|
+
@shared_arc_client ||= build_arc_client
|
|
15
61
|
end
|
|
16
62
|
|
|
17
63
|
# Register a protected route.
|
|
@@ -40,6 +86,7 @@ module X402
|
|
|
40
86
|
raise ConfigurationError,
|
|
41
87
|
"payee_locking_script_hex is required"
|
|
42
88
|
end
|
|
89
|
+
build_gateways_from_specs! if gateways.empty? && !gateway_specs.empty?
|
|
43
90
|
validate_gateways!
|
|
44
91
|
raise ConfigurationError, "at least one route must be protected" if routes.empty?
|
|
45
92
|
end
|
|
@@ -74,6 +121,89 @@ module X402
|
|
|
74
121
|
end
|
|
75
122
|
end
|
|
76
123
|
|
|
124
|
+
def build_arc_client
|
|
125
|
+
raise ConfigurationError, "arc_url is required (or inject arc_client directly)" if arc_url.nil? || arc_url.empty?
|
|
126
|
+
|
|
127
|
+
::BSV::Network::ARC.new(arc_url, api_key: arc_api_key)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_gateways_from_specs!
|
|
131
|
+
require_relative "bsv"
|
|
132
|
+
require "bsv-wallet"
|
|
133
|
+
|
|
134
|
+
@gateways = gateway_specs.map do |class_name, options|
|
|
135
|
+
klass = Object.const_get(class_name)
|
|
136
|
+
case class_name
|
|
137
|
+
when "X402::BSV::PayGateway" then build_pay_gateway(klass, options)
|
|
138
|
+
when "X402::BSV::ProofGateway" then build_proof_gateway(klass, options)
|
|
139
|
+
when "X402::BSV::BRC105Gateway" then build_brc105_gateway(klass, options)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_pay_gateway(klass, options)
|
|
145
|
+
reject_unknown_options!(:pay_gateway, options, PAY_GATEWAY_KNOWN_OPTS)
|
|
146
|
+
opts = { arc_client: options[:arc_client] || shared_arc_client,
|
|
147
|
+
payee_locking_script_hex: options[:payee_locking_script_hex] || payee_locking_script_hex }
|
|
148
|
+
%i[arc_wait_for arc_timeout binding_mode wallet challenge_secret].each do |key|
|
|
149
|
+
opts[key] = options[key] if options.key?(key)
|
|
150
|
+
end
|
|
151
|
+
klass.new(**opts)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_proof_gateway(klass, options)
|
|
155
|
+
reject_unknown_options!(:proof_gateway, options, PROOF_GATEWAY_KNOWN_OPTS)
|
|
156
|
+
|
|
157
|
+
raise ConfigurationError, "proof_gateway requires nonce_provider:" unless options.key?(:nonce_provider)
|
|
158
|
+
|
|
159
|
+
opts = { arc_client: options[:arc_client] || shared_arc_client,
|
|
160
|
+
payee_locking_script_hex: options[:payee_locking_script_hex] || payee_locking_script_hex,
|
|
161
|
+
nonce_provider: options[:nonce_provider] }
|
|
162
|
+
|
|
163
|
+
%i[wallet challenge_secret].each do |key|
|
|
164
|
+
opts[key] = options[key] if options.key?(key)
|
|
165
|
+
end
|
|
166
|
+
klass.new(**opts)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def build_brc105_gateway(klass, options) # rubocop:disable Metrics/PerceivedComplexity
|
|
170
|
+
reject_unknown_options!(:brc105_gateway, options, BRC105_GATEWAY_KNOWN_OPTS)
|
|
171
|
+
|
|
172
|
+
key_sources = %i[key_deriver server_wif server_key].select { |k| options.key?(k) }
|
|
173
|
+
if key_sources.size > 1
|
|
174
|
+
raise ConfigurationError,
|
|
175
|
+
"brc105_gateway: #{key_sources.join(", ")} are mutually exclusive — provide only one"
|
|
176
|
+
end
|
|
177
|
+
if key_sources.empty?
|
|
178
|
+
raise ConfigurationError,
|
|
179
|
+
"brc105_gateway requires one of: key_deriver:, server_wif:, or server_key:"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
key_deriver = if options.key?(:key_deriver)
|
|
183
|
+
options[:key_deriver]
|
|
184
|
+
elsif options.key?(:server_wif)
|
|
185
|
+
::BSV::Wallet::KeyDeriver.new(::BSV::Primitives::PrivateKey.from_wif(options[:server_wif]))
|
|
186
|
+
else
|
|
187
|
+
::BSV::Wallet::KeyDeriver.new(options[:server_key])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
klass.new(
|
|
191
|
+
key_deriver: key_deriver,
|
|
192
|
+
prefix_store: options[:prefix_store] || X402::BSV::PrefixStore::Memory.new,
|
|
193
|
+
arc_client: options[:arc_client] || shared_arc_client
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# -- Option validation ---------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def reject_unknown_options!(gateway_name, options, known)
|
|
200
|
+
unknown = options.keys - known
|
|
201
|
+
return if unknown.empty?
|
|
202
|
+
|
|
203
|
+
raise ConfigurationError,
|
|
204
|
+
"#{gateway_name}: unknown option(s): #{unknown.map(&:inspect).join(", ")}"
|
|
205
|
+
end
|
|
206
|
+
|
|
77
207
|
def method_matches?(route_method, request_method)
|
|
78
208
|
route_method == "*" || route_method == request_method.upcase
|
|
79
209
|
end
|
data/lib/x402/middleware.rb
CHANGED
|
@@ -70,8 +70,8 @@ module X402
|
|
|
70
70
|
error_response(e.status, e.reason)
|
|
71
71
|
rescue X402::Error => e
|
|
72
72
|
error_response(400, e.message)
|
|
73
|
-
rescue StandardError
|
|
74
|
-
error_response(500, "internal error
|
|
73
|
+
rescue StandardError
|
|
74
|
+
error_response(500, "internal error")
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def error_response(status, reason)
|
|
@@ -65,8 +65,8 @@ module X402
|
|
|
65
65
|
json = Base64Url.decode(header_value)
|
|
66
66
|
data = JSON.parse(json, symbolize_names: true)
|
|
67
67
|
new(data)
|
|
68
|
-
rescue JSON::ParserError
|
|
69
|
-
raise X402::Error, "invalid challenge JSON
|
|
68
|
+
rescue JSON::ParserError
|
|
69
|
+
raise X402::Error, "invalid challenge JSON"
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
end
|
data/lib/x402/protocol/proof.rb
CHANGED
|
@@ -20,8 +20,8 @@ module X402
|
|
|
20
20
|
challenge_sha256: data[:challenge_sha256],
|
|
21
21
|
payment: data[:payment]
|
|
22
22
|
)
|
|
23
|
-
rescue JSON::ParserError
|
|
24
|
-
raise X402::Error, "invalid proof JSON
|
|
23
|
+
rescue JSON::ParserError
|
|
24
|
+
raise X402::Error, "invalid proof JSON"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Convenience accessors for payment fields.
|
data/lib/x402/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: x402-rack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-04-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -29,28 +29,28 @@ 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
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: bsv-wallet
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.2'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.2'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: json-canonicalization
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- ".claude/plans/20260326-rack-stack-architecture.md"
|
|
92
92
|
- ".rspec"
|
|
93
93
|
- ".rubocop.yml"
|
|
94
|
+
- CHANGELOG.md
|
|
94
95
|
- CLAUDE.md
|
|
95
96
|
- DESIGN.md
|
|
96
97
|
- LICENSE.txt
|