calimero 0.2.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/AUTHORS +1 -0
- data/CHANGES.md +20 -0
- data/README.md +168 -0
- data/UNLICENSE +24 -0
- data/VERSION +1 -0
- data/lib/calimero/config/config.rb +48 -0
- data/lib/calimero/config.rb +3 -0
- data/lib/calimero/jsonrpc.rb +154 -0
- data/lib/calimero/types/context.rb +4 -0
- data/lib/calimero/types/keypair.rb +74 -0
- data/lib/calimero/types/rpc.rb +102 -0
- data/lib/calimero/types/rpc_request.rb +7 -0
- data/lib/calimero/types.rb +4 -0
- data/lib/calimero/version.rb +1 -0
- data/lib/calimero.rb +30 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 70d04b0fe2325e620f6ec464863c5ad0d631c4ab3fac3c64e0833588de23adf1
|
4
|
+
data.tar.gz: 5771da472ba8150018bda106145e13e60707962a7b20d0a3c6be3c601f92dbdd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: be4b26a632b5d9c73647d15e08622d64195ff9b665d979b7f3656a2c9f79ad9ffe48b2c04f8828587e9940746a84404aa0f2a9ae72866fd5fccc0e5ec125fb78
|
7
|
+
data.tar.gz: f1824a22ed7201565be9f76f2c8e1ff218755874ee1ff185c562180a2b47ba262629eafcb109014ad849c4bb485bfcaa2e6c34c40cb5e9291c8b85dc05c086a1
|
data/AUTHORS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* Kirill Abramov <septengineering@pm.me>
|
data/CHANGES.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## 0.2.0 - 2025-03-18
|
9
|
+
|
10
|
+
* Implement `Config` to manage config of Calimero nodes.
|
11
|
+
* Implement `Ed25519Keypair` to support identity keypairs and other keys used in Calimero.
|
12
|
+
* Implement authentication using Calimero identity key for interaction with the JSONRPC dev endpoint.
|
13
|
+
* Add tests for `Config` and `Ed25519Keypair` classes.
|
14
|
+
* Improve smoke tests.
|
15
|
+
* Update README with more examples.
|
16
|
+
|
17
|
+
## 0.1.0 - 2025-03-11
|
18
|
+
|
19
|
+
## 0.0.0 - 2025-03-08
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# Calimero Network for Ruby
|
2
|
+
|
3
|
+
[](https://unlicense.org)
|
4
|
+
[](https://rubygems.org/gems/calimero)
|
5
|
+
[](https://rubygems.org/gems/calimero)
|
6
|
+
[](https://rubydoc.info/gems/calimero)
|
7
|
+
|
8
|
+
**Calimero.rb** is a [Ruby] client library for the [Calimero Network].
|
9
|
+
|
10
|
+
> [!TIP]
|
11
|
+
> 🚧 _We are building in public. This is presently under heavy construction._
|
12
|
+
|
13
|
+
## ✨ Features
|
14
|
+
|
15
|
+
- Implemented natively in Ruby with minimal dependencies, ensuring low overhead and efficient performance.
|
16
|
+
- Implements a `JsonRpcClient` for sending queries and updates to the applications in Calimero nodes.
|
17
|
+
- Handles write and read calls to Calimero network applications.
|
18
|
+
- Handles config management of Calimero nodes.
|
19
|
+
- Manages authentication workflow using Ed25519-keypair.
|
20
|
+
- 🚧 Manages authentication workflow using token acquisitions and refresh.
|
21
|
+
- 🚧Implements a `WsSubscriptionsClient` for subscribing to real-time updates from the Calimero nodes.
|
22
|
+
- 🚧 Supports interaction with Calimero Admin and Calimero Node APIs.
|
23
|
+
- Adheres to the Ruby API Guidelines in its [naming conventions].
|
24
|
+
- 100% free and unencumbered public domain software.
|
25
|
+
|
26
|
+
## 🛠️ Prerequisites
|
27
|
+
|
28
|
+
- [Ruby] 3.0+
|
29
|
+
|
30
|
+
## ⬇️ Installation
|
31
|
+
|
32
|
+
### Installation via RubyGems
|
33
|
+
|
34
|
+
```bash
|
35
|
+
gem install calimero
|
36
|
+
```
|
37
|
+
|
38
|
+
## 👉 Examples
|
39
|
+
|
40
|
+
### Importing the library
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'calimero'
|
44
|
+
```
|
45
|
+
|
46
|
+
### Loading the Calimero config
|
47
|
+
|
48
|
+
You can load a Calimero config file the following way:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
require 'calimero'
|
52
|
+
|
53
|
+
config_path = "/path/to/your/calimero/config.toml"
|
54
|
+
config = Calimero::load_config(config_path)
|
55
|
+
```
|
56
|
+
|
57
|
+
If you would like to utilize the default Calimero config folder:
|
58
|
+
```ruby
|
59
|
+
require 'calimero'
|
60
|
+
|
61
|
+
config_path = "#{Calimero::default_config_folder}/node1/config.toml"
|
62
|
+
config = Calimero::load_config(config_path)
|
63
|
+
```
|
64
|
+
|
65
|
+
### Importing `Ed25519Keypair` from the config and signing an arbitrary message with it
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
require 'calimero'
|
69
|
+
|
70
|
+
config_path = "#{Calimero::default_config_folder}/node1/config.toml"
|
71
|
+
config = Calimero::load_config(config_path)
|
72
|
+
|
73
|
+
message = "Hello, Calimero"
|
74
|
+
signature = config.keypair.sign(message)
|
75
|
+
```
|
76
|
+
|
77
|
+
### Importing `Ed25519Keypair` from base58-encoded protobuf message and signing an arbitrary message with it
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
require 'calimero'
|
81
|
+
|
82
|
+
# The keypair should be base58-encoded protobuf message (using `libp2p_identity::Keypair`)
|
83
|
+
keypair_base58_protobuf = "<YOUR_BASE58_ENCODED_ED25519_KEYPAIR>"
|
84
|
+
keypair = Ed25519Keypair.new(keypair_base58_protobuf)
|
85
|
+
message = "Hello, Calimero"
|
86
|
+
signature = keypair.sign(message)
|
87
|
+
```
|
88
|
+
|
89
|
+
### Executing arbitrary method in Calimero Application with authentication using dev JSONRPC endpoint
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
require 'calimero'
|
93
|
+
require 'base58'
|
94
|
+
|
95
|
+
client = JsonRpcClient.new('http://localhost:2428', '/jsonrpc/dev')
|
96
|
+
params = RpcQueryParams.new('your_application_context_id', 'some_method', { 'some': 'args' }, 'executor_public_key')
|
97
|
+
|
98
|
+
config_path = "#{Calimero::default_config_folder}/node1/config.toml"
|
99
|
+
config = Calimero::load_config(config_path)
|
100
|
+
|
101
|
+
timestamp = Time.now.utc.to_i.to_s
|
102
|
+
signature = config.keypair.sign(timestamp)
|
103
|
+
signature_b58 = Base58.binary_to_base58(signature, :bitcoin)
|
104
|
+
|
105
|
+
headers = {
|
106
|
+
'Content-Type' => 'application/json',
|
107
|
+
'X-Signature' => signature_b58,
|
108
|
+
'X-Timestamp' => timestamp
|
109
|
+
}
|
110
|
+
request_config = RequestConfig.new(timeout: 1000, headers: headers)
|
111
|
+
result = client.execute(query_params, request_config)
|
112
|
+
if result.error
|
113
|
+
puts "Error: #{result.error}"
|
114
|
+
else
|
115
|
+
puts "Result: #{result.result}"
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
### Executing arbitrary method in Calimero Application
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
require 'calimero'
|
123
|
+
|
124
|
+
client = JsonRpcClient.new('http://localhost:2428', '/jsonrpc')
|
125
|
+
params = RpcQueryParams.new('your_application_context_id', 'some_method', { 'some': 'args' }, 'executor_public_key')
|
126
|
+
bearer_auth_token = "some bearer auth token"
|
127
|
+
headers = {
|
128
|
+
'Content-Type' => 'application/json',
|
129
|
+
'Authorization' => "Bearer #{bearer_auth_token}"
|
130
|
+
}
|
131
|
+
request_config = RequestConfig.new(timeout: 1000, headers: headers)
|
132
|
+
result = client.execute(params, request_config)
|
133
|
+
if result.error
|
134
|
+
puts "Error: #{result.error}"
|
135
|
+
else
|
136
|
+
puts "Result: #{result.result}"
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
### Fetching all posts from OnlyPeers application
|
141
|
+
|
142
|
+
You can query all the posts in the given [OnlyPeers] demo application, by using the following example:
|
143
|
+
```sh
|
144
|
+
CONTEXT_ID=<ONLYPEERS_CONTEXT_ID> EXECUTOR_PUBLIC_KEY=<YOUR_EXECUTOR_PUBLIC_KEY> ruby examples/onlypeers_get_all_posts.rb
|
145
|
+
```
|
146
|
+
|
147
|
+
That example also contains an example on how to use the `Config` and `Ed25519Keypair` to authenticate your requests to the Calimero node.
|
148
|
+
|
149
|
+
## 📚 Reference
|
150
|
+
|
151
|
+
https://rubydoc.info/gems/calimero
|
152
|
+
|
153
|
+
## 👨💻 Development
|
154
|
+
|
155
|
+
```bash
|
156
|
+
git clone https://github.com/dryruby/calimero.rb.git
|
157
|
+
```
|
158
|
+
|
159
|
+
- - -
|
160
|
+
|
161
|
+
[](https://x.com/share?url=https://github.com/dryruby/calimero.rb&text=calimero.rb)
|
162
|
+
[](https://reddit.com/submit?url=https://github.com/dryruby/calimero.rb&title=calimero.rb)
|
163
|
+
[](https://news.ycombinator.com/submitlink?u=https://github.com/dryruby/calimero.rb&t=calimero.rb)
|
164
|
+
[](https://www.facebook.com/sharer/sharer.php?u=https://github.com/dryruby/calimero.rb)
|
165
|
+
|
166
|
+
[Calimero Network]: https://calimero.network/
|
167
|
+
[Ruby]: https://ruby-lang.org
|
168
|
+
[OnlyPeers]: https://calimero-network.github.io/tutorials/awesome-projects/only-peers/
|
data/UNLICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <https://unlicense.org/>
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
require 'toml-rb'
|
4
|
+
require_relative '../types/keypair'
|
5
|
+
|
6
|
+
class ConfigError < StandardError; end
|
7
|
+
|
8
|
+
# Configuration class that holds a Keypair and is extensible for future for other fields
|
9
|
+
class Config
|
10
|
+
attr_reader :keypair
|
11
|
+
|
12
|
+
# Initialize with a TOML file path
|
13
|
+
def initialize(file_path)
|
14
|
+
@config_data = load_toml(file_path)
|
15
|
+
keypair_value = @config_data.dig('identity', 'keypair')
|
16
|
+
raise ConfigError, "'keypair' not found in [identity] section" unless keypair_value
|
17
|
+
@keypair = Ed25519Keypair.new(keypair_value)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Allow dynamic access to raw config data
|
21
|
+
def [](key)
|
22
|
+
@config_data[key]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Extend config with additional fields in the future
|
26
|
+
def method_missing(method_name, *args, &block)
|
27
|
+
if @config_data.key?(method_name.to_s)
|
28
|
+
@config_data[method_name.to_s]
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def respond_to_missing?(method_name, include_private = false)
|
35
|
+
@config_data.key?(method_name.to_s) || super
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_toml(file_path)
|
39
|
+
TomlRB.load_file(file_path)
|
40
|
+
rescue Errno::ENOENT
|
41
|
+
raise ConfigError, "Config file '#{file_path}' not found"
|
42
|
+
rescue TomlRB::ParseError => e
|
43
|
+
raise ConfigError, "Failed to parse TOML file: #{e.message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
private :load_toml
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative 'types/rpc'
|
6
|
+
|
7
|
+
module HTTPStatusCodes
|
8
|
+
HTTPOK = 200
|
9
|
+
HTTPBadRequest = 400
|
10
|
+
HTTPInternalServerError = 500
|
11
|
+
end
|
12
|
+
|
13
|
+
class JsonRpcClient < RpcClient
|
14
|
+
attr_reader :path, :base_url, :default_timeout
|
15
|
+
|
16
|
+
def initialize(base_url, path, default_timeout = 1000)
|
17
|
+
@base_url = base_url
|
18
|
+
@path = path
|
19
|
+
@default_timeout = default_timeout
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute(params, config = RequestConfig.new(timeout: default_timeout))
|
23
|
+
request('execute', params, config)
|
24
|
+
end
|
25
|
+
|
26
|
+
def request(method, params, config = RequestConfig.new(timeout: default_timeout))
|
27
|
+
request_id = get_random_request_id
|
28
|
+
data = {
|
29
|
+
jsonrpc: '2.0',
|
30
|
+
id: request_id,
|
31
|
+
method: method,
|
32
|
+
params: params.instance_variables.each_with_object({}) { |var, hash| hash[var.to_s.delete('@')] = params.instance_variable_get(var) }
|
33
|
+
}
|
34
|
+
|
35
|
+
uri = URI.parse("#{@base_url}#{@path}")
|
36
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
37
|
+
http.use_ssl = uri.scheme == 'https'
|
38
|
+
http.open_timeout = config.timeout || @default_timeout
|
39
|
+
http.read_timeout = config.timeout || @default_timeout
|
40
|
+
headers = {'Content-Type' => 'application/json'}.merge(config.headers ? config.headers : {})
|
41
|
+
|
42
|
+
begin
|
43
|
+
response = http.post(uri.path, data.to_json, headers)
|
44
|
+
parsed_response = JSON.parse(response.body)
|
45
|
+
|
46
|
+
if response.is_a?(Net::HTTPOK)
|
47
|
+
if parsed_response['id'] != request_id
|
48
|
+
return RpcResult.new(result: nil, error: {
|
49
|
+
code: HTTPStatusCodes::HTTPBadRequest,
|
50
|
+
id: parsed_response['id'],
|
51
|
+
jsonrpc: parsed_response['jsonrpc'],
|
52
|
+
error: {
|
53
|
+
name: 'MissmatchedRequestIdError',
|
54
|
+
cause: {
|
55
|
+
name: 'MissmatchedRequestIdError',
|
56
|
+
info: {
|
57
|
+
message: "Missmatched RequestId expected #{request_id}, got #{parsed_response['id']}"
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
})
|
62
|
+
end
|
63
|
+
|
64
|
+
error_data = parsed_response['error']
|
65
|
+
#TODO figure out if there are still weird use cases where error_data['data']['data'] might not be a Hash, but a String
|
66
|
+
if error_data
|
67
|
+
error_cause_name = if error_data['data'].is_a?(Hash)
|
68
|
+
error_data.dig('data', 'type')
|
69
|
+
else
|
70
|
+
error_data['type']
|
71
|
+
end
|
72
|
+
error_message = if error_data['data'].is_a?(Hash) && error_data['data']['data'].is_a?(Hash)
|
73
|
+
error_data.dig('data', 'data', 'type')
|
74
|
+
else
|
75
|
+
error_data['data']
|
76
|
+
end
|
77
|
+
return RpcResult.new(result: nil, error: {
|
78
|
+
code: HTTPStatusCodes::HTTPBadRequest,
|
79
|
+
id: parsed_response['id'],
|
80
|
+
jsonrpc: parsed_response['jsonrpc'],
|
81
|
+
error: {
|
82
|
+
name: error_data['type'],
|
83
|
+
cause: {
|
84
|
+
name: error_cause_name,
|
85
|
+
info: {
|
86
|
+
message: error_message
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
})
|
91
|
+
end
|
92
|
+
|
93
|
+
return RpcResult.new(result: parsed_response['result'], error: nil)
|
94
|
+
else
|
95
|
+
error_data = parsed_response['error']
|
96
|
+
error_message = if error_data['data'].is_a?(Hash) && error_data['data']['data'].is_a?(Hash)
|
97
|
+
error_data.dig('data', 'data', 'type')
|
98
|
+
else
|
99
|
+
error_data['data']
|
100
|
+
end
|
101
|
+
return RpcResult.new(result: nil, error: {
|
102
|
+
id: parsed_response['id'],
|
103
|
+
jsonrpc: parsed_response['jsonrpc'],
|
104
|
+
code: response.code.to_i,
|
105
|
+
error: {
|
106
|
+
name: 'InvalidRequestError',
|
107
|
+
cause: {
|
108
|
+
name: 'InvalidRequestError',
|
109
|
+
info: {
|
110
|
+
message: error_message
|
111
|
+
}
|
112
|
+
}
|
113
|
+
}
|
114
|
+
})
|
115
|
+
end
|
116
|
+
rescue JSON::ParserError
|
117
|
+
return RpcResult.new(result: nil, error: {
|
118
|
+
id: request_id,
|
119
|
+
jsonrpc: '2.0',
|
120
|
+
code: HTTPStatusCodes::HTTPInternalServerError,
|
121
|
+
error: {
|
122
|
+
name: 'InvalidJsonResponseError',
|
123
|
+
cause: {
|
124
|
+
name: 'InvalidJsonResponseError',
|
125
|
+
info: {
|
126
|
+
message: "Invalid JSON response from server."
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
})
|
131
|
+
rescue StandardError => e
|
132
|
+
return RpcResult.new(result: nil, error: {
|
133
|
+
id: request_id,
|
134
|
+
jsonrpc: '2.0',
|
135
|
+
code: HTTPStatusCodes::HTTPInternalServerError,
|
136
|
+
error: {
|
137
|
+
name: 'UnknownServerError',
|
138
|
+
cause: {
|
139
|
+
name: 'UnknownServerError',
|
140
|
+
info: {
|
141
|
+
message: e.message
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
})
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def get_random_request_id
|
150
|
+
rand(2**32)
|
151
|
+
end
|
152
|
+
|
153
|
+
private :request, :get_random_request_id
|
154
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
require 'base58'
|
4
|
+
require 'ed25519'
|
5
|
+
|
6
|
+
class KeypairError < StandardError; end
|
7
|
+
|
8
|
+
class Ed25519Keypair
|
9
|
+
attr_reader :private_key, :public_key, :stored_public_key
|
10
|
+
|
11
|
+
# Expected protobuf prefix for Ed25519 keypair (type: 1, data length: 64)
|
12
|
+
# The implementation is used for compatibility with Ed25519 keypair from
|
13
|
+
# [libp2p_identity/keypair](https://github.com/libp2p/rust-libp2p/blob/88f7875ad1a3e240aa2d9b9fb6f6c5354f1a62eb/identity/src/keypair.rs#L262)
|
14
|
+
PROTOBUF_PREFIX = "\x08\x01\x12\x40".freeze
|
15
|
+
|
16
|
+
# Initialize with a Base58-encoded keypair string
|
17
|
+
def initialize(base58_keypair)
|
18
|
+
raise KeypairError, "Base58 keypair cannot be nil" if base58_keypair.nil?
|
19
|
+
@key_bytes = decode_base58(base58_keypair)
|
20
|
+
validate_keypair_length
|
21
|
+
validate_protobuf_prefix
|
22
|
+
extract_keys
|
23
|
+
initialize_signing_key
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sign a message (as raw bytes)
|
27
|
+
def sign(message)
|
28
|
+
@signing_key.sign(message)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Verify a signature against a message
|
32
|
+
def verify(signature, message)
|
33
|
+
@verify_key.verify(signature, message)
|
34
|
+
true
|
35
|
+
rescue Ed25519::VerifyError
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def decode_base58(base58_keypair)
|
40
|
+
Base58.base58_to_binary(base58_keypair, :bitcoin)
|
41
|
+
rescue StandardError => e
|
42
|
+
raise KeypairError, "Failed to decode Base58 keypair: #{e.message}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_keypair_length
|
46
|
+
return if @key_bytes.length == 68
|
47
|
+
raise KeypairError, "Unexpected keypair length: #{@key_bytes.length} bytes (expected 68)"
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_protobuf_prefix
|
51
|
+
prefix = @key_bytes[0..3]
|
52
|
+
unless prefix == PROTOBUF_PREFIX
|
53
|
+
raise KeypairError, "Invalid protobuf prefix: #{prefix.unpack1('H*')} (expected #{PROTOBUF_PREFIX.unpack1('H*')})"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def extract_keys
|
58
|
+
@private_key = @key_bytes[4..35] # 32-byte private key
|
59
|
+
@stored_public_key = @key_bytes[36..67] # 32-byte stored public key
|
60
|
+
unless @private_key.length == 32 && @stored_public_key.length == 32
|
61
|
+
raise KeypairError, "Invalid key lengths: private #{@private_key.length}, stored public #{@stored_public_key.length} (expected 32 each)"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def initialize_signing_key
|
66
|
+
@signing_key = Ed25519::SigningKey.new(@private_key)
|
67
|
+
@verify_key = @signing_key.verify_key
|
68
|
+
@public_key = @verify_key.to_bytes
|
69
|
+
rescue ArgumentError => e
|
70
|
+
raise KeypairError, "Invalid Ed25519 private key: #{e.message}"
|
71
|
+
end
|
72
|
+
|
73
|
+
private :decode_base58, :validate_keypair_length, :extract_keys, :initialize_signing_key
|
74
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
require_relative 'context'
|
4
|
+
require_relative 'rpc_request'
|
5
|
+
|
6
|
+
class RpcError < StandardError
|
7
|
+
attr_reader :id, :jsonrpc, :code, :error_info
|
8
|
+
|
9
|
+
def initialize(id, jsonrpc, code, error_info)
|
10
|
+
@id = id
|
11
|
+
@jsonrpc = jsonrpc
|
12
|
+
@code = code
|
13
|
+
@error_info = error_info
|
14
|
+
super(error_info[:message])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class RpcErrorInfo < StandardError
|
19
|
+
attr_reader :name, :cause
|
20
|
+
|
21
|
+
def initialize(name, cause)
|
22
|
+
@name = name
|
23
|
+
@cause = cause
|
24
|
+
super(cause[:message])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class RpcCauseInfo < StandardError
|
29
|
+
attr_reader :name, :info
|
30
|
+
|
31
|
+
def initialize(name, info)
|
32
|
+
@name = name
|
33
|
+
@info = info
|
34
|
+
super(info[:message])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class RpcClient
|
39
|
+
def execute(params, config = {})
|
40
|
+
raise NotImplementedError, "Subclasses must implement execute"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class RequestConfig
|
45
|
+
attr_accessor :timeout, :headers
|
46
|
+
|
47
|
+
def initialize(timeout: nil, headers: nil)
|
48
|
+
@timeout = timeout
|
49
|
+
@headers = headers
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_h
|
53
|
+
{
|
54
|
+
timeout: @timeout,
|
55
|
+
headers: @headers
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class RpcResult
|
61
|
+
attr_accessor :result, :error
|
62
|
+
|
63
|
+
def initialize(result: nil, error: nil)
|
64
|
+
@result = result
|
65
|
+
@error = error
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_h
|
69
|
+
{
|
70
|
+
result: @result,
|
71
|
+
error: @error,
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class RpcQueryParams
|
77
|
+
attr_accessor :contextId, :method, :argsJson, :executorPublicKey
|
78
|
+
|
79
|
+
def initialize(contextId, method, argsJson, executorPublicKey)
|
80
|
+
@contextId = contextId
|
81
|
+
@method = method
|
82
|
+
@argsJson = argsJson
|
83
|
+
@executorPublicKey = executorPublicKey
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_h
|
87
|
+
{
|
88
|
+
contextId: @contextId,
|
89
|
+
method: @method,
|
90
|
+
argsJson: @argsJson,
|
91
|
+
executorPublicKey: @executorPublicKey
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class RpcQueryResponse
|
97
|
+
attr_accessor :output
|
98
|
+
|
99
|
+
def initialize(output: nil)
|
100
|
+
@output = output
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
data/lib/calimero.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
module Calimero; end
|
4
|
+
|
5
|
+
require_relative 'calimero/config'
|
6
|
+
require_relative 'calimero/jsonrpc'
|
7
|
+
require_relative 'calimero/types'
|
8
|
+
require_relative 'calimero/version'
|
9
|
+
|
10
|
+
module Calimero
|
11
|
+
##
|
12
|
+
# @return [Calimero::default_rpc_url]
|
13
|
+
def self.default_rpc_url
|
14
|
+
@rpc_url ||= "http://127.0.0.1:2428"
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# @return [Calimero::default_config_path]
|
19
|
+
def self.default_config_folder
|
20
|
+
@config_path ||= "#{Dir.home}/.calimero"
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# @return [Calimero::load_config]
|
25
|
+
# Utility method to load config from TOML file
|
26
|
+
def self.load_config(file_path)
|
27
|
+
Config.new(file_path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
metadata
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: calimero
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kirill Abramov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-03-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tempfile
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.3.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.3.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: net-http
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.4.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.4.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: uri
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.10.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.10.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: json
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.10'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.10'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: toml-rb
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.0.1
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 3.0.1
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: ed25519
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.3.0
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.3.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: base58
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.2.3
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.2.3
|
139
|
+
description: A Ruby client library for the Calimero Network.
|
140
|
+
email: septengineering@pm.me
|
141
|
+
executables: []
|
142
|
+
extensions: []
|
143
|
+
extra_rdoc_files: []
|
144
|
+
files:
|
145
|
+
- AUTHORS
|
146
|
+
- CHANGES.md
|
147
|
+
- README.md
|
148
|
+
- UNLICENSE
|
149
|
+
- VERSION
|
150
|
+
- lib/calimero.rb
|
151
|
+
- lib/calimero/config.rb
|
152
|
+
- lib/calimero/config/config.rb
|
153
|
+
- lib/calimero/jsonrpc.rb
|
154
|
+
- lib/calimero/types.rb
|
155
|
+
- lib/calimero/types/context.rb
|
156
|
+
- lib/calimero/types/keypair.rb
|
157
|
+
- lib/calimero/types/rpc.rb
|
158
|
+
- lib/calimero/types/rpc_request.rb
|
159
|
+
- lib/calimero/version.rb
|
160
|
+
homepage: https://github.com/dryruby/calimero.rb
|
161
|
+
licenses:
|
162
|
+
- Unlicense
|
163
|
+
metadata:
|
164
|
+
bug_tracker_uri: https://github.com/dryruby/calimero.rb/issues
|
165
|
+
changelog_uri: https://github.com/dryruby/calimero.rb/blob/master/CHANGES.md
|
166
|
+
documentation_uri: https://rubydoc.info/gems/calimero
|
167
|
+
homepage_uri: https://github.com/dryruby/calimero.rb
|
168
|
+
source_code_uri: https://github.com/dryruby/calimero.rb
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options: []
|
171
|
+
require_paths:
|
172
|
+
- lib
|
173
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">="
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '3.0'
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
requirements: []
|
184
|
+
rubygems_version: 3.0.3.1
|
185
|
+
signing_key:
|
186
|
+
specification_version: 4
|
187
|
+
summary: 'Calimero.rb: Calimero Network for Ruby'
|
188
|
+
test_files: []
|