digitalbits-sdk 0.27.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6792d096cad91255c2b26fc1d91559e94d12ce0e934d61d561232ee2f32eb2c4
4
+ data.tar.gz: a1d4158baa655e722b14ce91b53ad13f64211fe537f2f113d35440305555803f
5
+ SHA512:
6
+ metadata.gz: b87bbaf3c2e65e4e3b06cb426316367a2547868e586adac5c35c572e53ff6210bba9d76d15da6747374b0bc036ec47726828f04135eec2589744008ea3c4b459
7
+ data.tar.gz: 2e148b6aa7cea4bf74f1bb8d9d43d04d6cb65aa1c0df3350d9689663efa4b661ab119b90317c98408978abd4cd1f53399fb9080eb114be44fea16202ae545b96
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2021 XDB Foundation
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # DigitalBIts SDK for Ruby: Frontier Integration and Higher Level Abstractions
2
+ [
3
+ This library helps you to integrate your application into the [DigitalBits network](http://digitalbits.io).
4
+
5
+ ## Installation
6
+
7
+ Add this lines to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'digitalbits-sdk'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Also requires libsodium. Installable via `brew install libsodium` on OS X.
18
+
19
+ ## Usage
20
+
21
+ See [examples](examples).
22
+
23
+ A simple payment from the root account to some random accounts
24
+
25
+ ```ruby
26
+ require 'digitalbits-sdk'
27
+
28
+ account = DigitalBits::Account.master
29
+ client = DigitalBits::Client.default_testnet()
30
+ recipient = DigitalBits::Account.random
31
+
32
+ client.send_payment({
33
+ from: account,
34
+ to: recipient,
35
+ amount: DigitalBits::Amount.new(100_000_000)
36
+ })
37
+ ```
38
+
39
+ Be sure to set the network when submitting to the public network (more information in [digitalbits-base](https://www.github.com/xdbfoundation/ruby-digitalbits-base)):
40
+
41
+ ```ruby
42
+ DigitalBits.default_network = DigitalBits::Networks::PUBLIC
43
+ ```
44
+
45
+ ## Development
46
+
47
+ - Install and activate [rvm](https://rvm.io/rvm/install)
48
+ - Ensure your `bundler` version is up-to-date: `gem install bundler`
49
+ - Run `bundle install`
50
+ - Copy `spec/config.yml.sample` to `spec/config.yml`
51
+ - Replace anything in `spec/config.yml` especially if you will re-record specs
52
+ - `bundle exec rspec spec`
53
+
54
+ ## Contributing
55
+
56
+ 1. Fork it ( https://github.com/xdbfoundation/ruby-digitalbits-sdk/fork )
57
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
58
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
59
+ 4. Push to the branch (`git push origin my-new-feature`)
60
+ 5. Create a new Pull Request
@@ -0,0 +1,21 @@
1
+ require "digitalbits-base"
2
+
3
+ module DigitalBits
4
+ module SDK
5
+ VERSION = ::DigitalBits::VERSION
6
+ end
7
+
8
+ autoload :Account
9
+ autoload :Amount
10
+ autoload :Client
11
+ autoload :SEP10
12
+
13
+ module Frontier
14
+ extend ActiveSupport::Autoload
15
+
16
+ autoload :Problem
17
+ autoload :Result
18
+ end
19
+
20
+ autoload :TransactionPage
21
+ end
@@ -0,0 +1,89 @@
1
+ require "toml-rb"
2
+ require "uri"
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module DigitalBits
7
+ class Account
8
+ delegate :address, to: :keypair
9
+
10
+ def self.random
11
+ keypair = DigitalBits::KeyPair.random
12
+ new(keypair)
13
+ end
14
+
15
+ def self.from_seed(seed)
16
+ keypair = DigitalBits::KeyPair.from_seed(seed)
17
+ new(keypair)
18
+ end
19
+
20
+ def self.from_address(address)
21
+ keypair = DigitalBits::KeyPair.from_address(address)
22
+ new(keypair)
23
+ end
24
+
25
+ def self.lookup(federated_name)
26
+ _, domain = federated_name.split("*")
27
+ if domain.nil?
28
+ raise InvalidFederationAddress.new
29
+ end
30
+
31
+ domain_req = Faraday.new("https://#{domain}/.well-known/digitalbits.toml").get
32
+
33
+ unless domain_req.status == 200
34
+ raise InvalidDigitalBitsDomain.new("Domain does not contain digitalbits.toml file")
35
+ end
36
+
37
+ fed_server_url = TomlRB.parse(domain_req.body)["FEDERATION_SERVER"]
38
+ if fed_server_url.nil?
39
+ raise InvalidDigitalBitsTOML.new("Invalid DigitalBits TOML file")
40
+ end
41
+
42
+ unless fed_server_url&.match?(URI::DEFAULT_PARSER.make_regexp)
43
+ raise InvalidFederationURL.new("Invalid Federation Server URL")
44
+ end
45
+
46
+ lookup_req = Faraday.new(fed_server_url).get { |req|
47
+ req.params[:q] = federated_name
48
+ req.params[:type] = "name"
49
+ }
50
+
51
+ unless lookup_req.status == 200
52
+ raise AccountNotFound.new("Account not found")
53
+ end
54
+
55
+ JSON.parse(lookup_req.body)["account_id"]
56
+ end
57
+
58
+ def self.master
59
+ keypair = DigitalBits::KeyPair.from_raw_seed("allmylifemyhearthasbeensearching")
60
+ new(keypair)
61
+ end
62
+
63
+ attr_reader :keypair
64
+
65
+ # @param [DigitalBits::KeyPair] keypair
66
+ def initialize(keypair)
67
+ @keypair = keypair
68
+ end
69
+
70
+ def to_keypair
71
+ keypair
72
+ end
73
+ end
74
+
75
+ class AccountNotFound < StandardError
76
+ end
77
+
78
+ class InvalidDigitalBitsTOML < StandardError
79
+ end
80
+
81
+ class InvalidFederationAddress < StandardError
82
+ end
83
+
84
+ class InvalidDigitalBitsDomain < StandardError
85
+ end
86
+
87
+ class InvalidFederationURL < StandardError
88
+ end
89
+ end
@@ -0,0 +1,36 @@
1
+ module DigitalBits
2
+ class Amount
3
+ attr_reader :amount
4
+ attr_reader :asset
5
+
6
+ # @param [Fixnum] amount
7
+ # @param [DigitalBits::Asset] asset
8
+ def initialize(amount, asset = DigitalBits::Asset.native)
9
+ # TODO: how are we going to handle decimal considerations?
10
+
11
+ @amount = amount
12
+ @asset = asset
13
+ end
14
+
15
+ # @return [Array(Symbol, Fixnum)] in case of a native asset
16
+ # @return [Array(Symbol, String, DigitalBits::KeyPair, Fixnum)] in case of alphanum asset
17
+ def to_payment
18
+ case asset.type
19
+ when AssetType.asset_type_native
20
+ [:native, amount]
21
+ when AssetType.asset_type_credit_alphanum4
22
+ keypair = KeyPair.from_public_key(asset.issuer.value)
23
+ [:alphanum4, asset.code, keypair, amount]
24
+ when AssetType.asset_type_credit_alphanum12
25
+ keypair = KeyPair.from_public_key(asset.issuer.value)
26
+ [:alphanum12, asset.code, keypair, amount]
27
+ else
28
+ raise "Unknown asset type: #{asset.type}"
29
+ end
30
+ end
31
+
32
+ def inspect
33
+ "#<DigitalBits::Amount #{asset}(#{amount})>"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,298 @@
1
+ require "hyperclient"
2
+ require "active_support/core_ext/object/blank"
3
+ require "securerandom"
4
+
5
+ module DigitalBits
6
+ class AccountRequiresMemoError < StandardError
7
+ attr_reader :account_id, :operation_index
8
+
9
+ def initialize(message, account_id, operation_index)
10
+ super(message)
11
+ @account_id = account_id
12
+ @operation_index = operation_index
13
+ end
14
+ end
15
+
16
+ class Client
17
+ DEFAULT_FEE = 100
18
+
19
+ FRONTIER_LOCALHOST_URL = "http://127.0.0.1:8000"
20
+ FRONTIER_MAINNET_URL = "https://frontier.livenet.digitalbits.io"
21
+ FRONTIER_TESTNET_URL = "https://frontier.testnet.digitalbits.io"
22
+ FRIENDBOT_URL = "https://friendbot.testnet.digitalbits.io".freeze
23
+
24
+ def self.default(options = {})
25
+ new options.merge(
26
+ frontier: FRONTIER_MAINNET_URL
27
+ )
28
+ end
29
+
30
+ def self.default_testnet(options = {})
31
+ new options.merge(
32
+ frontier: FRONTIER_TESTNET_URL,
33
+ friendbot: FRONTIER_TESTNET_URL
34
+ )
35
+ end
36
+
37
+ def self.localhost(options = {})
38
+ new options.merge(
39
+ frontier: FRONTIER_LOCALHOST_URL
40
+ )
41
+ end
42
+
43
+ attr_reader :frontier
44
+
45
+ # @option options [String] :frontier The Frontier server URL.
46
+ def initialize(options)
47
+ @options = options
48
+ @frontier = Hyperclient.new(options[:frontier]) { |client|
49
+ client.faraday_block = lambda do |conn|
50
+ conn.use Faraday::Response::RaiseError
51
+ conn.use FaradayMiddleware::FollowRedirects
52
+ conn.request :url_encoded
53
+ conn.response :hal_json, content_type: /\bjson$/
54
+ conn.adapter :excon
55
+ end
56
+ client.headers = {
57
+ "Accept" => "application/hal+json,application/problem+json,application/json",
58
+ "X-Client-Name" => "ruby-digitalbits-sdk",
59
+ "X-Client-Version" => VERSION
60
+ }
61
+ }
62
+ end
63
+
64
+ # @param [DigitalBits::Account|String] account_or_address
65
+ def account_info(account_or_address)
66
+ account_id = if account_or_address.is_a?(DigitalBits::Account)
67
+ account_or_address.address
68
+ else
69
+ account_or_address
70
+ end
71
+ @frontier.account(account_id: account_id)._get
72
+ end
73
+
74
+ # @option options [DigitalBits::Account] :account
75
+ # @option options [DigitalBits::Account] :destination
76
+ def account_merge(options = {})
77
+ account = options[:account]
78
+ destination = options[:destination]
79
+ sequence = options[:sequence] || (account_info(account).sequence.to_i + 1)
80
+
81
+ transaction = DigitalBits::TransactionBuilder.account_merge(
82
+ source_account: destination.keypair,
83
+ sequence_number: sequence,
84
+ destination: destination.keypair
85
+ )
86
+
87
+ envelope = transaction.to_envelope(account.keypair)
88
+ submit_transaction(tx_envelope: envelope)
89
+ end
90
+
91
+ def friendbot(account)
92
+ uri = URI.parse(FRIENDBOT_URL)
93
+ uri.query = "addr=#{account.address}"
94
+ Faraday.post(uri.to_s)
95
+ end
96
+
97
+ # @option options [DigitalBits::Account] :account
98
+ # @option options [DigitalBits::Account] :funder
99
+ # @option options [Integer] :starting_balance
100
+ def create_account(options = {})
101
+ funder = options[:funder]
102
+ sequence = options[:sequence] || (account_info(funder).sequence.to_i + 1)
103
+ # In the future, the fee should be grabbed from the network's last transactions,
104
+ # instead of using a hard-coded default value.
105
+ fee = options[:fee] || DEFAULT_FEE
106
+
107
+ payment = DigitalBits::TransactionBuilder.create_account(
108
+ source_account: funder.keypair,
109
+ sequence_number: sequence,
110
+ base_fee: fee,
111
+ destination: options[:account].keypair,
112
+ starting_balance: options[:starting_balance]
113
+ )
114
+ envelope = payment.to_envelope(funder.keypair)
115
+ submit_transaction(tx_envelope: envelope)
116
+ end
117
+
118
+ # @option options [DigitalBits::Account] :from The source account
119
+ # @option options [DigitalBits::Account] :to The destination account
120
+ # @option options [DigitalBits::Amount] :amount The amount to send
121
+ def send_payment(options = {})
122
+ from_account = options[:from]
123
+ tx_source_account = options[:transaction_source] || from_account
124
+ op_source_account = from_account if tx_source_account.present?
125
+
126
+ sequence = options[:sequence] ||
127
+ (account_info(tx_source_account).sequence.to_i + 1)
128
+
129
+ payment = DigitalBits::TransactionBuilder.new(
130
+ source_account: tx_source_account.keypair,
131
+ sequence_number: sequence
132
+ ).add_operation(
133
+ DigitalBits::Operation.payment(
134
+ source_account: op_source_account.keypair,
135
+ destination: options[:to].keypair,
136
+ amount: options[:amount].to_payment
137
+ )
138
+ ).set_memo(options[:memo]).set_timeout(0).build
139
+
140
+ signers = [tx_source_account, op_source_account].uniq(&:address)
141
+ to_envelope_args = signers.map(&:keypair)
142
+
143
+ envelope = payment.to_envelope(*to_envelope_args)
144
+ submit_transaction(tx_envelope: envelope)
145
+ end
146
+
147
+ # @option options [DigitalBits::Account] :account
148
+ # @option options [Integer] :limit
149
+ # @option options [Integer] :cursor
150
+ # @return [DigitalBits::TransactionPage]
151
+ def transactions(options = {})
152
+ args = options.slice(:limit, :cursor)
153
+
154
+ resource = if options[:account]
155
+ args = args.merge(account_id: options[:account].address)
156
+ @frontier.account_transactions(args)
157
+ else
158
+ @frontier.transactions(args)
159
+ end
160
+
161
+ TransactionPage.new(resource)
162
+ end
163
+
164
+ # @param [Array(Symbol,String,DigitalBits::KeyPair|DigitalBits::Account)] asset
165
+ # @param [DigitalBits::Account] source
166
+ # @param [Integer] sequence
167
+ # @param [Integer] fee
168
+ # @param [Integer] limit
169
+ def change_trust(
170
+ asset:,
171
+ source:,
172
+ sequence: nil,
173
+ fee: DEFAULT_FEE,
174
+ limit: nil
175
+ )
176
+ sequence ||= (account_info(source).sequence.to_i + 1)
177
+
178
+ op_args = {
179
+ account: source.keypair,
180
+ sequence: sequence,
181
+ line: asset
182
+ }
183
+ op_args[:limit] = limit unless limit.nil?
184
+
185
+ tx = DigitalBits::TransactionBuilder.change_trust(
186
+ source_account: source.keypair,
187
+ sequence_number: sequence,
188
+ **op_args
189
+ )
190
+
191
+ envelope = tx.to_envelope(source.keypair)
192
+ submit_transaction(tx_envelope: envelope)
193
+ end
194
+
195
+ # @param [DigitalBits::TransactionEnvelope] tx_envelope
196
+ # @option options [Boolean] :skip_memo_required_check (false)
197
+ def submit_transaction(tx_envelope:, options: {skip_memo_required_check: false})
198
+ unless options[:skip_memo_required_check]
199
+ check_memo_required(tx_envelope)
200
+ end
201
+ @frontier.transactions._post(tx: tx_envelope.to_xdr(:base64))
202
+ end
203
+
204
+ # Required by SEP-0029
205
+ # @param [DigitalBits::TransactionEnvelope] tx_envelope
206
+ def check_memo_required(tx_envelope)
207
+ tx = tx_envelope.tx
208
+
209
+ if tx.is_a?(DigitalBits::FeeBumpTransaction)
210
+ tx = tx.inner_tx.v1!.tx
211
+ end
212
+
213
+ # Check transactions where the .memo field is nil or of type MemoType.memo_none
214
+ if !tx.memo.nil? && tx.memo.type != DigitalBits::MemoType.memo_none
215
+ return
216
+ end
217
+
218
+ destinations = Set.new
219
+ ot = DigitalBits::OperationType
220
+
221
+ tx.operations.each_with_index do |op, idx|
222
+ destination = case op.body.type
223
+ when ot.payment, ot.path_payment_strict_receive, ot.path_payment_strict_send
224
+ op.body.value.destination
225
+ when ot.account_merge
226
+ # There is no AccountMergeOp, op.body is an Operation object
227
+ # and op.body.value is a PublicKey (or AccountID) object.
228
+ op.body.value
229
+ else
230
+ next
231
+ end
232
+
233
+ if destinations.include?(destination) || destination.switch == DigitalBits::CryptoKeyType.key_type_muxed_ed25519
234
+ next
235
+ end
236
+
237
+ destinations.add(destination)
238
+ kp = DigitalBits::KeyPair.from_public_key(destination.value)
239
+
240
+ begin
241
+ info = account_info(kp.address)
242
+ rescue Faraday::ResourceNotFound
243
+ # Don't raise an error if its a 404, but throw one otherwise
244
+ next
245
+ end
246
+ if info.data["config.memo_required"] == "MQ=="
247
+ # MQ== is the base64 encoded string for the string "1"
248
+ raise AccountRequiresMemoError.new("account requires memo", destination, idx)
249
+ end
250
+ end
251
+ end
252
+
253
+ # DEPRECATED: this function has been moved DigitalBits::SEP10.build_challenge_tx and
254
+ # will be removed in the next major version release.
255
+ #
256
+ # A wrapper function for DigitalBits::SEP10::build_challenge_tx.
257
+ #
258
+ # @param server [DigitalBits::KeyPair] Keypair for server's signing account.
259
+ # @param client [DigitalBits::KeyPair] Keypair for the account whishing to authenticate with the server.
260
+ # @param anchor_name [String] Anchor's name to be used in the manage_data key.
261
+ # @param timeout [Integer] Challenge duration (default to 5 minutes).
262
+ #
263
+ # @return [String] A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction.
264
+ def build_challenge_tx(server:, client:, anchor_name:, timeout: 300)
265
+ DigitalBits::SEP10.build_challenge_tx(
266
+ server: server, client: client, anchor_name: anchor_name, timeout: timeout
267
+ )
268
+ end
269
+
270
+ # DEPRECATED: this function has been moved to DigitalBits::SEP10::read_challenge_tx and
271
+ # will be removed in the next major version release.
272
+ #
273
+ # A wrapper function for DigitalBits::SEP10.verify_challenge_transaction
274
+ #
275
+ # @param challenge [String] SEP0010 transaction challenge in base64.
276
+ # @param server [DigitalBits::KeyPair] DigitalBits::KeyPair for server where the challenge was generated.
277
+ #
278
+ # @return [Boolean]
279
+ def verify_challenge_tx(challenge:, server:)
280
+ DigitalBits::SEP10.verify_challenge_tx(challenge_xdr: challenge, server: server)
281
+ true
282
+ end
283
+
284
+ # DEPRECATED: this function has been moved to DigitalBits::SEP10::verify_tx_signed_by and
285
+ # will be removed in the next major version release.
286
+ #
287
+ # @param transaction_envelope [DigitalBits::TransactionEnvelope]
288
+ # @param keypair [DigitalBits::KeyPair]
289
+ #
290
+ # @return [Boolean]
291
+ #
292
+ def verify_tx_signed_by(transaction_envelope:, keypair:)
293
+ DigitalBits::SEP10.verify_tx_signed_by(
294
+ tx_envelope: transaction_envelope, keypair: keypair
295
+ )
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,45 @@
1
+ module DigitalBits
2
+ module Frontier
3
+ class Problem
4
+ def initialize(attributes)
5
+ @attributes = attributes.reverse_merge({
6
+ type: "about:blank",
7
+ title: "Unknown Error",
8
+ status: 500
9
+ })
10
+
11
+ @meta = @attributes.except!(:type, :title, :status, :detail, :instance)
12
+ end
13
+
14
+ # @return [String]
15
+ def type
16
+ @attributes[:type]
17
+ end
18
+
19
+ # @return [String]
20
+ def title
21
+ @attributes[:title]
22
+ end
23
+
24
+ # @return [Integer]
25
+ def status
26
+ @attributes[:status]
27
+ end
28
+
29
+ # @return [String]
30
+ def detail
31
+ @attributes[:detail]
32
+ end
33
+
34
+ # @return [String]
35
+ def instance
36
+ @attributes[:instance]
37
+ end
38
+
39
+ # @return [{String => Object}]
40
+ def meta
41
+ @attributes[:instance]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,274 @@
1
+ module DigitalBits
2
+ class InvalidSep10ChallengeError < StandardError; end
3
+
4
+ class SEP10
5
+ include DigitalBits::DSL
6
+
7
+ # Helper method to create a valid challenge transaction which you can use for DigitalBits Web Authentication.
8
+ #
9
+ # @example
10
+ # server = DigitalBits::KeyPair.random # SIGNING_KEY from your digitalbits.toml
11
+ # user = DigitalBits::KeyPair.from_address('G...')
12
+ # DigitalBits::SEP10.build_challenge_tx(server: server, client: user, domain: 'example.com', timeout: 300)
13
+ #
14
+ # @param server [DigitalBits::KeyPair] server's signing keypair (SIGNING_KEY in service's digitalbits.toml)
15
+ # @param client [DigitalBits::KeyPair] account trying to authenticate with the server
16
+ # @param domain [String] service's domain to be used in the manage_data key
17
+ # @param timeout [Integer] challenge duration (default to 5 minutes)
18
+ #
19
+ # @return [String] A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction.
20
+ #
21
+ def self.build_challenge_tx(server:, client:, domain: nil, timeout: 300, **options)
22
+ if domain.blank? && options.key?(:anchor_name)
23
+ ActiveSupport::Deprecation.new("next release", "digitalbits-sdk").warn <<~MSG
24
+ SEP-10 v2.0.0 requires usage of service home domain instead of anchor name in the challenge transaction.
25
+ Please update your implementation to use `DigitalBits::SEP10.build_challenge_tx(..., home_domain: 'example.com')`.
26
+ Using `anchor_name` parameter makes your service incompatible with SEP10-2.0 clients, support for this parameter
27
+ is deprecated and will be removed in the next major release of digitalbits-base.
28
+ MSG
29
+ domain = options[:anchor_name]
30
+ end
31
+
32
+ now = Time.now.to_i
33
+ time_bounds = DigitalBits::TimeBounds.new(
34
+ min_time: now,
35
+ max_time: now + timeout
36
+ )
37
+
38
+ tb = DigitalBits::TransactionBuilder.new(
39
+ source_account: server,
40
+ sequence_number: 0,
41
+ time_bounds: time_bounds
42
+ )
43
+
44
+ # The value must be 64 bytes long. It contains a 48 byte
45
+ # cryptographic-quality random string encoded using base64 (for a total of
46
+ # 64 bytes after encoding).
47
+ tb.add_operation(
48
+ DigitalBits::Operation.manage_data(
49
+ name: "#{domain} auth",
50
+ value: SecureRandom.base64(48),
51
+ source_account: client
52
+ )
53
+ )
54
+
55
+ if options.key?(:auth_domain)
56
+ tb.add_operation(
57
+ DigitalBits::Operation.manage_data(
58
+ name: "web_auth_domain",
59
+ value: options[:auth_domain],
60
+ source_account: server
61
+ )
62
+ )
63
+ end
64
+
65
+ tb.build.to_envelope(server).to_xdr(:base64)
66
+ end
67
+
68
+ # Reads a SEP 10 challenge transaction and returns the decoded transaction envelope and client account ID contained within.
69
+ #
70
+ # It also verifies that transaction is signed by the server.
71
+ #
72
+ # It does not verify that the transaction has been signed by the client or
73
+ # that any signatures other than the servers on the transaction are valid.
74
+ # Use either {.verify_challenge_tx_threshold} or {.verify_challenge_tx_signers} to completely verify
75
+ # the signed challenge
76
+ #
77
+ # @example
78
+ # sep10 = DigitalBits::SEP10
79
+ # server = DigitalBits::KeyPair.random # this should be the SIGNING_KEY from your digitalbits.toml
80
+ # challenge = sep10.build_challenge_tx(server: server, client: user, domain: domain, timeout: timeout)
81
+ # envelope, client_address = sep10.read_challenge_tx(server: server, challenge_xdr: challenge)
82
+ #
83
+ # @param challenge_xdr [String] SEP0010 transaction challenge in base64.
84
+ # @param server [DigitalBits::KeyPair] keypair for server where the challenge was generated.
85
+ #
86
+ # @return [Array(DigitalBits::TransactionEnvelope, String)]
87
+ def self.read_challenge_tx(server:, challenge_xdr:, **options)
88
+ envelope = DigitalBits::TransactionEnvelope.from_xdr(challenge_xdr, "base64")
89
+ transaction = envelope.tx
90
+
91
+ if transaction.seq_num != 0
92
+ raise InvalidSep10ChallengeError, "The transaction sequence number should be zero"
93
+ end
94
+
95
+ if transaction.source_account != server.muxed_account
96
+ raise InvalidSep10ChallengeError, "The transaction source account is not equal to the server's account"
97
+ end
98
+
99
+ if transaction.operations.size < 1
100
+ raise InvalidSep10ChallengeError, "The transaction should contain at least one operation"
101
+ end
102
+
103
+ auth_op, *rest_ops = transaction.operations
104
+ client_account_id = auth_op.source_account
105
+
106
+ auth_op_body = auth_op.body.value
107
+
108
+ if client_account_id.blank?
109
+ raise InvalidSep10ChallengeError, "The transaction's operation should contain a source account"
110
+ end
111
+
112
+ if auth_op.body.arm != :manage_data_op
113
+ raise InvalidSep10ChallengeError, "The transaction's first operation should be manageData"
114
+ end
115
+
116
+ if options.key?(:domain) && auth_op_body.data_name != "#{options[:domain]} auth"
117
+ raise InvalidSep10ChallengeError, "The transaction's operation data name is invalid"
118
+ end
119
+
120
+ if auth_op_body.data_value.unpack1("m").size != 48
121
+ raise InvalidSep10ChallengeError, "The transaction's operation value should be a 64 bytes base64 random string"
122
+ end
123
+
124
+ rest_ops.each do |op|
125
+ body = op.body
126
+
127
+ if body.arm != :manage_data_op
128
+ raise InvalidSep10ChallengeError, "The transaction has operations that are not of type 'manageData'"
129
+ elsif op.source_account != server.muxed_account
130
+ raise InvalidSep10ChallengeError, "The transaction has operations that are unrecognized"
131
+ else
132
+ op_params = body.value
133
+ if op_params.data_name == "web_auth_domain" && options.key?(:auth_domain) && op_params.data_value != options[:auth_domain]
134
+ raise InvalidSep10ChallengeError, "The transaction has 'manageData' operation with 'web_auth_domain' key and invalid value"
135
+ end
136
+ end
137
+ end
138
+
139
+ unless verify_tx_signed_by(tx_envelope: envelope, keypair: server)
140
+ raise InvalidSep10ChallengeError, "The transaction is not signed by the server"
141
+ end
142
+
143
+ time_bounds = transaction.time_bounds
144
+ now = Time.now.to_i
145
+
146
+ if time_bounds.blank? || !now.between?(time_bounds.min_time, time_bounds.max_time)
147
+ raise InvalidSep10ChallengeError, "The transaction has expired"
148
+ end
149
+
150
+ # Mirror the return type of the other SDK's and return a string
151
+ client_kp = DigitalBits::KeyPair.from_public_key(client_account_id.ed25519!)
152
+
153
+ [envelope, client_kp.address]
154
+ end
155
+
156
+ # Verifies that for a SEP 10 challenge transaction all signatures on the transaction
157
+ # are accounted for and that the signatures meet a threshold on an account. A
158
+ # transaction is verified if it is signed by the server account, and all other
159
+ # signatures match a signer that has been provided as an argument, and those
160
+ # signatures meet a threshold on the account.
161
+ #
162
+ # @param server [DigitalBits::KeyPair] keypair for server's account.
163
+ # @param challenge_xdr [String] SEP0010 challenge transaction in base64.
164
+ # @param signers [{String => Integer}] The signers of client account.
165
+ # @param threshold [Integer] The medThreshold on the client account.
166
+ #
167
+ # @raise InvalidSep10ChallengeError if the transaction has unrecognized signatures (only server's
168
+ # signing key and keypairs found in the `signing` argument are recognized) or total weight of
169
+ # the signers does not meet the `threshold`
170
+ #
171
+ # @return [<String>] subset of input signers who have signed `challenge_xdr`
172
+ def self.verify_challenge_tx_threshold(server:, challenge_xdr:, signers:, threshold:)
173
+ signers_found = verify_challenge_tx_signers(
174
+ server: server, challenge_xdr: challenge_xdr, signers: signers.keys
175
+ )
176
+
177
+ total_weight = signers.values_at(*signers_found).sum
178
+
179
+ if total_weight < threshold
180
+ raise InvalidSep10ChallengeError, "signers with weight #{total_weight} do not meet threshold #{threshold}."
181
+ end
182
+
183
+ signers_found
184
+ end
185
+
186
+ # Verifies that for a SEP 10 challenge transaction all signatures on the transaction are accounted for.
187
+ #
188
+ # A transaction is verified if it is signed by the server account, and all other signatures match a signer
189
+ # that has been provided as an argument. Additional signers can be provided that do not have a signature,
190
+ # but all signatures must be matched to a signer for verification to succeed.
191
+ #
192
+ # If verification succeeds a list of signers that were found is returned, excluding the server account ID.
193
+ #
194
+ # @param server [DigitalBits::Keypair] server's signing key
195
+ # @param challenge_xdr [String] SEP0010 transaction challenge transaction in base64.
196
+ # @param signers [<String>] The signers of client account.
197
+ #
198
+ # @raise InvalidSep10ChallengeError one or more signatures in the transaction are not identifiable
199
+ # as the server account or one of the signers provided in the arguments
200
+ #
201
+ # @return [<String>] subset of input signers who have signed `challenge_xdr`
202
+ def self.verify_challenge_tx_signers(server:, challenge_xdr:, signers:)
203
+ raise InvalidSep10ChallengeError, "no signers provided" if signers.empty?
204
+
205
+ te, _ = read_challenge_tx(server: server, challenge_xdr: challenge_xdr)
206
+
207
+ # ignore non-G signers and server's own address
208
+ client_signers = signers.select { |s| s =~ /G[A-Z0-9]{55}/ && s != server.address }.to_set
209
+ raise InvalidSep10ChallengeError, "at least one regular signer must be provided" if client_signers.empty?
210
+
211
+ # verify all signatures in one pass
212
+ client_signers.add(server.address)
213
+ signers_found = verify_tx_signatures(tx_envelope: te, signers: client_signers)
214
+
215
+ # ensure server signed transaction and remove it
216
+ unless signers_found.delete?(server.address)
217
+ raise InvalidSep10ChallengeError, "Transaction not signed by server: #{server.address}"
218
+ end
219
+
220
+ # Confirm we matched signatures to the client signers.
221
+ if signers_found.empty?
222
+ raise InvalidSep10ChallengeError, "Transaction not signed by any client signer."
223
+ end
224
+
225
+ # Confirm all signatures were consumed by a signer.
226
+ if signers_found.size != te.signatures.length - 1
227
+ raise InvalidSep10ChallengeError, "Transaction has unrecognized signatures."
228
+ end
229
+
230
+ signers_found
231
+ end
232
+
233
+ # Verifies every signer passed matches a signature on the transaction exactly once,
234
+ # returning a list of unique signers that were found to have signed the transaction.
235
+ #
236
+ # @param tx_envelope [DigitalBits::TransactionEnvelope] SEP0010 transaction challenge transaction envelope.
237
+ # @param signers [<String>] The signers of client account.
238
+ #
239
+ # @return [Set<DigitalBits::KeyPair>]
240
+ def self.verify_tx_signatures(tx_envelope:, signers:)
241
+ signatures = tx_envelope.signatures
242
+ if signatures.empty?
243
+ raise InvalidSep10ChallengeError, "Transaction has no signatures."
244
+ end
245
+
246
+ tx_hash = tx_envelope.tx.hash
247
+ to_keypair = DigitalBits::DSL.method(:KeyPair)
248
+ keys_by_hint = signers.map(&to_keypair).index_by(&:signature_hint)
249
+
250
+ tx_envelope.signatures.each.with_object(Set.new) do |sig, result|
251
+ key = keys_by_hint.delete(sig.hint)
252
+ result.add(key.address) if key&.verify(sig.signature, tx_hash)
253
+ end
254
+ end
255
+
256
+ # Verifies if a DigitalBits::TransactionEnvelope was signed by the given DigitalBits::KeyPair
257
+ #
258
+ # @example
259
+ # DigitalBits::SEP10.verify_tx_signed_by(tx_envelope: envelope, keypair: keypair)
260
+ #
261
+ # @param tx_envelope [DigitalBits::TransactionEnvelope]
262
+ # @param keypair [DigitalBits::KeyPair]
263
+ #
264
+ # @return [Boolean]
265
+ def self.verify_tx_signed_by(tx_envelope:, keypair:)
266
+ tx_hash = tx_envelope.tx.hash
267
+ tx_envelope.signatures.any? do |sig|
268
+ next if sig.hint != keypair.signature_hint
269
+
270
+ keypair.verify(sig.signature, tx_hash)
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,25 @@
1
+ module DigitalBits
2
+ class TransactionPage
3
+ include Enumerable
4
+
5
+ # @param [Hyperclient::Link] resource
6
+ def initialize(resource)
7
+ @resource = resource
8
+ end
9
+
10
+ def each
11
+ @resource.records.each do |tx|
12
+ yield tx if block_given?
13
+ end
14
+ end
15
+
16
+ # @return [DigitalBits::TransactionPage]
17
+ def next_page
18
+ self.class.new(@resource.next)
19
+ end
20
+
21
+ def next_page!
22
+ @resource = @resource.next
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: digitalbits-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.27.1
5
+ platform: ruby
6
+ authors:
7
+ - XDB Foundation
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-08-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: digitalbits-base
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.27.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.27.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.0.0
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '7.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 5.0.0
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: excon
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.71.0
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.71.0
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: hyperclient
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 0.7.0
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '2.0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.7.0
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '2.0'
87
+ - !ruby/object:Gem::Dependency
88
+ name: toml-rb
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 1.1.1
94
+ - - "<"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ type: :runtime
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 1.1.1
104
+ - - "<"
105
+ - !ruby/object:Gem::Version
106
+ version: '3.0'
107
+ - !ruby/object:Gem::Dependency
108
+ name: bundler
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '2.2'
114
+ type: :development
115
+ prerelease: false
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '2.2'
121
+ - !ruby/object:Gem::Dependency
122
+ name: rake
123
+ requirement: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '13'
128
+ type: :development
129
+ prerelease: false
130
+ version_requirements: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '13'
135
+ - !ruby/object:Gem::Dependency
136
+ name: rspec
137
+ requirement: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '3.9'
142
+ type: :development
143
+ prerelease: false
144
+ version_requirements: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: '3.9'
149
+ description:
150
+ email:
151
+ executables: []
152
+ extensions: []
153
+ extra_rdoc_files:
154
+ - README.md
155
+ - LICENSE
156
+ files:
157
+ - LICENSE
158
+ - README.md
159
+ - lib/digitalbits-sdk.rb
160
+ - lib/digitalbits/account.rb
161
+ - lib/digitalbits/amount.rb
162
+ - lib/digitalbits/client.rb
163
+ - lib/digitalbits/frontier/problem.rb
164
+ - lib/digitalbits/sep10.rb
165
+ - lib/digitalbits/transaction_page.rb
166
+ homepage: https://github.com/xdbfoundation/ruby-digitalbits-sdk
167
+ licenses:
168
+ - Apache-2.0
169
+ metadata:
170
+ bug_tracker_uri: https://github.com/xdbfoundation/ruby-digitalbits-sdk/issues
171
+ changelog_uri: https://github.com/xdbfoundation/ruby-digitalbits-sdk/blob/v0.27.1/sdk/CHANGELOG.md
172
+ documentation_uri: https://rubydoc.info/gems/digitalbits-sdk/0.27.1/
173
+ github_repo: ssh://github.com/xdbfoundation/ruby-digitalbits-sdk
174
+ homepage_uri: https://github.com/xdbfoundation/ruby-digitalbits-sdk/tree/main/sdk
175
+ source_code_uri: https://github.com/xdbfoundation/ruby-digitalbits-sdk/tree/v0.27.1/sdk
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: 2.5.0
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubygems_version: 3.1.6
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: DigitalBits client library
195
+ test_files: []