blockchain0x 0.0.1.alpha.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 +7 -0
- data/LICENSE +201 -0
- data/README.md +195 -0
- data/lib/blockchain0x/client.rb +194 -0
- data/lib/blockchain0x/errors.rb +85 -0
- data/lib/blockchain0x/resources/api_keys.rb +129 -0
- data/lib/blockchain0x/resources/payment_requests.rb +73 -0
- data/lib/blockchain0x/resources/payments.rb +82 -0
- data/lib/blockchain0x/resources/transactions.rb +32 -0
- data/lib/blockchain0x/version.rb +8 -0
- data/lib/blockchain0x/webhooks.rb +206 -0
- data/lib/blockchain0x.rb +33 -0
- metadata +109 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ef9ababb8a0e13928f13336087fc2fb9ea8762aeb4f18077f3574e1cddf491db
|
|
4
|
+
data.tar.gz: 833a4fab94699de66bc561c011cf4a9ceebf77a692175ea7b3594ffce11c5656
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f7646672e245cdbaebb3fcd45bbc1d216bdaa1538b7c58763f4d860a36413d851e498fb677ec8df6e5a081931b5f5a2802511e340b5b525718ed9c3bd3e8236e
|
|
7
|
+
data.tar.gz: 7a9c2d95eb3c8f5b1ea1fa7e5192a0528f3bc4b23eca2529e46ec6f22a782578dd035af4cf671efc9b5a23c5722a68fb6c70a8f9d0aee9a0ad0c7a0ef0c3d491
|
data/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Support. While redistributing the Work or
|
|
166
|
+
Derivative Works thereof, You may choose to offer, and charge a
|
|
167
|
+
fee for, acceptance of support, warranty, indemnity, or other
|
|
168
|
+
liability obligations and/or rights consistent with this License.
|
|
169
|
+
However, in accepting such obligations, You may act only on Your
|
|
170
|
+
own behalf and on Your sole responsibility, not on behalf of any
|
|
171
|
+
other Contributor, and only if You agree to indemnify, defend,
|
|
172
|
+
and hold each Contributor harmless for any liability incurred by,
|
|
173
|
+
or claims asserted against, such Contributor by reason of your
|
|
174
|
+
accepting any such warranty or support.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Tosh Labs
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
200
|
+
implied. See the License for the specific language governing
|
|
201
|
+
permissions and limitations under the License.
|
data/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# blockchain0x
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/blockchain0x)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#requirements)
|
|
6
|
+
|
|
7
|
+
**Official Ruby SDK for [Blockchain0x](https://blockchain0x.com)** -
|
|
8
|
+
the non-custodial AI-agent wallet platform on Base.
|
|
9
|
+
|
|
10
|
+
> Pre-release: `0.0.1.alpha.0` ships the operational essentials -
|
|
11
|
+
> HTTP transport, `api_keys` resource, and `Webhooks.verify`. The full
|
|
12
|
+
> surface (every resource + x402 client) lands in sub-plan 21.3 Phase
|
|
13
|
+
> C follow-up rows.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install blockchain0x --pre
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or in a `Gemfile`:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'blockchain0x', '~> 0.0.1.alpha'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Ruby 3.0 or newer.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'blockchain0x'
|
|
33
|
+
|
|
34
|
+
client = Blockchain0x::Client.new(api_key: ENV.fetch('BLOCKCHAIN0X_API_KEY'))
|
|
35
|
+
page = client.api_keys.list
|
|
36
|
+
page['data'].each { |k| puts "#{k['id']}\t#{k['prefix']}" }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The client pins the network from the API-key prefix (`sk_test_*` →
|
|
40
|
+
testnet, `sk_live_*` → mainnet). Override with
|
|
41
|
+
`Blockchain0x::Client.new(api_key: ..., network: 'mainnet')` when
|
|
42
|
+
running mixed-mode tests.
|
|
43
|
+
|
|
44
|
+
## Verify webhook signatures
|
|
45
|
+
|
|
46
|
+
The single most important utility this SDK ships - drop it into the
|
|
47
|
+
top of your webhook controller BEFORE touching the body.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Rails example. Adapt for Sinatra / Hanami / Roda - the only
|
|
51
|
+
# requirement is the raw request body, NOT a parsed JSON hash.
|
|
52
|
+
class WebhooksController < ApplicationController
|
|
53
|
+
skip_before_action :verify_authenticity_token, only: :receive
|
|
54
|
+
|
|
55
|
+
def receive
|
|
56
|
+
result = Blockchain0x::Webhooks.verify(
|
|
57
|
+
headers: request.headers,
|
|
58
|
+
raw_body: request.raw_post,
|
|
59
|
+
secret: ENV.fetch('BLOCKCHAIN0X_WEBHOOK_SECRET'),
|
|
60
|
+
)
|
|
61
|
+
return head(:bad_request) unless result.ok?
|
|
62
|
+
|
|
63
|
+
# result.event_type / result.event_id / result.delivery_id populated.
|
|
64
|
+
Sidekiq.redis do |r|
|
|
65
|
+
r.lpush("events:#{result.event_type}", request.raw_post)
|
|
66
|
+
end
|
|
67
|
+
head :no_content
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The verifier:
|
|
73
|
+
|
|
74
|
+
- Reads `X-Blockchain0x-Signature` in either `t=<ts>,v1=<hex>` or
|
|
75
|
+
bare-hex form (some load balancers strip commas).
|
|
76
|
+
- Falls back to `X-Blockchain0x-Timestamp` when the signature is bare.
|
|
77
|
+
- Rejects with `webhook.timestamp_outside_window` when drift exceeds
|
|
78
|
+
300 seconds.
|
|
79
|
+
- Constant-time compares via `OpenSSL.fixed_length_secure_compare`
|
|
80
|
+
(or a manual byte-XOR fallback for older OpenSSL builds).
|
|
81
|
+
|
|
82
|
+
For exception-based flows pass `raise_on_fail: true`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
begin
|
|
86
|
+
result = Blockchain0x::Webhooks.verify(
|
|
87
|
+
headers: request.headers,
|
|
88
|
+
raw_body: request.raw_post,
|
|
89
|
+
secret: ENV.fetch('BLOCKCHAIN0X_WEBHOOK_SECRET'),
|
|
90
|
+
raise_on_fail: true,
|
|
91
|
+
)
|
|
92
|
+
rescue Blockchain0x::WebhookSignatureError => e
|
|
93
|
+
return render json: { code: e.code }, status: :bad_request
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
Two classes:
|
|
100
|
+
|
|
101
|
+
- `Blockchain0x::Error` - base class; every SDK error inherits.
|
|
102
|
+
- `Blockchain0x::APIKeyError` - subclass for HTTP 401 / 403 envelopes
|
|
103
|
+
whose `error.code` starts with `apikey.` (e.g.
|
|
104
|
+
`apikey.scope_insufficient`, `apikey.wallet_not_assigned`).
|
|
105
|
+
|
|
106
|
+
Always branch on `.code`, never regex-match `.message`:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
begin
|
|
110
|
+
client.api_keys.list
|
|
111
|
+
rescue Blockchain0x::APIKeyError => e
|
|
112
|
+
if e.code == 'apikey.scope_insufficient'
|
|
113
|
+
# mint a fresh key with more scope
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The module helper `Blockchain0x.apikey_error?(error)` returns true
|
|
119
|
+
when the exception's code begins with `apikey.` regardless of
|
|
120
|
+
subclass.
|
|
121
|
+
|
|
122
|
+
## Retry behaviour
|
|
123
|
+
|
|
124
|
+
The transport retries on `429` and `5xx` with exponential backoff
|
|
125
|
+
(0.5s → 1s → 2s → … → 30s cap, 3 retries by default).
|
|
126
|
+
`Retry-After` is honoured when the server sends it.
|
|
127
|
+
|
|
128
|
+
`POST` / `PATCH` / `DELETE` requests carry an `Idempotency-Key`
|
|
129
|
+
header - the SDK mints a UUID v4 if you do not supply one. Pass
|
|
130
|
+
`idempotency_key: '...'` to thread a stable key across SDK retries OR
|
|
131
|
+
across processes (e.g. a cron job that hashes its input
|
|
132
|
+
deterministically).
|
|
133
|
+
|
|
134
|
+
## Workspace keys (sub-plan 21.3)
|
|
135
|
+
|
|
136
|
+
Two key shapes exist (see
|
|
137
|
+
[docs/concept-api-key-types.md](https://github.com/Tosh-Labs/blockchain0x-app/blob/dev/docs/concept-api-key-types.md)
|
|
138
|
+
for the full decision tree):
|
|
139
|
+
|
|
140
|
+
- **Wallet-only** - bound to ONE agent. Right shape for an autonomous AI agent that IS one wallet.
|
|
141
|
+
- **Workspace** - human-operator key that can carry workspace-level scopes AND assignments to N specific wallets.
|
|
142
|
+
|
|
143
|
+
The Ruby SDK forwards both shapes through `client.api_keys`. Once
|
|
144
|
+
the C-2-style `create` method lands beyond the alpha scaffold:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
key = client.api_keys.create(
|
|
148
|
+
label: 'Treasury reconciliation',
|
|
149
|
+
workspace_scopes: ['read_workspace'],
|
|
150
|
+
wallet_assignments: [
|
|
151
|
+
{ agent_id: 'agt_trading', scopes: ['read_wallet_metadata'] },
|
|
152
|
+
{ agent_id: 'agt_settlement', scopes: ['read_wallet_metadata'] },
|
|
153
|
+
],
|
|
154
|
+
expires_in_days: 30,
|
|
155
|
+
)
|
|
156
|
+
puts key['secret'] # shown ONCE
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Server-side RBAC: the minter cannot grant a scope they do not have
|
|
160
|
+
themselves. Over-grants reject with `apikey.role_insufficient_for_grants`
|
|
161
|
+
which is surfaced as a `Blockchain0x::APIKeyError`.
|
|
162
|
+
|
|
163
|
+
## x402 (Phase C-7)
|
|
164
|
+
|
|
165
|
+
The sibling gem `blockchain0x-x402` (Ruby port of
|
|
166
|
+
[`@blockchain0x/x402`](https://www.npmjs.com/package/@blockchain0x/x402))
|
|
167
|
+
will ship the x402 client + Rack middleware in sub-plan 21.3 row C-7.
|
|
168
|
+
The wire format is identical across languages so a Ruby service can
|
|
169
|
+
accept payments from a Node client and vice-versa.
|
|
170
|
+
|
|
171
|
+
## Codegen
|
|
172
|
+
|
|
173
|
+
Model classes are generated from
|
|
174
|
+
`apps/backend/openapi/openapi.yaml` via
|
|
175
|
+
[openapi-generator-cli](https://github.com/OpenAPITools/openapi-generator)
|
|
176
|
+
with `-g ruby --global-property=models,supportingFiles=false` - see
|
|
177
|
+
[codegen/README.md](./codegen/README.md) for the decision
|
|
178
|
+
rationale.
|
|
179
|
+
|
|
180
|
+
## Source-of-truth + distribution
|
|
181
|
+
|
|
182
|
+
Source-of-truth: this directory in
|
|
183
|
+
[Tosh-Labs/blockchain0x-app](https://github.com/Tosh-Labs/blockchain0x-app)
|
|
184
|
+
under `packages/sdk-ruby/`.
|
|
185
|
+
|
|
186
|
+
Public mirror: [Tosh-Labs/blockchain0x-ruby](https://github.com/Tosh-Labs/blockchain0x-ruby)
|
|
187
|
+
(receives merges from this directory on dispatch of the
|
|
188
|
+
`mirror-sdk-ruby` workflow).
|
|
189
|
+
|
|
190
|
+
Distribution: [rubygems](https://rubygems.org/gems/blockchain0x) via
|
|
191
|
+
Trusted Publisher OIDC.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
Apache-2.0.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Sub-plan 21.3 row C-4: Ruby HTTP client.
|
|
2
|
+
#
|
|
3
|
+
# Behaviour mirrors the @blockchain0x/node + Python + Go SDKs:
|
|
4
|
+
#
|
|
5
|
+
# - injects Bearer + X-Network headers on every request,
|
|
6
|
+
# - mints an Idempotency-Key on POST/PATCH/DELETE unless caller
|
|
7
|
+
# supplies one,
|
|
8
|
+
# - retries 5xx + 429 with exponential backoff (Retry-After
|
|
9
|
+
# honoured),
|
|
10
|
+
# - converts the canonical error envelope into APIKeyError / Error.
|
|
11
|
+
#
|
|
12
|
+
# Network selection:
|
|
13
|
+
# - `sk_test_*` keys force testnet,
|
|
14
|
+
# - `sk_live_*` keys force mainnet,
|
|
15
|
+
# - explicit `network:` kwarg overrides the prefix when the SDK is
|
|
16
|
+
# used in mixed-mode tests.
|
|
17
|
+
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
require 'faraday'
|
|
21
|
+
require 'json'
|
|
22
|
+
require 'securerandom'
|
|
23
|
+
require_relative 'errors'
|
|
24
|
+
require_relative 'version'
|
|
25
|
+
|
|
26
|
+
module Blockchain0x
|
|
27
|
+
class Client
|
|
28
|
+
DEFAULT_BASE_URL = 'https://api.blockchain0x.com'
|
|
29
|
+
DEFAULT_TIMEOUT = 30
|
|
30
|
+
DEFAULT_MAX_RETRIES = 3
|
|
31
|
+
USER_AGENT = "blockchain0x-ruby/#{VERSION}"
|
|
32
|
+
|
|
33
|
+
# @param api_key [String] required; sk_test_... or sk_live_...
|
|
34
|
+
# @param base_url [String] override the default API host
|
|
35
|
+
# @param network [Symbol,String] :mainnet / :testnet override
|
|
36
|
+
# @param timeout [Integer] per-request wall-clock seconds
|
|
37
|
+
# @param max_retries [Integer] retry budget on 5xx + 429
|
|
38
|
+
# @param adapter [Symbol] Faraday adapter (default :net_http)
|
|
39
|
+
# @param connection [Faraday::Connection] override for tests
|
|
40
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, network: nil, timeout: DEFAULT_TIMEOUT,
|
|
41
|
+
max_retries: DEFAULT_MAX_RETRIES, adapter: :net_http, connection: nil)
|
|
42
|
+
raise ArgumentError, 'api_key is required' if api_key.nil? || api_key.empty?
|
|
43
|
+
|
|
44
|
+
@api_key = api_key
|
|
45
|
+
@network = (network || network_from_key(api_key)).to_s
|
|
46
|
+
@max_retries = max_retries
|
|
47
|
+
@connection = connection || build_connection(base_url, timeout, adapter)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def api_keys
|
|
51
|
+
@api_keys ||= begin
|
|
52
|
+
require_relative 'resources/api_keys'
|
|
53
|
+
Resources::APIKeys.new(self)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def payment_requests
|
|
58
|
+
@payment_requests ||= begin
|
|
59
|
+
require_relative 'resources/payment_requests'
|
|
60
|
+
Resources::PaymentRequests.new(self)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def payments
|
|
65
|
+
@payments ||= begin
|
|
66
|
+
require_relative 'resources/payments'
|
|
67
|
+
Resources::Payments.new(self)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def transactions
|
|
72
|
+
@transactions ||= begin
|
|
73
|
+
require_relative 'resources/transactions'
|
|
74
|
+
Resources::Transactions.new(self)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ---- low-level verb helpers --------------------------------------
|
|
79
|
+
def get(path, params: nil)
|
|
80
|
+
request(:get, path, params: params)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def post(path, body: nil, idempotency_key: nil)
|
|
84
|
+
request(:post, path, body: body, idempotency_key: idempotency_key)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def patch(path, body: nil, idempotency_key: nil)
|
|
88
|
+
request(:patch, path, body: body, idempotency_key: idempotency_key)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def delete(path, idempotency_key: nil)
|
|
92
|
+
request(:delete, path, idempotency_key: idempotency_key)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def request(method, path, params: nil, body: nil, idempotency_key: nil)
|
|
98
|
+
headers = {
|
|
99
|
+
'Authorization' => "Bearer #{@api_key}",
|
|
100
|
+
'Accept' => 'application/json',
|
|
101
|
+
'User-Agent' => USER_AGENT,
|
|
102
|
+
'X-Network' => @network,
|
|
103
|
+
}
|
|
104
|
+
headers['Content-Type'] = 'application/json' if body
|
|
105
|
+
if %i[post patch delete].include?(method)
|
|
106
|
+
headers['Idempotency-Key'] = idempotency_key || SecureRandom.uuid
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
payload = body.nil? ? nil : JSON.generate(body)
|
|
110
|
+
|
|
111
|
+
attempt = 0
|
|
112
|
+
loop do
|
|
113
|
+
attempt += 1
|
|
114
|
+
begin
|
|
115
|
+
response = @connection.run_request(method, path, payload, headers) do |req|
|
|
116
|
+
req.params.update(params) if params
|
|
117
|
+
end
|
|
118
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
|
|
119
|
+
raise Error.new(code: 'network.unavailable', message: e.message) if attempt > @max_retries
|
|
120
|
+
|
|
121
|
+
sleep(backoff(attempt))
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if response.status < 400
|
|
126
|
+
return parse_success(response)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if response.status == 429 || response.status >= 500
|
|
130
|
+
if attempt > @max_retries
|
|
131
|
+
raise build_error(response)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
wait = response.headers['Retry-After']&.to_i
|
|
135
|
+
sleep(wait || backoff(attempt))
|
|
136
|
+
next
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
raise build_error(response)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parse_success(response)
|
|
144
|
+
return nil if response.status == 204 || response.body.nil? || response.body.empty?
|
|
145
|
+
|
|
146
|
+
JSON.parse(response.body)
|
|
147
|
+
rescue JSON::ParserError
|
|
148
|
+
raise Error.new(code: 'response.invalid_envelope', message: 'non-JSON response body')
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def build_error(response)
|
|
152
|
+
request_id = response.headers['x-request-id']
|
|
153
|
+
payload = begin
|
|
154
|
+
JSON.parse(response.body || '')
|
|
155
|
+
rescue JSON::ParserError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
err = payload.is_a?(Hash) ? payload['error'] : nil
|
|
159
|
+
unless err.is_a?(Hash) && err['code']
|
|
160
|
+
return Error.new(
|
|
161
|
+
code: 'response.invalid_envelope',
|
|
162
|
+
message: "non-canonical error body (status=#{response.status})",
|
|
163
|
+
http_status: response.status,
|
|
164
|
+
request_id: request_id,
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
code = err['code'].to_s
|
|
169
|
+
message = err['message'].to_s
|
|
170
|
+
# The canonical error envelope carries the request id in the body
|
|
171
|
+
# (`error.requestId`); the `x-request-id` response header is a
|
|
172
|
+
# fallback for transports that surface it there instead.
|
|
173
|
+
request_id = err['requestId'] || request_id
|
|
174
|
+
klass = code.start_with?('apikey.') ? APIKeyError : Error
|
|
175
|
+
klass.new(code: code, message: message, http_status: response.status, request_id: request_id)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def backoff(attempt)
|
|
179
|
+
[0.5 * (2**(attempt - 1)), 30.0].min
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def network_from_key(api_key)
|
|
183
|
+
api_key.start_with?('sk_live_') ? 'mainnet' : 'testnet'
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def build_connection(base_url, timeout, adapter)
|
|
187
|
+
Faraday.new(url: base_url) do |f|
|
|
188
|
+
f.options.timeout = timeout
|
|
189
|
+
f.options.open_timeout = timeout
|
|
190
|
+
f.adapter adapter
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Sub-plan 21.3 C-4 + C-6 (Ruby). Mirrors the @blockchain0x/node +
|
|
2
|
+
# Python + Go SDK error hierarchies so consumers branch on stable
|
|
3
|
+
# wire `code` strings instead of regex-matching messages.
|
|
4
|
+
#
|
|
5
|
+
# Blockchain0x::Error (every SDK error)
|
|
6
|
+
# Blockchain0x::APIKeyError (HTTP 401/403 with apikey.*)
|
|
7
|
+
# Blockchain0x::WebhookSignatureError (raised only by
|
|
8
|
+
# Webhooks.verify(..., raise_on_fail: true);
|
|
9
|
+
# default verify returns a Result struct)
|
|
10
|
+
#
|
|
11
|
+
# Every error carries:
|
|
12
|
+
# code stable wire-level code from the openapi catalog
|
|
13
|
+
# message human-readable description
|
|
14
|
+
# http_status the HTTP status that produced this error (nil for
|
|
15
|
+
# client-side raises like WebhookSignatureError)
|
|
16
|
+
# request_id server-issued request id (nil on local-only errors)
|
|
17
|
+
|
|
18
|
+
# frozen_string_literal: true
|
|
19
|
+
|
|
20
|
+
module Blockchain0x
|
|
21
|
+
class Error < StandardError
|
|
22
|
+
attr_reader :code, :http_status, :request_id
|
|
23
|
+
|
|
24
|
+
def initialize(code:, message:, http_status: nil, request_id: nil)
|
|
25
|
+
super("#{code}: #{message}")
|
|
26
|
+
@code = code
|
|
27
|
+
@http_status = http_status
|
|
28
|
+
@request_id = request_id
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Subclass for HTTP 401 / 403 envelopes whose `error.code` starts
|
|
33
|
+
# with `apikey.`. Catch this to handle apikey-related failures:
|
|
34
|
+
#
|
|
35
|
+
# begin
|
|
36
|
+
# client.api_keys.list
|
|
37
|
+
# rescue Blockchain0x::APIKeyError => e
|
|
38
|
+
# if e.code == 'apikey.scope_insufficient'
|
|
39
|
+
# # mint a fresh key with more scope
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
class APIKeyError < Error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Raised by Webhooks.verify(..., raise_on_fail: true). The default
|
|
46
|
+
# `verify` path returns a discriminated-union Result; this exists
|
|
47
|
+
# for integrations that prefer exceptions over branching on
|
|
48
|
+
# `result.ok?`.
|
|
49
|
+
class WebhookSignatureError < Error
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Boolean] true when the exception's code begins with
|
|
53
|
+
# `apikey.` (regardless of which subclass it is).
|
|
54
|
+
def self.apikey_error?(error)
|
|
55
|
+
return false unless error.respond_to?(:code) && error.code.is_a?(String)
|
|
56
|
+
|
|
57
|
+
error.code.start_with?('apikey.')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Stable apikey.* failure-mode catalog (sub-plan 21.3 row C-8). The
|
|
61
|
+
# backend emits these codes verbatim in the wire envelope; SDK
|
|
62
|
+
# consumers branch on them via APIKeyError#code. Splits into four
|
|
63
|
+
# groups: identity-bound, agent-flavor binding, surface-restriction,
|
|
64
|
+
# and the four 21.3-introduced workspace-flavor codes.
|
|
65
|
+
module APIKeyCodes
|
|
66
|
+
# Identity-bound (the key itself is invalid).
|
|
67
|
+
INVALID = 'apikey.invalid'
|
|
68
|
+
REVOKED = 'apikey.revoked'
|
|
69
|
+
EXPIRED = 'apikey.expired'
|
|
70
|
+
# Agent-flavor binding errors (sub-plan 21.1).
|
|
71
|
+
AGENT_REVOKED = 'apikey.agent_revoked'
|
|
72
|
+
AGENT_MISMATCH = 'apikey.agent_mismatch'
|
|
73
|
+
# Surface-restriction errors.
|
|
74
|
+
WORKSPACE_ENDPOINT_BLOCKED = 'apikey.workspace_endpoint_blocked'
|
|
75
|
+
UNSUPPORTED_ENDPOINT = 'apikey.unsupported_endpoint'
|
|
76
|
+
# Network + scope.
|
|
77
|
+
NETWORK_MISMATCH = 'apikey.network_mismatch'
|
|
78
|
+
SCOPE_INSUFFICIENT = 'apikey.scope_insufficient'
|
|
79
|
+
# Workspace-flavor errors (sub-plan 21.3).
|
|
80
|
+
WALLET_NOT_ASSIGNED = 'apikey.wallet_not_assigned'
|
|
81
|
+
WORKSPACE_SCOPE_INSUFFICIENT = 'apikey.workspace_scope_insufficient'
|
|
82
|
+
ROLE_INSUFFICIENT_FOR_GRANTS = 'apikey.role_insufficient_for_grants'
|
|
83
|
+
NO_GRANTS_REMAINING = 'apikey.no_grants_remaining'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Sub-plan 21.3 row C-4 first resource: api_keys.
|
|
2
|
+
#
|
|
3
|
+
# Thin wrapper over the wire shape. Until codegen lands
|
|
4
|
+
# (codegen/regenerate.sh per the C-1 decision), the response is a
|
|
5
|
+
# plain Hash mirroring the openapi `ApiKey` schema verbatim:
|
|
6
|
+
#
|
|
7
|
+
# { 'id' => ..., 'prefix' => ..., 'scopes' => [...],
|
|
8
|
+
# 'workspaceScopes' => [...], 'walletAssignments' => [...] }
|
|
9
|
+
#
|
|
10
|
+
# Once codegen ships, this resource returns typed model classes.
|
|
11
|
+
#
|
|
12
|
+
# C-4 follow-up: typed `create_workspace_key` + `create_agent_key`
|
|
13
|
+
# close the sdk-parity-matrix "Workspace-flavor `create` body" 🟡
|
|
14
|
+
# drift for Ruby. Mirrors the Python `create_workspace_key` +
|
|
15
|
+
# Go `CreateWorkspaceKey` signatures.
|
|
16
|
+
|
|
17
|
+
# frozen_string_literal: true
|
|
18
|
+
|
|
19
|
+
module Blockchain0x
|
|
20
|
+
module Resources
|
|
21
|
+
# WalletAssignment is the typed per-wallet entry for the
|
|
22
|
+
# workspace-flavor create body. Splits an `agent_id` + `scopes`
|
|
23
|
+
# array. The 4 valid scopes are `read_wallet_metadata`,
|
|
24
|
+
# `manage_wallet_metadata`, `pay_bills`, `receive_money`.
|
|
25
|
+
WalletAssignment = Struct.new(:agent_id, :scopes, keyword_init: true) do
|
|
26
|
+
def to_h_wire
|
|
27
|
+
{ 'agentId' => agent_id, 'scopes' => Array(scopes) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class APIKeys
|
|
32
|
+
def initialize(client)
|
|
33
|
+
@client = client
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Hash] cursor-paginated page envelope
|
|
37
|
+
def list
|
|
38
|
+
@client.get('/v1/api-keys')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param api_key_id [String]
|
|
42
|
+
# @return [Hash]
|
|
43
|
+
def get(api_key_id)
|
|
44
|
+
raise ArgumentError, 'api_key_id is required' if api_key_id.nil? || api_key_id.empty?
|
|
45
|
+
|
|
46
|
+
@client.get("/v1/api-keys/#{api_key_id}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Revoke an API key. Returns nil (204 No Content).
|
|
50
|
+
def revoke(api_key_id)
|
|
51
|
+
raise ArgumentError, 'api_key_id is required' if api_key_id.nil? || api_key_id.empty?
|
|
52
|
+
|
|
53
|
+
@client.delete("/v1/api-keys/#{api_key_id}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Mint a sub-plan 21.3 workspace-flavor API key.
|
|
57
|
+
#
|
|
58
|
+
# At least one of `workspace_scopes` or `wallet_assignments`
|
|
59
|
+
# MUST be non-empty - a workspace-flavor key with neither
|
|
60
|
+
# rejects at the server with `request.invalid`. The client
|
|
61
|
+
# raises ArgumentError before issuing the wire call so the
|
|
62
|
+
# mistake is caught locally.
|
|
63
|
+
#
|
|
64
|
+
# @param label [String] human-readable label shown in the dashboard.
|
|
65
|
+
# @param workspace_scopes [Array<String>, nil] zero or more workspace-level scopes.
|
|
66
|
+
# @param wallet_assignments [Array<WalletAssignment>, nil] zero or more per-wallet grants.
|
|
67
|
+
# @param expires_in_days [Integer, nil] optional rotation hint (7/30/60/90).
|
|
68
|
+
# @param idempotency_key [String, nil] explicit override; otherwise the client mints a fresh uuid4.
|
|
69
|
+
# @return [Hash] the raw envelope `{ 'id', 'secret', 'prefix', ... }`. The `secret` field is the
|
|
70
|
+
# plaintext key returned ONCE; persist it now.
|
|
71
|
+
# @raise [APIKeyError] when the RBAC ceiling rejects the grant
|
|
72
|
+
# (`apikey.role_insufficient_for_grants`).
|
|
73
|
+
def create_workspace_key(
|
|
74
|
+
label:,
|
|
75
|
+
workspace_scopes: nil,
|
|
76
|
+
wallet_assignments: nil,
|
|
77
|
+
expires_in_days: nil,
|
|
78
|
+
idempotency_key: nil
|
|
79
|
+
)
|
|
80
|
+
raise ArgumentError, 'label is required' if label.nil? || label.empty?
|
|
81
|
+
|
|
82
|
+
ws = Array(workspace_scopes)
|
|
83
|
+
wa = Array(wallet_assignments)
|
|
84
|
+
if ws.empty? && wa.empty?
|
|
85
|
+
raise ArgumentError,
|
|
86
|
+
'workspace-flavor key needs at least one workspace_scope OR wallet_assignment'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
body = { 'label' => label }
|
|
90
|
+
body['workspaceScopes'] = ws unless ws.empty?
|
|
91
|
+
body['walletAssignments'] = wa.map { |a| a.respond_to?(:to_h_wire) ? a.to_h_wire : a } unless wa.empty?
|
|
92
|
+
body['expiresInDays'] = expires_in_days unless expires_in_days.nil?
|
|
93
|
+
|
|
94
|
+
@client.post('/v1/api-keys', body: body, idempotency_key: idempotency_key)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Mint a legacy 21.1 agent-bound API key.
|
|
98
|
+
#
|
|
99
|
+
# @param agent_id [String] the agent the key is bound to.
|
|
100
|
+
# @param label [String] human-readable label.
|
|
101
|
+
# @param scopes [Array<String>] one or more wallet-scoped permissions.
|
|
102
|
+
# @param expires_in_days [Integer, nil] optional rotation hint.
|
|
103
|
+
# @param idempotency_key [String, nil] explicit override.
|
|
104
|
+
# @return [Hash] the raw envelope `{ 'id', 'secret', 'prefix', ... }`.
|
|
105
|
+
def create_agent_key(
|
|
106
|
+
agent_id:,
|
|
107
|
+
label:,
|
|
108
|
+
scopes:,
|
|
109
|
+
expires_in_days: nil,
|
|
110
|
+
idempotency_key: nil
|
|
111
|
+
)
|
|
112
|
+
raise ArgumentError, 'agent_id is required' if agent_id.nil? || agent_id.empty?
|
|
113
|
+
raise ArgumentError, 'label is required' if label.nil? || label.empty?
|
|
114
|
+
|
|
115
|
+
sc = Array(scopes)
|
|
116
|
+
raise ArgumentError, 'at least one scope is required' if sc.empty?
|
|
117
|
+
|
|
118
|
+
body = {
|
|
119
|
+
'agentId' => agent_id,
|
|
120
|
+
'label' => label,
|
|
121
|
+
'scopes' => sc,
|
|
122
|
+
}
|
|
123
|
+
body['expiresInDays'] = expires_in_days unless expires_in_days.nil?
|
|
124
|
+
|
|
125
|
+
@client.post('/v1/api-keys', body: body, idempotency_key: idempotency_key)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Sub-plan 21.2 row A-3 / B-3 (Ruby port).
|
|
2
|
+
#
|
|
3
|
+
# `client.payment_requests.settle` is the path the x402 server adapter
|
|
4
|
+
# calls after verifying an X-Payment header. The request body carries
|
|
5
|
+
# the on-chain proof tuple (txHash + payerAddress + amountUsdcVerified);
|
|
6
|
+
# the backend re-verifies it against the canonical `transactions`
|
|
7
|
+
# table before flipping the invoice to `settled`. Trust model: the
|
|
8
|
+
# SDK is a thin wrapper, the server is the trust anchor.
|
|
9
|
+
#
|
|
10
|
+
# No idempotency_key mint here - settle is naturally idempotent
|
|
11
|
+
# server-side: an invoice already in `settled` state returns 409
|
|
12
|
+
# `payment_request.not_settleable`, never a duplicate event.
|
|
13
|
+
|
|
14
|
+
# frozen_string_literal: true
|
|
15
|
+
|
|
16
|
+
module Blockchain0x
|
|
17
|
+
module Resources
|
|
18
|
+
# PaymentRequestSettleBody is the typed proof-tuple. Snake_case
|
|
19
|
+
# on the Ruby side, camelCase on the wire (the `#to_h_wire`
|
|
20
|
+
# serialiser handles the rename). Mirrors the Python
|
|
21
|
+
# PaymentRequestSettleBody dataclass + Go PaymentRequestSettleBody
|
|
22
|
+
# struct.
|
|
23
|
+
PaymentRequestSettleBody = Struct.new(
|
|
24
|
+
:tx_hash,
|
|
25
|
+
:payer_address,
|
|
26
|
+
:amount_usdc_verified,
|
|
27
|
+
keyword_init: true,
|
|
28
|
+
) do
|
|
29
|
+
def to_h_wire
|
|
30
|
+
{
|
|
31
|
+
'txHash' => tx_hash,
|
|
32
|
+
'payerAddress' => payer_address,
|
|
33
|
+
'amountUsdcVerified' => amount_usdc_verified,
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# PaymentRequestsResource is `client.payment_requests.*`.
|
|
39
|
+
class PaymentRequests
|
|
40
|
+
def initialize(client)
|
|
41
|
+
@client = client
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Settle a payment request with the on-chain proof tuple.
|
|
45
|
+
#
|
|
46
|
+
# @param payment_request_id [String] the `pr_*` id from the 402
|
|
47
|
+
# `accepts[].paymentRequestId` field.
|
|
48
|
+
# @param body [PaymentRequestSettleBody, Hash] either the typed
|
|
49
|
+
# Struct OR a plain hash with the canonical camelCase shape -
|
|
50
|
+
# the x402 server adapter already passes the dict directly.
|
|
51
|
+
# @return [Hash] the raw envelope
|
|
52
|
+
# `{ 'id', 'status' => 'settled', 'settledTxHash', 'settledAt' }`.
|
|
53
|
+
# @raise [ArgumentError] when payment_request_id is empty.
|
|
54
|
+
# @raise [Blockchain0x::Error] on any non-2xx envelope.
|
|
55
|
+
def settle(payment_request_id, body)
|
|
56
|
+
if payment_request_id.nil? || payment_request_id.to_s.empty?
|
|
57
|
+
raise ArgumentError, 'payment_request_id is required'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
wire_body =
|
|
61
|
+
if body.respond_to?(:to_h_wire)
|
|
62
|
+
body.to_h_wire
|
|
63
|
+
else
|
|
64
|
+
# Accept the canonical camelCase shape verbatim - the
|
|
65
|
+
# x402 server adapter already passes this exact shape.
|
|
66
|
+
body.to_h.transform_keys(&:to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@client.post("/v1/payment-requests/#{payment_request_id}/settle", body: wire_body)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Sub-plan 21.1 row B-4 (Ruby port).
|
|
2
|
+
#
|
|
3
|
+
# `client.payments.create` is the agent's outbound-spend path. Two
|
|
4
|
+
# safety differences from every other SDK call:
|
|
5
|
+
#
|
|
6
|
+
# 1. **Auto Idempotency-Key.** The underlying `Client#post` already
|
|
7
|
+
# mints a uuid4 on every POST; this resource exposes an explicit
|
|
8
|
+
# `idempotency_key:` knob so consumers can thread a stable value
|
|
9
|
+
# across retries.
|
|
10
|
+
# 2. **Retry stays at the global Client setting.** A payment retry
|
|
11
|
+
# without a matching idempotency key could double-submit. The
|
|
12
|
+
# Ruby SDK does not yet expose a per-call retry override (the
|
|
13
|
+
# Node SDK's `retry: 'off' | 'default'` switch is a future
|
|
14
|
+
# extension); consumers control retry via the Client's
|
|
15
|
+
# `max_retries:` argument.
|
|
16
|
+
|
|
17
|
+
# frozen_string_literal: true
|
|
18
|
+
|
|
19
|
+
module Blockchain0x
|
|
20
|
+
module Resources
|
|
21
|
+
# PaymentCreateBody is the typed body for POST /v1/payments.
|
|
22
|
+
# Snake_case on the Ruby side; camelCase on the wire (via
|
|
23
|
+
# `#to_h_wire`). Mirrors the Python PaymentCreateBody dataclass +
|
|
24
|
+
# Go PaymentCreateRequest struct.
|
|
25
|
+
PaymentCreateBody = Struct.new(
|
|
26
|
+
:agent_id,
|
|
27
|
+
:to,
|
|
28
|
+
:amount_wei,
|
|
29
|
+
:token,
|
|
30
|
+
:metadata,
|
|
31
|
+
keyword_init: true,
|
|
32
|
+
) do
|
|
33
|
+
def initialize(agent_id:, to:, amount_wei:, token: nil, metadata: nil)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h_wire
|
|
38
|
+
h = {
|
|
39
|
+
'agentId' => agent_id,
|
|
40
|
+
'to' => to,
|
|
41
|
+
'amountWei' => amount_wei,
|
|
42
|
+
}
|
|
43
|
+
h['token'] = token unless token.nil?
|
|
44
|
+
h['metadata'] = metadata.to_h unless metadata.nil?
|
|
45
|
+
h
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `client.payments.*` resource.
|
|
50
|
+
class Payments
|
|
51
|
+
def initialize(client)
|
|
52
|
+
@client = client
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Submit an outbound payment.
|
|
56
|
+
#
|
|
57
|
+
# @param body [PaymentCreateBody, Hash] either the typed Struct
|
|
58
|
+
# OR a plain hash with the canonical camelCase shape - the
|
|
59
|
+
# x402 client wrapper passes a hash directly so it just works.
|
|
60
|
+
# @param idempotency_key [String, nil] explicit override;
|
|
61
|
+
# otherwise the underlying Client mints a uuid4.
|
|
62
|
+
# @return [Hash] raw envelope
|
|
63
|
+
# `{ 'id', 'agentId', 'status', 'txHash'?, 'amountWei', 'network', ... }`.
|
|
64
|
+
# @raise [ArgumentError] when any required field is empty.
|
|
65
|
+
# @raise [Blockchain0x::Error] on any non-2xx envelope.
|
|
66
|
+
def create(body:, idempotency_key: nil)
|
|
67
|
+
wire =
|
|
68
|
+
if body.respond_to?(:to_h_wire)
|
|
69
|
+
body.to_h_wire
|
|
70
|
+
else
|
|
71
|
+
body.to_h.transform_keys(&:to_s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
raise ArgumentError, 'agentId is required' if wire['agentId'].nil? || wire['agentId'].empty?
|
|
75
|
+
raise ArgumentError, 'to is required' if wire['to'].nil? || wire['to'].empty?
|
|
76
|
+
raise ArgumentError, 'amountWei is required' if wire['amountWei'].nil? || wire['amountWei'].empty?
|
|
77
|
+
|
|
78
|
+
@client.post('/v1/payments', body: wire, idempotency_key: idempotency_key)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Sub-plan 21.2 row B-3 (Ruby port).
|
|
2
|
+
#
|
|
3
|
+
# Read-only handle on the `transactions` table. The x402 client polls
|
|
4
|
+
# `client.transactions.get` to find out when a freshly broadcast
|
|
5
|
+
# `payments.create` has confirmed on-chain (status flips to
|
|
6
|
+
# `confirmed`). Scope: `read_wallet_metadata`.
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
module Blockchain0x
|
|
11
|
+
module Resources
|
|
12
|
+
class Transactions
|
|
13
|
+
def initialize(client)
|
|
14
|
+
@client = client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Fetch one transaction by id.
|
|
18
|
+
#
|
|
19
|
+
# @param transaction_id [String] the `tx_*` id returned from
|
|
20
|
+
# `payments.create` (or a chain webhook).
|
|
21
|
+
# @return [Hash] raw envelope.
|
|
22
|
+
# @raise [ArgumentError] when transaction_id is empty.
|
|
23
|
+
def get(transaction_id)
|
|
24
|
+
if transaction_id.nil? || transaction_id.to_s.empty?
|
|
25
|
+
raise ArgumentError, 'transaction_id is required'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@client.get("/v1/transactions/#{transaction_id}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Sub-plan 21.3 row C-6 (Ruby). Ports the @blockchain0x/node + Python
|
|
2
|
+
# + Go `webhooks.verify` byte-for-byte.
|
|
3
|
+
#
|
|
4
|
+
# Wire format (mirrors apps/worker/src/adapters/webhook.ts):
|
|
5
|
+
#
|
|
6
|
+
# X-Blockchain0x-Timestamp: <unix seconds>
|
|
7
|
+
# X-Blockchain0x-Signature: t=<ts>,v1=<hex>
|
|
8
|
+
# X-Blockchain0x-Event-Type: <slug>
|
|
9
|
+
# X-Blockchain0x-Event-Id: <ulid>
|
|
10
|
+
# X-Blockchain0x-Delivery-Id: webhook_<id>
|
|
11
|
+
#
|
|
12
|
+
# Algorithm:
|
|
13
|
+
#
|
|
14
|
+
# want = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{ts}.#{raw_body}")
|
|
15
|
+
# ok = OpenSSL.fixed_length_secure_compare(want, sig) &&
|
|
16
|
+
# (Time.now.to_i - ts).abs <= tolerance_seconds
|
|
17
|
+
#
|
|
18
|
+
# Discriminated-union return so callers branch on `result.ok?`:
|
|
19
|
+
#
|
|
20
|
+
# result = Blockchain0x::Webhooks.verify(
|
|
21
|
+
# headers: request.headers, raw_body: request.raw_post,
|
|
22
|
+
# secret: ENV['BLOCKCHAIN0X_WEBHOOK_SECRET'],
|
|
23
|
+
# )
|
|
24
|
+
# return head(:bad_request) unless result.ok?
|
|
25
|
+
#
|
|
26
|
+
# The verifier accepts EITHER the structured `t=<ts>,v1=<hex>` value
|
|
27
|
+
# OR a bare hex signature (some load balancers strip comma-delimited
|
|
28
|
+
# values); when bare-hex, the `t` value is read from the
|
|
29
|
+
# X-Blockchain0x-Timestamp header.
|
|
30
|
+
|
|
31
|
+
# frozen_string_literal: true
|
|
32
|
+
|
|
33
|
+
require 'openssl'
|
|
34
|
+
require_relative 'errors'
|
|
35
|
+
|
|
36
|
+
module Blockchain0x
|
|
37
|
+
module Webhooks
|
|
38
|
+
SIG_HEADER = 'X-Blockchain0x-Signature'
|
|
39
|
+
TS_HEADER = 'X-Blockchain0x-Timestamp'
|
|
40
|
+
TYPE_HEADER = 'X-Blockchain0x-Event-Type'
|
|
41
|
+
EVENT_ID_HEADER = 'X-Blockchain0x-Event-Id'
|
|
42
|
+
DELIVERY_ID_HEADER = 'X-Blockchain0x-Delivery-Id'
|
|
43
|
+
|
|
44
|
+
DEFAULT_TOLERANCE_SECONDS = 300
|
|
45
|
+
|
|
46
|
+
# Failure-code constants mirror @blockchain0x/node + Python + Go.
|
|
47
|
+
SIGNATURE_MISSING = 'webhook.signature_missing'
|
|
48
|
+
SIGNATURE_MALFORMED = 'webhook.signature_malformed'
|
|
49
|
+
TIMESTAMP_OUTSIDE_WINDOW = 'webhook.timestamp_outside_window'
|
|
50
|
+
SIGNATURE_MISMATCH = 'webhook.signature_mismatch'
|
|
51
|
+
SECRET_MISSING = 'webhook.secret_missing'
|
|
52
|
+
TIMESTAMP_MISSING = 'webhook.timestamp_missing'
|
|
53
|
+
TIMESTAMP_INVALID = 'webhook.timestamp_invalid'
|
|
54
|
+
|
|
55
|
+
# Result returned by `verify`. Use `result.ok?` to branch.
|
|
56
|
+
#
|
|
57
|
+
# On success, `event_type`, `event_id`, `delivery_id` are
|
|
58
|
+
# populated (may be nil if the framework dropped the header but
|
|
59
|
+
# the signature verified).
|
|
60
|
+
#
|
|
61
|
+
# On failure, `code` is one of the constants above and the
|
|
62
|
+
# event-detail fields are nil.
|
|
63
|
+
Result = Struct.new(:ok, :code, :event_type, :event_id, :delivery_id, keyword_init: true) do
|
|
64
|
+
def ok?
|
|
65
|
+
ok == true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class << self
|
|
70
|
+
# @param headers [Hash] case-insensitive header bag (a plain
|
|
71
|
+
# Hash works, ActionController headers work)
|
|
72
|
+
# @param raw_body [String] request body EXACTLY as it arrived;
|
|
73
|
+
# do not pre-parse JSON
|
|
74
|
+
# @param secret [String] webhook signing secret
|
|
75
|
+
# @param tolerance_seconds [Integer] override the 5-minute default
|
|
76
|
+
# @param now [Integer] override `Time.now.to_i` for tests
|
|
77
|
+
# @param raise_on_fail [Boolean] when true, raise
|
|
78
|
+
# WebhookSignatureError on any failure
|
|
79
|
+
# @return [Result] discriminated-union result (default path)
|
|
80
|
+
# @raise [WebhookSignatureError] when raise_on_fail: true and verify fails
|
|
81
|
+
def verify(headers:, raw_body:, secret:, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil, raise_on_fail: false)
|
|
82
|
+
if secret.nil? || secret.empty?
|
|
83
|
+
return fail_result(SECRET_MISSING, 'missing webhook secret', raise_on_fail)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
raw_sig = pick_header(headers, SIG_HEADER)
|
|
87
|
+
if raw_sig.nil?
|
|
88
|
+
return fail_result(SIGNATURE_MISSING, 'missing X-Blockchain0x-Signature header', raise_on_fail)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
parsed = parse_signature(raw_sig)
|
|
92
|
+
return fail_result(SIGNATURE_MALFORMED, 'malformed signature header', raise_on_fail) if parsed.nil?
|
|
93
|
+
|
|
94
|
+
ts_from_header, sig_hex = parsed
|
|
95
|
+
ts = ts_from_header
|
|
96
|
+
if ts.nil?
|
|
97
|
+
raw_ts = pick_header(headers, TS_HEADER)
|
|
98
|
+
return fail_result(TIMESTAMP_MISSING, 'missing X-Blockchain0x-Timestamp header', raise_on_fail) if raw_ts.nil?
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
ts = Integer(raw_ts)
|
|
102
|
+
rescue ArgumentError, TypeError
|
|
103
|
+
return fail_result(TIMESTAMP_INVALID, 'invalid X-Blockchain0x-Timestamp value', raise_on_fail)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
current = now || Time.now.to_i
|
|
108
|
+
if (current - ts).abs > tolerance_seconds
|
|
109
|
+
return fail_result(TIMESTAMP_OUTSIDE_WINDOW, 'timestamp outside tolerance window', raise_on_fail)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
want = OpenSSL::HMAC.hexdigest('SHA256', secret, "#{ts}.#{raw_body}")
|
|
113
|
+
unless secure_compare(want, sig_hex.downcase)
|
|
114
|
+
return fail_result(SIGNATURE_MISMATCH, 'signature mismatch', raise_on_fail)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
Result.new(
|
|
118
|
+
ok: true,
|
|
119
|
+
code: nil,
|
|
120
|
+
event_type: pick_header(headers, TYPE_HEADER),
|
|
121
|
+
event_id: pick_header(headers, EVENT_ID_HEADER),
|
|
122
|
+
delivery_id: pick_header(headers, DELIVERY_ID_HEADER),
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def pick_header(headers, name)
|
|
129
|
+
return nil if headers.nil?
|
|
130
|
+
|
|
131
|
+
# Try the canonical name first (works for ActionDispatch::Headers
|
|
132
|
+
# and most middleware bags), then fall back to a case-insensitive
|
|
133
|
+
# lookup on plain Hash inputs.
|
|
134
|
+
if headers.respond_to?(:[])
|
|
135
|
+
v = headers[name]
|
|
136
|
+
return v if !v.nil? && !v.to_s.empty?
|
|
137
|
+
|
|
138
|
+
v = headers[name.downcase]
|
|
139
|
+
return v if !v.nil? && !v.to_s.empty?
|
|
140
|
+
end
|
|
141
|
+
return nil unless headers.respond_to?(:each)
|
|
142
|
+
|
|
143
|
+
target = name.downcase
|
|
144
|
+
headers.each do |k, v|
|
|
145
|
+
return v if k.to_s.downcase == target && !v.to_s.empty?
|
|
146
|
+
end
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Parses `t=<ts>,v1=<hex>` OR a bare hex string.
|
|
151
|
+
# Returns [ts_or_nil, sig_hex] on success; nil on malformed input.
|
|
152
|
+
def parse_signature(raw)
|
|
153
|
+
unless raw.include?('=')
|
|
154
|
+
cleaned = raw.strip
|
|
155
|
+
return nil unless cleaned =~ /\A[0-9a-fA-F]+\z/
|
|
156
|
+
|
|
157
|
+
return [nil, cleaned.downcase]
|
|
158
|
+
end
|
|
159
|
+
ts = nil
|
|
160
|
+
sig = nil
|
|
161
|
+
raw.split(',').each do |part|
|
|
162
|
+
key, _eq, value = part.strip.partition('=')
|
|
163
|
+
case key
|
|
164
|
+
when 't'
|
|
165
|
+
begin
|
|
166
|
+
ts = Integer(value)
|
|
167
|
+
rescue ArgumentError, TypeError
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
when 'v1'
|
|
171
|
+
sig = value.strip.downcase
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
return nil if sig.nil?
|
|
175
|
+
|
|
176
|
+
[ts, sig]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Constant-time compare. Ruby 3.0+ with a modern openssl gem
|
|
180
|
+
# exposes OpenSSL.fixed_length_secure_compare; older stdlib
|
|
181
|
+
# builds (e.g. some minimal docker images) ship without it, so
|
|
182
|
+
# fall back to a manual byte XOR-and-OR that scans the full
|
|
183
|
+
# input regardless of where the mismatch is. Either path
|
|
184
|
+
# short-circuits on a length mismatch BEFORE the constant-time
|
|
185
|
+
# compare; signatures are sized by the algorithm, not by the
|
|
186
|
+
# caller, so the length is not user-secret.
|
|
187
|
+
def secure_compare(a, b)
|
|
188
|
+
return false unless a.bytesize == b.bytesize
|
|
189
|
+
|
|
190
|
+
if OpenSSL.respond_to?(:fixed_length_secure_compare)
|
|
191
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
192
|
+
else
|
|
193
|
+
diff = 0
|
|
194
|
+
a.bytes.zip(b.bytes) { |x, y| diff |= x ^ y }
|
|
195
|
+
diff.zero?
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def fail_result(code, message, raise_on_fail)
|
|
200
|
+
raise WebhookSignatureError.new(code: code, message: message) if raise_on_fail
|
|
201
|
+
|
|
202
|
+
Result.new(ok: false, code: code)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/blockchain0x.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Sub-plan 21.3 row C-4: top-level module + autoload setup.
|
|
2
|
+
#
|
|
3
|
+
# Public surface:
|
|
4
|
+
# Blockchain0x::Client HTTP client (client.api_keys.*)
|
|
5
|
+
# Blockchain0x::Webhooks webhook signature verifier
|
|
6
|
+
# Blockchain0x::Error base exception class
|
|
7
|
+
# Blockchain0x::APIKeyError 401/403 apikey.* envelope subclass
|
|
8
|
+
# Blockchain0x::APIKeyCodes 13 stable apikey.* failure codes (sub-plan 21.3 C-8)
|
|
9
|
+
# Blockchain0x::WebhookSignatureError raised by Webhooks.verify(raise_on_fail: true)
|
|
10
|
+
# Blockchain0x::Resources::WalletAssignment typed per-wallet entry for create_workspace_key
|
|
11
|
+
# Blockchain0x::Resources::PaymentRequestSettleBody typed proof tuple for payment_requests.settle
|
|
12
|
+
# Blockchain0x::Resources::PaymentCreateBody typed body for payments.create
|
|
13
|
+
# Blockchain0x::VERSION gem version string
|
|
14
|
+
|
|
15
|
+
# frozen_string_literal: true
|
|
16
|
+
|
|
17
|
+
require_relative 'blockchain0x/version'
|
|
18
|
+
require_relative 'blockchain0x/errors'
|
|
19
|
+
require_relative 'blockchain0x/webhooks'
|
|
20
|
+
# Client is required lazily so consumers who only use Webhooks do not
|
|
21
|
+
# pay the Faraday load cost.
|
|
22
|
+
|
|
23
|
+
module Blockchain0x
|
|
24
|
+
# @return [Class] Blockchain0x::Client (lazy-loaded)
|
|
25
|
+
def self.const_missing(name)
|
|
26
|
+
if name == :Client
|
|
27
|
+
require_relative 'blockchain0x/client'
|
|
28
|
+
const_get(:Client)
|
|
29
|
+
else
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: blockchain0x
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1.alpha.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Blockchain0x
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: faraday
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rspec
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.13'
|
|
40
|
+
type: :development
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: webmock
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.23'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.23'
|
|
61
|
+
description: Non-custodial AI-agent wallet platform on Base - authenticate, read balances
|
|
62
|
+
+ transactions, send + receive payments, and verify webhook signatures.
|
|
63
|
+
email:
|
|
64
|
+
- support@blockchain0x.com
|
|
65
|
+
executables: []
|
|
66
|
+
extensions: []
|
|
67
|
+
extra_rdoc_files: []
|
|
68
|
+
files:
|
|
69
|
+
- LICENSE
|
|
70
|
+
- README.md
|
|
71
|
+
- lib/blockchain0x.rb
|
|
72
|
+
- lib/blockchain0x/client.rb
|
|
73
|
+
- lib/blockchain0x/errors.rb
|
|
74
|
+
- lib/blockchain0x/resources/api_keys.rb
|
|
75
|
+
- lib/blockchain0x/resources/payment_requests.rb
|
|
76
|
+
- lib/blockchain0x/resources/payments.rb
|
|
77
|
+
- lib/blockchain0x/resources/transactions.rb
|
|
78
|
+
- lib/blockchain0x/version.rb
|
|
79
|
+
- lib/blockchain0x/webhooks.rb
|
|
80
|
+
homepage: https://blockchain0x.com
|
|
81
|
+
licenses:
|
|
82
|
+
- Apache-2.0
|
|
83
|
+
metadata:
|
|
84
|
+
homepage_uri: https://blockchain0x.com
|
|
85
|
+
source_code_uri: https://github.com/Tosh-Labs/blockchain0x-app/tree/dev/packages/sdk-ruby
|
|
86
|
+
bug_tracker_uri: https://github.com/Tosh-Labs/blockchain0x-app/issues
|
|
87
|
+
documentation_uri: https://docs.blockchain0x.com
|
|
88
|
+
changelog_uri: https://github.com/Tosh-Labs/blockchain0x-ruby/blob/main/CHANGELOG.md
|
|
89
|
+
rubygems_mfa_required: 'true'
|
|
90
|
+
post_install_message:
|
|
91
|
+
rdoc_options: []
|
|
92
|
+
require_paths:
|
|
93
|
+
- lib
|
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '3.0'
|
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
requirements: []
|
|
105
|
+
rubygems_version: 3.5.22
|
|
106
|
+
signing_key:
|
|
107
|
+
specification_version: 4
|
|
108
|
+
summary: Official Ruby SDK for Blockchain0x
|
|
109
|
+
test_files: []
|