attago 0.1.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/README.md +252 -0
- data/lib/attago/agent.rb +22 -0
- data/lib/attago/api_keys.rb +29 -0
- data/lib/attago/auth.rb +170 -0
- data/lib/attago/bundles.rb +22 -0
- data/lib/attago/client.rb +227 -0
- data/lib/attago/data.rb +29 -0
- data/lib/attago/errors.rb +76 -0
- data/lib/attago/listener.rb +124 -0
- data/lib/attago/mcp.rb +71 -0
- data/lib/attago/payments.rb +28 -0
- data/lib/attago/push.rb +33 -0
- data/lib/attago/redeem.rb +15 -0
- data/lib/attago/subscriptions.rb +62 -0
- data/lib/attago/types.rb +671 -0
- data/lib/attago/version.rb +5 -0
- data/lib/attago/wallets.rb +35 -0
- data/lib/attago/webhooks.rb +137 -0
- data/lib/attago/x402.rb +74 -0
- data/lib/attago.rb +23 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 90446f07e1eb80c83339dfe22a11073a29d8407d8dd1339b602fa9aeb6cd53c3
|
|
4
|
+
data.tar.gz: aa45cf4c90dca3fbeb0be2816c5d6fd733f40c9d25ce428800c29fa9cb728d09
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5383151b402e020df0f63e70334e7e40d1e5e689edfbfbed9c245794d394d839bb9fdf9f8472795432943fcf5e5c2a6f9fdcc1adcc3258011f4be9d6bd29d901
|
|
7
|
+
data.tar.gz: 839aee72e606fdc9547eb040b111b027ace186636407b765f5d43656d8b31d1a716856da2cfab31ff5c47c09a1e4943fead6874e3ad069ecb9cebe65127b2906
|
data/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# attago
|
|
2
|
+
|
|
3
|
+
[](https://github.com/AttaGo/attago-rb-sdk/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/attago)
|
|
5
|
+
|
|
6
|
+
Ruby SDK for the [AttaGo](https://attago.bid) crypto trading dashboard API.
|
|
7
|
+
|
|
8
|
+
Go/No-Go crypto trading signals, alert subscriptions, x402 payments, webhook
|
|
9
|
+
HMAC verification, and MCP JSON-RPC 2.0 -- **zero runtime dependencies**
|
|
10
|
+
(stdlib only).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "attago"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install attago
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires **Ruby 3.1+**.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### API Key (agent endpoints)
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "attago"
|
|
33
|
+
|
|
34
|
+
client = Attago::Client.new(api_key: "ak_live_...")
|
|
35
|
+
|
|
36
|
+
score = client.agent.get_score("BTC")
|
|
37
|
+
puts score.composite.signal # "GO", "NO-GO", or "NEUTRAL"
|
|
38
|
+
puts score.composite.score # 0-100
|
|
39
|
+
puts score.composite.confidence # 0.0-1.0
|
|
40
|
+
|
|
41
|
+
client.close
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### x402 Signer (pay-per-request)
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
client = Attago::Client.new(signer: my_signer)
|
|
48
|
+
|
|
49
|
+
# x402 payment handled transparently
|
|
50
|
+
score = client.agent.get_score("ETH")
|
|
51
|
+
data = client.agent.get_data("BTC", "ETH", "SOL")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Cognito (account management)
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
client = Attago::Client.new(
|
|
58
|
+
email: "user@example.com",
|
|
59
|
+
password: "...",
|
|
60
|
+
cognito_client_id: "abc123"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
subs = client.subscriptions.list
|
|
64
|
+
status = client.payments.status
|
|
65
|
+
|
|
66
|
+
client.close
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Auth Modes
|
|
70
|
+
|
|
71
|
+
Exactly one auth mode per client. Mix-and-match raises `ConfigError`.
|
|
72
|
+
|
|
73
|
+
| Mode | Use Case | Endpoints |
|
|
74
|
+
|------|----------|-----------|
|
|
75
|
+
| `api_key:` | Scripts, bots, CI | Agent (score, data) |
|
|
76
|
+
| `signer:` | Pay-per-request (x402) | Agent (score, data) |
|
|
77
|
+
| `email: + password:` | Account management | Subscriptions, wallets, payments, webhooks, API keys, bundles, push, redeem |
|
|
78
|
+
|
|
79
|
+
No auth is also valid -- public data endpoints only.
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
82
|
+
|
|
83
|
+
| Service | Methods |
|
|
84
|
+
|---------|---------|
|
|
85
|
+
| `client.agent` | `get_score(symbol)`, `get_data(*symbols)` |
|
|
86
|
+
| `client.data` | `get_latest`, `get_token_data(token)`, `get_data_push(id)` |
|
|
87
|
+
| `client.subscriptions` | `catalog`, `list`, `create(input)`, `update(id, input)`, `delete(id)` |
|
|
88
|
+
| `client.payments` | `subscribe(input)`, `status`, `upgrade_quote(tier, cycle)` |
|
|
89
|
+
| `client.wallets` | `register(input)`, `list`, `remove(address)` |
|
|
90
|
+
| `client.webhooks` | `create(url)`, `list`, `delete(id)`, `send_test(opts)`, `send_server_test(id)` |
|
|
91
|
+
| `client.mcp` | `initialize_session`, `list_tools`, `call_tool(name, args)`, `ping` |
|
|
92
|
+
| `client.api_keys` | `create(name)`, `list`, `revoke(id)` |
|
|
93
|
+
| `client.bundles` | `list`, `purchase(input)` |
|
|
94
|
+
| `client.push` | `list`, `create(input)`, `delete(id)` |
|
|
95
|
+
| `client.redeem` | `redeem(code)` |
|
|
96
|
+
|
|
97
|
+
## Typed Inputs
|
|
98
|
+
|
|
99
|
+
Service methods that accept structured input use typed structs:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# Create a subscription
|
|
103
|
+
input = Attago::SubscriptionCreateInput.new(
|
|
104
|
+
token_id: "BTC",
|
|
105
|
+
label: "BTC Price Alert",
|
|
106
|
+
groups: [
|
|
107
|
+
[Attago::Condition.new(
|
|
108
|
+
metric_name: "spotPrice",
|
|
109
|
+
threshold_op: "gte",
|
|
110
|
+
threshold_val: 100_000
|
|
111
|
+
)]
|
|
112
|
+
],
|
|
113
|
+
cooldown_minutes: 60
|
|
114
|
+
)
|
|
115
|
+
sub = client.subscriptions.create(input)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Register a wallet
|
|
120
|
+
input = Attago::WalletRegisterInput.new(
|
|
121
|
+
wallet_address: "0x...",
|
|
122
|
+
chain: "base",
|
|
123
|
+
signature: "0x...",
|
|
124
|
+
timestamp: Time.now.utc.iso8601
|
|
125
|
+
)
|
|
126
|
+
wallet = client.wallets.register(input)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Webhook Verification
|
|
130
|
+
|
|
131
|
+
Verify incoming webhook signatures without a full client:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
body = request.body.read
|
|
135
|
+
signature = request.headers["X-AttaGo-Signature"]
|
|
136
|
+
secret = "whsec_..."
|
|
137
|
+
|
|
138
|
+
if Attago::Webhooks.verify_signature(body, secret, signature)
|
|
139
|
+
payload = JSON.parse(body)
|
|
140
|
+
# Handle webhook
|
|
141
|
+
else
|
|
142
|
+
# Invalid signature -- reject
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Webhook Listener
|
|
147
|
+
|
|
148
|
+
Standalone HTTP server for receiving webhooks in development or background
|
|
149
|
+
workers:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
require "attago"
|
|
153
|
+
|
|
154
|
+
listener = Attago::WebhookListener.new(secret: "whsec_...", port: 4000)
|
|
155
|
+
|
|
156
|
+
listener.on_alert do |payload|
|
|
157
|
+
puts "#{payload.alert.token}: #{payload.alert.state}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
listener.on_test do |payload|
|
|
161
|
+
puts "Test webhook received"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
listener.on_error do |err|
|
|
165
|
+
warn "Webhook error: #{err.message}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
listener.start # Runs in a background thread
|
|
169
|
+
# ... do other work ...
|
|
170
|
+
listener.stop
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## MCP (Model Context Protocol)
|
|
174
|
+
|
|
175
|
+
JSON-RPC 2.0 over HTTP for AI agent integration:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
client = Attago::Client.new(api_key: "ak_live_...")
|
|
179
|
+
|
|
180
|
+
info = client.mcp.initialize_session
|
|
181
|
+
tools = client.mcp.list_tools
|
|
182
|
+
|
|
183
|
+
result = client.mcp.call_tool("get_score", { "symbol" => "BTC" })
|
|
184
|
+
puts result.content.first.text
|
|
185
|
+
|
|
186
|
+
client.mcp.ping
|
|
187
|
+
|
|
188
|
+
client.close
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Error Handling
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
begin
|
|
195
|
+
score = client.agent.get_score("BTC")
|
|
196
|
+
rescue Attago::PaymentRequiredError => e
|
|
197
|
+
puts "Payment required: #{e.message}"
|
|
198
|
+
puts "Requirements: #{e.payment_requirements}"
|
|
199
|
+
rescue Attago::RateLimitError => e
|
|
200
|
+
puts "Rate limited, retry after: #{e.retry_after}s"
|
|
201
|
+
rescue Attago::ApiError => e
|
|
202
|
+
puts "API error #{e.status_code}: #{e.message}"
|
|
203
|
+
rescue Attago::McpError => e
|
|
204
|
+
puts "MCP error #{e.mcp_code}: #{e.mcp_message}"
|
|
205
|
+
rescue Attago::AuthError => e
|
|
206
|
+
puts "Auth error: #{e.message}"
|
|
207
|
+
rescue Attago::MfaRequiredError => e
|
|
208
|
+
puts "MFA required: #{e.challenge_name}"
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Configuration
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
client = Attago::Client.new(
|
|
216
|
+
api_key: "ak_live_...",
|
|
217
|
+
base_url: "https://staging.attago.io", # Default: https://api.attago.bid
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Testing
|
|
222
|
+
|
|
223
|
+
The SDK ships with a `MockTransport` for unit testing your integrations:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
require "attago"
|
|
227
|
+
|
|
228
|
+
transport = Attago::Testing::MockTransport.new do |req|
|
|
229
|
+
Attago::Testing::MockResponse.new(
|
|
230
|
+
code: 200,
|
|
231
|
+
body: { "token" => "BTC", "composite" => { "score" => 75, "signal" => "GO" } }
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
client = Attago::Client.new(api_key: "test", transport: transport)
|
|
236
|
+
score = client.agent.get_score("BTC")
|
|
237
|
+
assert_equal "GO", score.composite.signal
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Development
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
git clone git@github.com:AttaGo/attago-rb-sdk.git
|
|
244
|
+
cd attago-rb-sdk
|
|
245
|
+
bundle install
|
|
246
|
+
|
|
247
|
+
# Run unit tests
|
|
248
|
+
bundle exec rake test
|
|
249
|
+
|
|
250
|
+
# Run conformance tests (requires live API)
|
|
251
|
+
ATTAGO_BASE_URL=https://api.attago.bid ATTAGO_API_KEY=ak_... bundle exec rake conformance
|
|
252
|
+
```
|
data/lib/attago/agent.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Attago
|
|
4
|
+
class AgentService
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# GET /agent/score?symbol=SYM
|
|
10
|
+
def get_score(symbol)
|
|
11
|
+
data = @client.request("GET", "/agent/score", params: { "symbol" => symbol })
|
|
12
|
+
AgentScoreResponse.from_hash(data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# GET /agent/data?symbols=SYM1,SYM2 (omit for all)
|
|
16
|
+
def get_data(*symbols)
|
|
17
|
+
params = symbols.empty? ? {} : { "symbols" => symbols.join(",") }
|
|
18
|
+
data = @client.request("GET", "/agent/data", params: params)
|
|
19
|
+
AgentDataResponse.from_hash(data)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Attago
|
|
6
|
+
class ApiKeyService
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# POST /api-keys
|
|
12
|
+
def create(name)
|
|
13
|
+
data = @client.request("POST", "/user/api-keys", body: { "name" => name })
|
|
14
|
+
ApiKeyCreateResponse.from_hash(data)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# GET /api-keys
|
|
18
|
+
def list
|
|
19
|
+
data = @client.request("GET", "/user/api-keys")
|
|
20
|
+
(data["keys"] || []).map { |k| ApiKeyListItem.from_hash(k) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# DELETE /api-keys/{key_id}
|
|
24
|
+
def revoke(key_id)
|
|
25
|
+
@client.request("DELETE", "/user/api-keys/#{URI.encode_www_form_component(key_id)}")
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/attago/auth.rb
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Attago
|
|
8
|
+
class CognitoAuth
|
|
9
|
+
# Allow injection of an HTTP client for testing.
|
|
10
|
+
attr_writer :http_client
|
|
11
|
+
|
|
12
|
+
def initialize(client_id:, region: DEFAULT_COGNITO_REGION, email: nil, password: nil)
|
|
13
|
+
@client_id = client_id
|
|
14
|
+
@region = region
|
|
15
|
+
@email = email
|
|
16
|
+
@password = password
|
|
17
|
+
@tokens = nil
|
|
18
|
+
@mu = Mutex.new # Thread safety (GO-C1 audit lesson)
|
|
19
|
+
@http_client = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sign in and return tokens. Raises MfaRequiredError if MFA challenge.
|
|
23
|
+
def sign_in
|
|
24
|
+
resp = cognito_request("AWSCognitoIdentityProviderService.InitiateAuth", {
|
|
25
|
+
"AuthFlow" => "USER_PASSWORD_AUTH",
|
|
26
|
+
"ClientId" => @client_id,
|
|
27
|
+
"AuthParameters" => {
|
|
28
|
+
"USERNAME" => @email,
|
|
29
|
+
"PASSWORD" => @password
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if resp["ChallengeName"]
|
|
34
|
+
raise MfaRequiredError.new(
|
|
35
|
+
session: resp["Session"],
|
|
36
|
+
challenge_name: resp["ChallengeName"]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
tokens = parse_auth_result(resp["AuthenticationResult"])
|
|
41
|
+
@mu.synchronize { @tokens = tokens }
|
|
42
|
+
tokens
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sign_out
|
|
46
|
+
@mu.synchronize { @tokens = nil }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns ID token, auto sign-in if needed.
|
|
50
|
+
def get_id_token
|
|
51
|
+
t = @mu.synchronize { @tokens }
|
|
52
|
+
if t.nil?
|
|
53
|
+
sign_in
|
|
54
|
+
t = @mu.synchronize { @tokens }
|
|
55
|
+
end
|
|
56
|
+
t.id_token
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def set_tokens(tokens)
|
|
60
|
+
@mu.synchronize { @tokens = tokens }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def get_tokens
|
|
64
|
+
@mu.synchronize { @tokens }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Respond to MFA challenge.
|
|
68
|
+
def respond_to_mfa(session, totp_code)
|
|
69
|
+
resp = cognito_request("AWSCognitoIdentityProviderService.RespondToAuthChallenge", {
|
|
70
|
+
"ChallengeName" => "SOFTWARE_TOKEN_MFA",
|
|
71
|
+
"ClientId" => @client_id,
|
|
72
|
+
"Session" => session,
|
|
73
|
+
"ChallengeResponses" => {
|
|
74
|
+
"USERNAME" => @email,
|
|
75
|
+
"SOFTWARE_TOKEN_MFA_CODE" => totp_code
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
tokens = parse_auth_result(resp["AuthenticationResult"])
|
|
80
|
+
@mu.synchronize { @tokens = tokens }
|
|
81
|
+
tokens
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Class-level methods (no instance needed)
|
|
85
|
+
def self.sign_up(email:, password:, client_id:, region: DEFAULT_COGNITO_REGION, http_client: nil)
|
|
86
|
+
auth = new(client_id: client_id, region: region)
|
|
87
|
+
auth.http_client = http_client if http_client
|
|
88
|
+
auth.send(:cognito_request, "AWSCognitoIdentityProviderService.SignUp", {
|
|
89
|
+
"ClientId" => client_id,
|
|
90
|
+
"Username" => email,
|
|
91
|
+
"Password" => password
|
|
92
|
+
})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.confirm_sign_up(email:, code:, client_id:, region: DEFAULT_COGNITO_REGION, http_client: nil)
|
|
96
|
+
auth = new(client_id: client_id, region: region)
|
|
97
|
+
auth.http_client = http_client if http_client
|
|
98
|
+
auth.send(:cognito_request, "AWSCognitoIdentityProviderService.ConfirmSignUp", {
|
|
99
|
+
"ClientId" => client_id,
|
|
100
|
+
"Username" => email,
|
|
101
|
+
"ConfirmationCode" => code
|
|
102
|
+
})
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.forgot_password(email:, client_id:, region: DEFAULT_COGNITO_REGION, http_client: nil)
|
|
106
|
+
auth = new(client_id: client_id, region: region)
|
|
107
|
+
auth.http_client = http_client if http_client
|
|
108
|
+
auth.send(:cognito_request, "AWSCognitoIdentityProviderService.ForgotPassword", {
|
|
109
|
+
"ClientId" => client_id,
|
|
110
|
+
"Username" => email
|
|
111
|
+
})
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.confirm_forgot_password(email:, code:, new_password:, client_id:, region: DEFAULT_COGNITO_REGION, http_client: nil)
|
|
115
|
+
auth = new(client_id: client_id, region: region)
|
|
116
|
+
auth.http_client = http_client if http_client
|
|
117
|
+
auth.send(:cognito_request, "AWSCognitoIdentityProviderService.ConfirmForgotPassword", {
|
|
118
|
+
"ClientId" => client_id,
|
|
119
|
+
"Username" => email,
|
|
120
|
+
"ConfirmationCode" => code,
|
|
121
|
+
"Password" => new_password
|
|
122
|
+
})
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def parse_auth_result(result)
|
|
128
|
+
CognitoTokens.new(
|
|
129
|
+
id_token: result["IdToken"],
|
|
130
|
+
access_token: result["AccessToken"],
|
|
131
|
+
refresh_token: result["RefreshToken"]
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def cognito_request(target, body)
|
|
136
|
+
if @http_client
|
|
137
|
+
# Use injected HTTP client for testing
|
|
138
|
+
resp = @http_client.call(target, body)
|
|
139
|
+
else
|
|
140
|
+
uri = URI.parse("https://cognito-idp.#{@region}.amazonaws.com/")
|
|
141
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
142
|
+
http.use_ssl = true
|
|
143
|
+
http.open_timeout = 10
|
|
144
|
+
http.read_timeout = 15
|
|
145
|
+
|
|
146
|
+
req = Net::HTTP::Post.new("/")
|
|
147
|
+
req["Content-Type"] = "application/x-amz-json-1.1"
|
|
148
|
+
req["X-Amz-Target"] = target
|
|
149
|
+
req.body = JSON.generate(body)
|
|
150
|
+
|
|
151
|
+
resp = http.request(req)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check HTTP status BEFORE JSON decode (GO audit I3 lesson)
|
|
155
|
+
unless resp.code.to_i == 200
|
|
156
|
+
begin
|
|
157
|
+
err_body = JSON.parse(resp.body || "")
|
|
158
|
+
err_type = err_body["__type"]&.split("#")&.last || "UnknownError"
|
|
159
|
+
err_msg = err_body["message"] || err_body["Message"] || "Authentication failed"
|
|
160
|
+
rescue JSON::ParserError
|
|
161
|
+
err_type = "UnknownError"
|
|
162
|
+
err_msg = "HTTP #{resp.code}"
|
|
163
|
+
end
|
|
164
|
+
raise AuthError.new(err_msg, code: err_type)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
JSON.parse(resp.body)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Attago
|
|
4
|
+
class BundleService
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# GET /api/bundles
|
|
10
|
+
def list
|
|
11
|
+
data = @client.request("GET", "/api/bundles")
|
|
12
|
+
BundleListResponse.from_hash(data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# POST /api/bundles
|
|
16
|
+
def purchase(input)
|
|
17
|
+
body = { "bundleIndex" => input.bundle_index, "walletAddress" => input.wallet_address }
|
|
18
|
+
data = @client.request("POST", "/api/bundles", body: body)
|
|
19
|
+
BundlePurchaseResponse.from_hash(data)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|