weft-sdk 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +9 -0
- data/docs/APIKeysApi.md +208 -0
- data/docs/AccountApi.md +72 -0
- data/docs/AccountDetails.md +30 -0
- data/docs/Agent.md +38 -0
- data/docs/AgentListResponse.md +20 -0
- data/docs/AgentResponse.md +18 -0
- data/docs/AgentStats.md +30 -0
- data/docs/AgentsApi.md +147 -0
- data/docs/ApiKey.md +28 -0
- data/docs/ApiKeyCreated.md +26 -0
- data/docs/ApiKeyCreatedResponse.md +18 -0
- data/docs/ApiKeyListResponse.md +18 -0
- data/docs/AuthApi.md +385 -0
- data/docs/AuthResponse.md +18 -0
- data/docs/AuthResponseData.md +22 -0
- data/docs/ConfirmRequest.md +18 -0
- data/docs/CreateApiKeyRequest.md +18 -0
- data/docs/DefaultApi.md +67 -0
- data/docs/Error.md +24 -0
- data/docs/ErrorResponse.md +18 -0
- data/docs/MeResponse.md +18 -0
- data/docs/MessageResponse.md +18 -0
- data/docs/MessageResponseData.md +18 -0
- data/docs/Pagination.md +24 -0
- data/docs/PasswordResetRequest.md +18 -0
- data/docs/PasswordUpdateRequest.md +22 -0
- data/docs/Payment.md +44 -0
- data/docs/PaymentListResponse.md +20 -0
- data/docs/PaymentResponse.md +18 -0
- data/docs/PaymentsApi.md +147 -0
- data/docs/ResendConfirmationRequest.md +18 -0
- data/docs/SignInRequest.md +20 -0
- data/docs/SignUpRequest.md +22 -0
- data/docs/User.md +22 -0
- data/lib/weft/facilitator/client.rb +89 -0
- data/lib/weft/facilitator/fee.rb +47 -0
- data/lib/weft/facilitator/middleware.rb +190 -0
- data/lib/weft/generated/api/account_api.rb +77 -0
- data/lib/weft/generated/api/agents_api.rb +148 -0
- data/lib/weft/generated/api/api_keys_api.rb +204 -0
- data/lib/weft/generated/api/auth_api.rb +418 -0
- data/lib/weft/generated/api/default_api.rb +77 -0
- data/lib/weft/generated/api/payments_api.rb +148 -0
- data/lib/weft/generated/api_client.rb +397 -0
- data/lib/weft/generated/api_error.rb +58 -0
- data/lib/weft/generated/api_model_base.rb +88 -0
- data/lib/weft/generated/configuration.rb +317 -0
- data/lib/weft/generated/models/account_details.rb +310 -0
- data/lib/weft/generated/models/agent.rb +417 -0
- data/lib/weft/generated/models/agent_list_response.rb +192 -0
- data/lib/weft/generated/models/agent_response.rb +164 -0
- data/lib/weft/generated/models/agent_stats.rb +201 -0
- data/lib/weft/generated/models/api_key.rb +244 -0
- data/lib/weft/generated/models/api_key_created.rb +252 -0
- data/lib/weft/generated/models/api_key_created_response.rb +164 -0
- data/lib/weft/generated/models/api_key_list_response.rb +166 -0
- data/lib/weft/generated/models/auth_response.rb +164 -0
- data/lib/weft/generated/models/auth_response_data.rb +199 -0
- data/lib/weft/generated/models/confirm_request.rb +164 -0
- data/lib/weft/generated/models/create_api_key_request.rb +148 -0
- data/lib/weft/generated/models/error.rb +208 -0
- data/lib/weft/generated/models/error_response.rb +164 -0
- data/lib/weft/generated/models/me_response.rb +164 -0
- data/lib/weft/generated/models/message_response.rb +164 -0
- data/lib/weft/generated/models/message_response_data.rb +164 -0
- data/lib/weft/generated/models/pagination.rb +242 -0
- data/lib/weft/generated/models/password_reset_request.rb +164 -0
- data/lib/weft/generated/models/password_update_request.rb +216 -0
- data/lib/weft/generated/models/payment.rb +437 -0
- data/lib/weft/generated/models/payment_list_response.rb +192 -0
- data/lib/weft/generated/models/payment_response.rb +164 -0
- data/lib/weft/generated/models/resend_confirmation_request.rb +164 -0
- data/lib/weft/generated/models/sign_in_request.rb +190 -0
- data/lib/weft/generated/models/sign_up_request.rb +216 -0
- data/lib/weft/generated/models/user.rb +199 -0
- data/lib/weft/generated/version.rb +15 -0
- data/lib/weft/generated.rb +4 -0
- data/lib/weft/sdk.rb +10 -0
- metadata +123 -0
data/docs/Payment.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Weft::Payment
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **id** | **Integer** | | |
|
|
8
|
+
| **tx_hash** | **String** | | |
|
|
9
|
+
| **payer_address** | **String** | | |
|
|
10
|
+
| **recipient_address** | **String** | | |
|
|
11
|
+
| **amount** | **Integer** | Amount in atomic units (1 USDC = 1,000,000) | |
|
|
12
|
+
| **amount_formatted** | **String** | Human-readable amount (e.g. \"1.00 USDC\") | |
|
|
13
|
+
| **currency** | **String** | | |
|
|
14
|
+
| **network** | **String** | CAIP-2 chain identifier | |
|
|
15
|
+
| **resource_url** | **String** | | [optional] |
|
|
16
|
+
| **resource_host** | **String** | | [optional] |
|
|
17
|
+
| **fee_amount** | **Integer** | | [optional] |
|
|
18
|
+
| **settlement_latency_ms** | **Integer** | | [optional] |
|
|
19
|
+
| **settled_at** | **Time** | | |
|
|
20
|
+
| **api_key_name** | **String** | | |
|
|
21
|
+
|
|
22
|
+
## Example
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require 'weft-sdk'
|
|
26
|
+
|
|
27
|
+
instance = Weft::Payment.new(
|
|
28
|
+
id: null,
|
|
29
|
+
tx_hash: null,
|
|
30
|
+
payer_address: null,
|
|
31
|
+
recipient_address: null,
|
|
32
|
+
amount: null,
|
|
33
|
+
amount_formatted: null,
|
|
34
|
+
currency: null,
|
|
35
|
+
network: null,
|
|
36
|
+
resource_url: null,
|
|
37
|
+
resource_host: null,
|
|
38
|
+
fee_amount: null,
|
|
39
|
+
settlement_latency_ms: null,
|
|
40
|
+
settled_at: null,
|
|
41
|
+
api_key_name: null
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Weft::PaymentListResponse
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **data** | [**Array<Payment>**](Payment.md) | | |
|
|
8
|
+
| **pagination** | [**Pagination**](Pagination.md) | | |
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
require 'weft-sdk'
|
|
14
|
+
|
|
15
|
+
instance = Weft::PaymentListResponse.new(
|
|
16
|
+
data: null,
|
|
17
|
+
pagination: null
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Weft::PaymentResponse
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **data** | [**Payment**](Payment.md) | | |
|
|
8
|
+
|
|
9
|
+
## Example
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require 'weft-sdk'
|
|
13
|
+
|
|
14
|
+
instance = Weft::PaymentResponse.new(
|
|
15
|
+
data: null
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
data/docs/PaymentsApi.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Weft::PaymentsApi
|
|
2
|
+
|
|
3
|
+
All URIs are relative to *https://api.weftlabs.com*
|
|
4
|
+
|
|
5
|
+
| Method | HTTP request | Description |
|
|
6
|
+
| ------ | ------------ | ----------- |
|
|
7
|
+
| [**get_payment**](PaymentsApi.md#get_payment) | **GET** /api/v1/payments/{id} | Get payment details |
|
|
8
|
+
| [**list_payments**](PaymentsApi.md#list_payments) | **GET** /api/v1/payments | List payments |
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## get_payment
|
|
12
|
+
|
|
13
|
+
> <PaymentResponse> get_payment(id)
|
|
14
|
+
|
|
15
|
+
Get payment details
|
|
16
|
+
|
|
17
|
+
### Examples
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'time'
|
|
21
|
+
require 'weft-sdk'
|
|
22
|
+
# setup authorization
|
|
23
|
+
Weft.configure do |config|
|
|
24
|
+
# Configure Bearer authorization (APIKey): bearerAuth
|
|
25
|
+
config.access_token = 'YOUR_BEARER_TOKEN'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
api_instance = Weft::PaymentsApi.new
|
|
29
|
+
id = 56 # Integer | Payment ID
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
# Get payment details
|
|
33
|
+
result = api_instance.get_payment(id)
|
|
34
|
+
p result
|
|
35
|
+
rescue Weft::ApiError => e
|
|
36
|
+
puts "Error when calling PaymentsApi->get_payment: #{e}"
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### Using the get_payment_with_http_info variant
|
|
41
|
+
|
|
42
|
+
This returns an Array which contains the response data, status code and headers.
|
|
43
|
+
|
|
44
|
+
> <Array(<PaymentResponse>, Integer, Hash)> get_payment_with_http_info(id)
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
begin
|
|
48
|
+
# Get payment details
|
|
49
|
+
data, status_code, headers = api_instance.get_payment_with_http_info(id)
|
|
50
|
+
p status_code # => 2xx
|
|
51
|
+
p headers # => { ... }
|
|
52
|
+
p data # => <PaymentResponse>
|
|
53
|
+
rescue Weft::ApiError => e
|
|
54
|
+
puts "Error when calling PaymentsApi->get_payment_with_http_info: #{e}"
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Parameters
|
|
59
|
+
|
|
60
|
+
| Name | Type | Description | Notes |
|
|
61
|
+
| ---- | ---- | ----------- | ----- |
|
|
62
|
+
| **id** | **Integer** | Payment ID | |
|
|
63
|
+
|
|
64
|
+
### Return type
|
|
65
|
+
|
|
66
|
+
[**PaymentResponse**](PaymentResponse.md)
|
|
67
|
+
|
|
68
|
+
### Authorization
|
|
69
|
+
|
|
70
|
+
[bearerAuth](../README.md#bearerAuth)
|
|
71
|
+
|
|
72
|
+
### HTTP request headers
|
|
73
|
+
|
|
74
|
+
- **Content-Type**: Not defined
|
|
75
|
+
- **Accept**: application/json
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## list_payments
|
|
79
|
+
|
|
80
|
+
> <PaymentListResponse> list_payments(opts)
|
|
81
|
+
|
|
82
|
+
List payments
|
|
83
|
+
|
|
84
|
+
### Examples
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require 'time'
|
|
88
|
+
require 'weft-sdk'
|
|
89
|
+
# setup authorization
|
|
90
|
+
Weft.configure do |config|
|
|
91
|
+
# Configure Bearer authorization (APIKey): bearerAuth
|
|
92
|
+
config.access_token = 'YOUR_BEARER_TOKEN'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
api_instance = Weft::PaymentsApi.new
|
|
96
|
+
opts = {
|
|
97
|
+
page: 56, # Integer | Page number
|
|
98
|
+
per_page: 56 # Integer | Items per page
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
# List payments
|
|
103
|
+
result = api_instance.list_payments(opts)
|
|
104
|
+
p result
|
|
105
|
+
rescue Weft::ApiError => e
|
|
106
|
+
puts "Error when calling PaymentsApi->list_payments: #{e}"
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Using the list_payments_with_http_info variant
|
|
111
|
+
|
|
112
|
+
This returns an Array which contains the response data, status code and headers.
|
|
113
|
+
|
|
114
|
+
> <Array(<PaymentListResponse>, Integer, Hash)> list_payments_with_http_info(opts)
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
begin
|
|
118
|
+
# List payments
|
|
119
|
+
data, status_code, headers = api_instance.list_payments_with_http_info(opts)
|
|
120
|
+
p status_code # => 2xx
|
|
121
|
+
p headers # => { ... }
|
|
122
|
+
p data # => <PaymentListResponse>
|
|
123
|
+
rescue Weft::ApiError => e
|
|
124
|
+
puts "Error when calling PaymentsApi->list_payments_with_http_info: #{e}"
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Parameters
|
|
129
|
+
|
|
130
|
+
| Name | Type | Description | Notes |
|
|
131
|
+
| ---- | ---- | ----------- | ----- |
|
|
132
|
+
| **page** | **Integer** | Page number | [optional][default to 1] |
|
|
133
|
+
| **per_page** | **Integer** | Items per page | [optional][default to 25] |
|
|
134
|
+
|
|
135
|
+
### Return type
|
|
136
|
+
|
|
137
|
+
[**PaymentListResponse**](PaymentListResponse.md)
|
|
138
|
+
|
|
139
|
+
### Authorization
|
|
140
|
+
|
|
141
|
+
[bearerAuth](../README.md#bearerAuth)
|
|
142
|
+
|
|
143
|
+
### HTTP request headers
|
|
144
|
+
|
|
145
|
+
- **Content-Type**: Not defined
|
|
146
|
+
- **Accept**: application/json
|
|
147
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Weft::ResendConfirmationRequest
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **email** | **String** | | |
|
|
8
|
+
|
|
9
|
+
## Example
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require 'weft-sdk'
|
|
13
|
+
|
|
14
|
+
instance = Weft::ResendConfirmationRequest.new(
|
|
15
|
+
email: null
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Weft::SignInRequest
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **email** | **String** | | |
|
|
8
|
+
| **password** | **String** | | |
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
require 'weft-sdk'
|
|
14
|
+
|
|
15
|
+
instance = Weft::SignInRequest.new(
|
|
16
|
+
email: null,
|
|
17
|
+
password: null
|
|
18
|
+
)
|
|
19
|
+
```
|
|
20
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Weft::SignUpRequest
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **email** | **String** | | |
|
|
8
|
+
| **password** | **String** | | |
|
|
9
|
+
| **password_confirmation** | **String** | | |
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'weft-sdk'
|
|
15
|
+
|
|
16
|
+
instance = Weft::SignUpRequest.new(
|
|
17
|
+
email: null,
|
|
18
|
+
password: null,
|
|
19
|
+
password_confirmation: null
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
data/docs/User.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Weft::User
|
|
2
|
+
|
|
3
|
+
## Properties
|
|
4
|
+
|
|
5
|
+
| Name | Type | Description | Notes |
|
|
6
|
+
| ---- | ---- | ----------- | ----- |
|
|
7
|
+
| **id** | **Integer** | | |
|
|
8
|
+
| **email** | **String** | | |
|
|
9
|
+
| **status** | **String** | | [optional] |
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require 'weft-sdk'
|
|
15
|
+
|
|
16
|
+
instance = Weft::User.new(
|
|
17
|
+
id: null,
|
|
18
|
+
email: null,
|
|
19
|
+
status: null
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module Weft
|
|
5
|
+
module Facilitator
|
|
6
|
+
DEFAULT_URL = 'https://x402.weft.network'.freeze
|
|
7
|
+
DEFAULT_ENV = 'X402_FACILITATOR_URL'.freeze
|
|
8
|
+
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(url: nil, create_headers: nil)
|
|
11
|
+
@url = resolve_url(url)
|
|
12
|
+
validate_url(@url)
|
|
13
|
+
@create_headers = create_headers
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def verify(payment_payload:, payment_requirements:)
|
|
17
|
+
post_json('/verify', {
|
|
18
|
+
x402Version: 2,
|
|
19
|
+
paymentPayload: payment_payload,
|
|
20
|
+
paymentRequirements: payment_requirements
|
|
21
|
+
})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def settle(payment_payload:, payment_requirements:)
|
|
25
|
+
post_json('/settle', {
|
|
26
|
+
x402Version: 2,
|
|
27
|
+
paymentPayload: payment_payload,
|
|
28
|
+
paymentRequirements: payment_requirements
|
|
29
|
+
})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def supported
|
|
33
|
+
get_json('/supported')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def resolve_url(url)
|
|
39
|
+
return url if url && !url.empty?
|
|
40
|
+
|
|
41
|
+
env = ENV.fetch(DEFAULT_ENV, nil)
|
|
42
|
+
return env if env && !env.empty?
|
|
43
|
+
|
|
44
|
+
DEFAULT_URL
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_url(url)
|
|
48
|
+
return unless url.empty? || (!url.start_with?('http://') && !url.start_with?('https://'))
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "Invalid URL: #{url}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_headers(scope)
|
|
54
|
+
headers = { 'Content-Type' => 'application/json' }
|
|
55
|
+
return headers unless @create_headers
|
|
56
|
+
|
|
57
|
+
custom = @create_headers.call
|
|
58
|
+
scope_headers = custom && custom[scope]
|
|
59
|
+
headers.merge(scope_headers || {})
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def get_json(path)
|
|
63
|
+
uri = URI.join(@url, path)
|
|
64
|
+
request = Net::HTTP::Get.new(uri, build_headers('supported'))
|
|
65
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
66
|
+
http.request(request)
|
|
67
|
+
end
|
|
68
|
+
parse_response(response)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def post_json(path, body)
|
|
72
|
+
uri = URI.join(@url, path)
|
|
73
|
+
scope = path.include?('verify') ? 'verify' : 'settle'
|
|
74
|
+
request = Net::HTTP::Post.new(uri, build_headers(scope))
|
|
75
|
+
request.body = JSON.generate(body)
|
|
76
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
77
|
+
http.request(request)
|
|
78
|
+
end
|
|
79
|
+
parse_response(response)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parse_response(response)
|
|
83
|
+
raise "Facilitator request failed: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
84
|
+
|
|
85
|
+
JSON.parse(response.body)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Weft
|
|
2
|
+
module Facilitator
|
|
3
|
+
module Fee
|
|
4
|
+
DEFAULT_TTL = 300
|
|
5
|
+
|
|
6
|
+
@cache = nil
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def get_fee_info(client: Client.new, ttl: DEFAULT_TTL)
|
|
10
|
+
return @cache[:fee] if cache_valid?
|
|
11
|
+
|
|
12
|
+
supported = client.supported
|
|
13
|
+
fee = supported['fee']
|
|
14
|
+
raise 'Fee information not found in /supported response' unless fee
|
|
15
|
+
|
|
16
|
+
validate_fee!(fee)
|
|
17
|
+
|
|
18
|
+
@cache = {
|
|
19
|
+
fee: fee,
|
|
20
|
+
fetched_at: Time.now.to_i,
|
|
21
|
+
ttl: ttl
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fee
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def invalidate_fee_cache
|
|
28
|
+
@cache = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def cache_valid?
|
|
34
|
+
return false unless @cache
|
|
35
|
+
|
|
36
|
+
Time.now.to_i - @cache[:fetched_at] < @cache[:ttl]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_fee!(fee)
|
|
40
|
+
raise 'Invalid fee structure: amount must be a string' unless fee['amount'].is_a?(String)
|
|
41
|
+
raise 'Invalid fee structure: asset must be a string' unless fee['asset'].is_a?(String)
|
|
42
|
+
raise 'Invalid fee structure: network must be a string' unless fee['network'].is_a?(String)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require_relative 'client'
|
|
3
|
+
|
|
4
|
+
module Weft
|
|
5
|
+
module Facilitator
|
|
6
|
+
# Configuration for a route that requires payment.
|
|
7
|
+
#
|
|
8
|
+
# {
|
|
9
|
+
# path: "/api/resource", # string or Regexp
|
|
10
|
+
# price: "0.01", # price in asset units
|
|
11
|
+
# asset: "USDC",
|
|
12
|
+
# network: "base-sepolia",
|
|
13
|
+
# pay_to: "0x...", # recipient address
|
|
14
|
+
# description: "Access resource",
|
|
15
|
+
# max_deadline_seconds: 60,
|
|
16
|
+
# }
|
|
17
|
+
RouteConfig = Struct.new(
|
|
18
|
+
:path, :price, :asset, :network, :pay_to,
|
|
19
|
+
:description, :max_deadline_seconds,
|
|
20
|
+
keyword_init: true
|
|
21
|
+
) do
|
|
22
|
+
def initialize(**kwargs)
|
|
23
|
+
super
|
|
24
|
+
self.network ||= 'base-sepolia'
|
|
25
|
+
self.max_deadline_seconds ||= 60
|
|
26
|
+
self.description ||= ''
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class RackMiddleware
|
|
31
|
+
PAYMENT_HEADERS = %w[HTTP_X_PAYMENT HTTP_PAYMENT_SIGNATURE].freeze
|
|
32
|
+
X402_VERSION = 2
|
|
33
|
+
|
|
34
|
+
# @param app [#call] The downstream Rack app
|
|
35
|
+
# @param routes [Array<Hash>] Route payment configurations
|
|
36
|
+
# @param config [Hash] Options including :facilitator_url
|
|
37
|
+
def initialize(app, routes: [], config: {})
|
|
38
|
+
@app = app
|
|
39
|
+
@config = config
|
|
40
|
+
@routes = routes.map do |r|
|
|
41
|
+
r.is_a?(RouteConfig) ? r : RouteConfig.new(**r)
|
|
42
|
+
end
|
|
43
|
+
@client = Client.new(url: config[:facilitator_url])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(env)
|
|
47
|
+
return @app.call(env) if @routes.empty?
|
|
48
|
+
|
|
49
|
+
request_path = env['PATH_INFO'] || '/'
|
|
50
|
+
route = match_route(request_path)
|
|
51
|
+
|
|
52
|
+
return @app.call(env) unless route
|
|
53
|
+
|
|
54
|
+
payment_header = extract_payment_header(env)
|
|
55
|
+
|
|
56
|
+
return payment_required_response(route, env) unless payment_header
|
|
57
|
+
|
|
58
|
+
# Verify the payment via facilitator
|
|
59
|
+
payment_payload = parse_payment(payment_header)
|
|
60
|
+
requirements = build_requirements(route, env)
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
verify_result = @client.verify(
|
|
64
|
+
payment_payload: payment_payload,
|
|
65
|
+
payment_requirements: requirements
|
|
66
|
+
)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
return error_response(402, "Payment verification failed: #{e.message}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
unless verify_result['valid']
|
|
72
|
+
return error_response(402, verify_result['message'] || 'Payment verification failed')
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Call the downstream app
|
|
76
|
+
status, headers, body = @app.call(env)
|
|
77
|
+
|
|
78
|
+
# If the app returned an error, don't settle
|
|
79
|
+
return [status, headers, body] if status >= 400
|
|
80
|
+
|
|
81
|
+
# Settle the payment
|
|
82
|
+
begin
|
|
83
|
+
settle_result = @client.settle(
|
|
84
|
+
payment_payload: payment_payload,
|
|
85
|
+
payment_requirements: requirements
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return error_response(402, "Settlement failed: #{settle_result['message']}") unless settle_result['success']
|
|
89
|
+
|
|
90
|
+
headers['X-Payment-TxHash'] = settle_result['txHash'] if settle_result['txHash']
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
return error_response(402, "Settlement failed: #{e.message}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
[status, headers, body]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def match_route(path)
|
|
101
|
+
@routes.find do |route|
|
|
102
|
+
case route.path
|
|
103
|
+
when Regexp
|
|
104
|
+
route.path.match?(path)
|
|
105
|
+
when '*'
|
|
106
|
+
true
|
|
107
|
+
else
|
|
108
|
+
route.path == path
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def extract_payment_header(env)
|
|
114
|
+
PAYMENT_HEADERS.each do |header|
|
|
115
|
+
value = env[header]
|
|
116
|
+
return value if value && !value.empty?
|
|
117
|
+
end
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_payment(header_value)
|
|
122
|
+
JSON.parse(header_value)
|
|
123
|
+
rescue JSON::ParserError
|
|
124
|
+
# If not JSON, treat as an opaque token
|
|
125
|
+
{ 'token' => header_value }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_requirements(route, env)
|
|
129
|
+
{
|
|
130
|
+
'scheme' => 'exact',
|
|
131
|
+
'network' => route.network,
|
|
132
|
+
'asset' => route.asset,
|
|
133
|
+
'amount' => route.price,
|
|
134
|
+
'payTo' => route.pay_to,
|
|
135
|
+
'maxTimeoutSeconds' => route.max_deadline_seconds,
|
|
136
|
+
'resource' => {
|
|
137
|
+
'url' => request_url(env),
|
|
138
|
+
'method' => env['REQUEST_METHOD'],
|
|
139
|
+
'description' => route.description
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def request_url(env)
|
|
145
|
+
scheme = env['rack.url_scheme'] || 'https'
|
|
146
|
+
host = env['HTTP_HOST'] || env['SERVER_NAME'] || 'localhost'
|
|
147
|
+
path = env['PATH_INFO'] || '/'
|
|
148
|
+
"#{scheme}://#{host}#{path}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def payment_required_response(route, env)
|
|
152
|
+
body = {
|
|
153
|
+
'x402Version' => X402_VERSION,
|
|
154
|
+
'error' => 'Payment Required',
|
|
155
|
+
'accepts' => [{
|
|
156
|
+
'scheme' => 'exact',
|
|
157
|
+
'network' => route.network,
|
|
158
|
+
'asset' => route.asset,
|
|
159
|
+
'amount' => route.price,
|
|
160
|
+
'payTo' => route.pay_to,
|
|
161
|
+
'maxTimeoutSeconds' => route.max_deadline_seconds,
|
|
162
|
+
'resource' => {
|
|
163
|
+
'url' => request_url(env),
|
|
164
|
+
'method' => env['REQUEST_METHOD'],
|
|
165
|
+
'description' => route.description
|
|
166
|
+
}
|
|
167
|
+
}]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
[
|
|
171
|
+
402,
|
|
172
|
+
{
|
|
173
|
+
'Content-Type' => 'application/json',
|
|
174
|
+
'X-Payment-Required' => 'true'
|
|
175
|
+
},
|
|
176
|
+
[JSON.generate(body)]
|
|
177
|
+
]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def error_response(status, message)
|
|
181
|
+
body = { 'error' => message }
|
|
182
|
+
[
|
|
183
|
+
status,
|
|
184
|
+
{ 'Content-Type' => 'application/json' },
|
|
185
|
+
[JSON.generate(body)]
|
|
186
|
+
]
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|