digitalbits-sdk 0.27.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +60 -0
- data/lib/digitalbits-sdk.rb +21 -0
- data/lib/digitalbits/account.rb +89 -0
- data/lib/digitalbits/amount.rb +36 -0
- data/lib/digitalbits/client.rb +298 -0
- data/lib/digitalbits/frontier/problem.rb +45 -0
- data/lib/digitalbits/sep10.rb +274 -0
- data/lib/digitalbits/transaction_page.rb +25 -0
- metadata +195 -0
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: []
|